mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
41 Commits
snapshot-1
...
snapshot-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
112
.github/workflows/build.yml
vendored
112
.github/workflows/build.yml
vendored
@@ -18,55 +18,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'
|
||||
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 +58,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 +66,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:
|
||||
@@ -110,7 +85,6 @@ jobs:
|
||||
with:
|
||||
version: '6.8.1'
|
||||
cache: true
|
||||
aqtversion: '==3.1.21'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -118,15 +92,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 +136,26 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: Reclass-linux64-qt6
|
||||
path: Reclass-linux64-qt6.AppImage
|
||||
|
||||
release:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: [windows, linux]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Get date tag
|
||||
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 +164,8 @@ 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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,3 +11,6 @@ build/
|
||||
*.suo
|
||||
.vs/
|
||||
CMakeUserPresets.json
|
||||
plugins/RcNetPluginCompatLayer/bridge/obj
|
||||
plugins/RcNetPluginCompatLayer/bridge/bin
|
||||
.cache
|
||||
|
||||
138
CMakeLists.txt
138
CMakeLists.txt
@@ -31,6 +31,15 @@ endif()
|
||||
|
||||
find_package(QScintilla REQUIRED)
|
||||
|
||||
# RawPDB — direct PDB file reader (no DIA SDK / msdia140.dll dependency)
|
||||
file(GLOB RAW_PDB_SRCS third_party/raw_pdb/src/*.cpp)
|
||||
add_library(raw_pdb STATIC ${RAW_PDB_SRCS})
|
||||
target_include_directories(raw_pdb PUBLIC third_party/raw_pdb/src)
|
||||
target_compile_features(raw_pdb PRIVATE cxx_std_11)
|
||||
if(WIN32)
|
||||
target_link_libraries(raw_pdb PRIVATE rpcrt4)
|
||||
endif()
|
||||
|
||||
add_executable(Reclass
|
||||
src/main.cpp
|
||||
src/editor.h
|
||||
@@ -60,12 +69,16 @@ 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/mainwindow.h
|
||||
src/optionsdialog.h
|
||||
src/optionsdialog.cpp
|
||||
@@ -73,6 +86,8 @@ add_executable(Reclass
|
||||
src/titlebar.cpp
|
||||
src/mcp/mcp_bridge.h
|
||||
src/mcp/mcp_bridge.cpp
|
||||
src/addressparser.h
|
||||
src/addressparser.cpp
|
||||
src/disasm.h
|
||||
src/disasm.cpp
|
||||
third_party/fadec/decode.c
|
||||
@@ -92,7 +107,7 @@ target_link_libraries(Reclass PRIVATE
|
||||
${_QT_WINEXTRAS}
|
||||
)
|
||||
if(WIN32)
|
||||
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi)
|
||||
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi raw_pdb)
|
||||
endif()
|
||||
|
||||
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
|
||||
@@ -154,17 +169,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,42 +195,63 @@ 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)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_import_pdb tests/test_import_pdb.cpp
|
||||
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_import_pdb PRIVATE src)
|
||||
target_link_libraries(test_import_pdb PRIVATE
|
||||
${QT}::Core ${QT}::Test raw_pdb)
|
||||
add_test(NAME test_import_pdb COMMAND test_import_pdb)
|
||||
|
||||
add_executable(bench_import_pdb tests/bench_import_pdb.cpp
|
||||
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(bench_import_pdb PRIVATE src)
|
||||
target_link_libraries(bench_import_pdb PRIVATE
|
||||
${QT}::Core ${QT}::Test raw_pdb)
|
||||
add_test(NAME bench_import_pdb COMMAND bench_import_pdb)
|
||||
endif()
|
||||
|
||||
# ── UI tests (require Qt::Widgets / QScintilla / display — skip on headless CI) ──
|
||||
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
|
||||
if(BUILD_UI_TESTS)
|
||||
|
||||
add_executable(test_controller tests/test_controller.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
@@ -229,7 +265,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
|
||||
add_executable(test_validation tests/test_validation.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
@@ -243,7 +279,7 @@ if(BUILD_TESTING)
|
||||
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
|
||||
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})
|
||||
@@ -256,8 +292,22 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
|
||||
add_executable(test_source_management tests/test_source_management.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_source_management PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_management PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_management PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_management COMMAND test_source_management)
|
||||
|
||||
add_executable(test_editor tests/test_editor.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp
|
||||
src/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)
|
||||
@@ -267,7 +317,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
|
||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp)
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_rendered_view PRIVATE src)
|
||||
target_link_libraries(test_rendered_view PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
@@ -275,7 +325,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||
|
||||
add_executable(test_new_features tests/test_new_features.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
@@ -289,7 +339,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME test_new_features COMMAND test_new_features)
|
||||
|
||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
@@ -302,6 +352,19 @@ if(BUILD_TESTING)
|
||||
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
|
||||
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
@@ -309,6 +372,21 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||
|
||||
add_executable(test_source_provider tests/test_source_provider.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
|
||||
src/resources.qrc)
|
||||
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||
@@ -318,13 +396,18 @@ if(BUILD_TESTING)
|
||||
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
|
||||
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)
|
||||
add_executable(test_com_security tests/test_com_security.cpp)
|
||||
target_link_libraries(test_com_security PRIVATE dbgeng ole32 version)
|
||||
add_test(NAME test_com_security COMMAND test_com_security)
|
||||
target_link_libraries(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)
|
||||
@@ -342,6 +425,7 @@ if(BUILD_TESTING)
|
||||
endif() # BUILD_UI_TESTS
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
add_subdirectory(plugins/RemoteProcessMemory)
|
||||
if(WIN32)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||
|
||||
154
README.md
154
README.md
@@ -1,43 +1,139 @@
|
||||
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>A complete overhaul of the popular "reclassing" tools**
|
||||
|
||||
[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, 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
|
||||
- **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
|
||||
- **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
|
||||
- **MCP bridge** — expose all tool functionality to AI clients via Model Context Protocol
|
||||
- **Plugin system** — extend with custom data source providers via DLL plugins; following ship by default
|
||||
- **Process plugin** — access memory of live processes on Windows and Linux
|
||||
- **WinDbg plugin** — access data sources live in WinDbg debugging sessions
|
||||
- **ReClass.NET compatibility layer** — load existing .NET and native ReClass.NET plugins
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Process memory section enumeration
|
||||
- [ ] Address parser auto-complete
|
||||
- [ ] Safe mode
|
||||
- [ ] File import for other Reclass instances
|
||||
- [ ] Expose UI functionality to plugins
|
||||
- [ ] iOS/macOS support
|
||||
- [ ] Display RTTI information
|
||||
|
||||
---
|
||||
|
||||
## Data Sources
|
||||
|
||||
- **File** — open any binary file and inspect its contents as structured data
|
||||
- **Process** — attach to a live process and read its memory in real time
|
||||
- **Remote Process** — read another process's memory via shared memory
|
||||
- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## MCP Integration
|
||||
|
||||
Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `ReclassMcpBridge`. The server does not start by default and can be toggled 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",
|
||||
"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** with MinGW — [Qt Online Installer](https://doc.qt.io/qt-6/qt-online-installation.html) (select MinGW kit + CMake/Ninja from the Tools section)
|
||||
- **CMake 3.20+** — [cmake.org](https://cmake.org/download/) (bundled with Qt)
|
||||
- **Ninja** — bundled with the Qt installer
|
||||
|
||||
2. Quick Build (relies on powershell| for manual build skip to step 3)
|
||||
### Quick Build
|
||||
|
||||
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
|
||||
cd Reclass
|
||||
.\scripts\build_qscintilla.ps1
|
||||
.\scripts\build.ps1
|
||||
^ script above tries to autodetect Qt install (as we learned not everyone installs to C:/Qt/)
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
|
||||
cd Reclass
|
||||
.\scripts\build_qscintilla.ps1
|
||||
.\scripts\build.ps1
|
||||
```
|
||||
|
||||
3. Manual Build
|
||||
The build script auto-detects your Qt install location.
|
||||
|
||||
Step by step for peoplewho want to run commands themselves:
|
||||
1. Clone with --recurse-submodules (+ fallback git submodule update --init --recursive)
|
||||
2. Build QScintilla: qmake + mingw32-make in third_party/qscintilla/src
|
||||
3. CMake configure + build with -DCMAKE_PREFIX_PATH
|
||||
4. optionallly windeployqt the exe
|
||||
### Manual Build
|
||||
|
||||
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
|
||||
2. Build QScintilla: `qmake` + `mingw32-make` in `third_party/qscintilla/src`
|
||||
3. Configure and build:
|
||||
```bash
|
||||
cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/mingw_64
|
||||
cmake --build build
|
||||
```
|
||||
4. Optionally run `windeployqt` on the output executable
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Alternatives
|
||||
|
||||
- ReClass.NET (reclass.net) - https://github.com/ReClassNET/ReClass.NET
|
||||
- ReClassEx - https://github.com/ajkhoury/ReClassEx
|
||||
- [ReClass.NET](https://github.com/ReClassNET/ReClass.NET)
|
||||
- [ReClassEx](https://github.com/ajkhoury/ReClassEx)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>MIT License</sub>
|
||||
</div>
|
||||
|
||||
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 |
@@ -284,6 +284,15 @@ void ProcessMemoryProvider::cacheModules()
|
||||
|
||||
#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
|
||||
|
||||
@@ -24,6 +24,7 @@ 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; }
|
||||
|
||||
@@ -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)
|
||||
927
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp
Normal file
927
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp
Normal file
@@ -0,0 +1,927 @@
|
||||
#include "RemoteProcessMemoryPlugin.h"
|
||||
#include "rcx_rpc_protocol.h"
|
||||
#include "../../src/processpicker.h"
|
||||
|
||||
#include <QStyle>
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QPixmap>
|
||||
#include <QImage>
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32)
|
||||
#include <QtWin>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
# include <tlhelp32.h>
|
||||
# include <psapi.h>
|
||||
# include <shellapi.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
# include <fcntl.h>
|
||||
# include <dlfcn.h>
|
||||
# include <sys/mman.h>
|
||||
# include <sys/wait.h>
|
||||
# include <sys/ptrace.h>
|
||||
# include <sys/user.h>
|
||||
# include <semaphore.h>
|
||||
# include <signal.h>
|
||||
# include <link.h>
|
||||
# include <climits>
|
||||
# include <cstring>
|
||||
# include <fstream>
|
||||
# include <sstream>
|
||||
#endif
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* IPC Client
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
struct IpcClient {
|
||||
#ifdef _WIN32
|
||||
HANDLE hShm = nullptr;
|
||||
HANDLE hReqEvent = nullptr;
|
||||
HANDLE hRspEvent = nullptr;
|
||||
#else
|
||||
int shmFd = -1;
|
||||
sem_t* reqSem = SEM_FAILED;
|
||||
sem_t* rspSem = SEM_FAILED;
|
||||
char shmNameBuf[128] = {};
|
||||
char reqNameBuf[128] = {};
|
||||
char rspNameBuf[128] = {};
|
||||
#endif
|
||||
void* mappedView = nullptr;
|
||||
QMutex mutex;
|
||||
bool connected = false;
|
||||
|
||||
~IpcClient() { disconnect(); }
|
||||
|
||||
/* ── connect / disconnect ──────────────────────────────────────── */
|
||||
|
||||
bool connect(uint32_t pid, int timeoutMs = 5000)
|
||||
{
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
/* poll for shared memory to appear (payload creating it) */
|
||||
auto deadline = GetTickCount64() + (uint64_t)timeoutMs;
|
||||
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
|
||||
if (GetTickCount64() >= deadline) return false;
|
||||
Sleep(10);
|
||||
}
|
||||
|
||||
mappedView = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
|
||||
if (!mappedView) { CloseHandle(hShm); hShm = nullptr; return false; }
|
||||
|
||||
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
|
||||
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
|
||||
if (!hReqEvent || !hRspEvent) { disconnect(); return false; }
|
||||
#else
|
||||
strncpy(shmNameBuf, shmName, sizeof(shmNameBuf) - 1);
|
||||
strncpy(reqNameBuf, reqName, sizeof(reqNameBuf) - 1);
|
||||
strncpy(rspNameBuf, rspName, sizeof(rspNameBuf) - 1);
|
||||
|
||||
/* poll for shared memory */
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (true) {
|
||||
shmFd = shm_open(shmName, O_RDWR, 0);
|
||||
if (shmFd >= 0) break;
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) return false;
|
||||
usleep(10000);
|
||||
}
|
||||
|
||||
mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, shmFd, 0);
|
||||
if (mappedView == MAP_FAILED) { mappedView = nullptr; close(shmFd); shmFd = -1; return false; }
|
||||
|
||||
reqSem = sem_open(reqName, 0);
|
||||
rspSem = sem_open(rspName, 0);
|
||||
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) { disconnect(); return false; }
|
||||
#endif
|
||||
|
||||
/* wait for payloadReady */
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
#ifdef _WIN32
|
||||
while (!hdr->payloadReady) {
|
||||
if (GetTickCount64() >= deadline) { disconnect(); return false; }
|
||||
Sleep(5);
|
||||
}
|
||||
#else
|
||||
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) { disconnect(); return false; }
|
||||
usleep(5000);
|
||||
}
|
||||
#endif
|
||||
|
||||
connected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnect()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (mappedView) { UnmapViewOfFile(mappedView); mappedView = nullptr; }
|
||||
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
|
||||
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
|
||||
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
|
||||
#else
|
||||
if (mappedView) { munmap(mappedView, RCX_RPC_SHM_SIZE); mappedView = nullptr; }
|
||||
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
|
||||
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
|
||||
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
|
||||
#endif
|
||||
connected = false;
|
||||
}
|
||||
|
||||
/* ── low-level RPC round-trip ──────────────────────────────────── */
|
||||
|
||||
bool signalAndWait(int timeoutMs = 2000)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
SetEvent(hReqEvent);
|
||||
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
|
||||
#else
|
||||
sem_post(reqSem);
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_sec += timeoutMs / 1000;
|
||||
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
|
||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||
return sem_timedwait(rspSem, &ts) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── public API ────────────────────────────────────────────────── */
|
||||
|
||||
bool readSingle(uint64_t addr, void* buf, int len)
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected || len <= 0) return false;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_READ_BATCH;
|
||||
hdr->requestCount = 1;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
auto* entry = reinterpret_cast<RcxRpcReadEntry*>(data);
|
||||
entry->address = addr;
|
||||
entry->length = (uint32_t)len;
|
||||
entry->dataOffset = sizeof(RcxRpcReadEntry);
|
||||
|
||||
if (!signalAndWait()) { connected = false; return false; }
|
||||
|
||||
memcpy(buf, data + entry->dataOffset, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeSingle(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected || len <= 0) return false;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_WRITE;
|
||||
hdr->writeAddress = addr;
|
||||
hdr->writeLength = (uint32_t)len;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
memcpy(data, buf, len);
|
||||
|
||||
if (!signalAndWait()) { connected = false; return false; }
|
||||
|
||||
return hdr->status == RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
QVector<RemoteProcessProvider::ModuleInfo> enumerateModules()
|
||||
{
|
||||
QVector<RemoteProcessProvider::ModuleInfo> result;
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected) return result;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_ENUM_MODULES;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
if (!signalAndWait()) { connected = false; return result; }
|
||||
if (hdr->status != RCX_RPC_STATUS_OK) return result;
|
||||
|
||||
uint32_t count = hdr->responseCount;
|
||||
result.reserve((int)count);
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto* entry = reinterpret_cast<const RcxRpcModuleEntry*>(
|
||||
data + i * sizeof(RcxRpcModuleEntry));
|
||||
|
||||
QString modName;
|
||||
#ifdef _WIN32
|
||||
modName = QString::fromWCharArray(
|
||||
reinterpret_cast<const wchar_t*>(data + entry->nameOffset),
|
||||
(int)(entry->nameLength / sizeof(wchar_t)));
|
||||
#else
|
||||
modName = QString::fromUtf8(
|
||||
reinterpret_cast<const char*>(data + entry->nameOffset),
|
||||
(int)entry->nameLength);
|
||||
#endif
|
||||
result.append({modName, entry->base, entry->size});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ping()
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected) return false;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
hdr->command = RPC_CMD_PING;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
if (!signalAndWait()) { connected = false; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown()
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected) return;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
hdr->command = RPC_CMD_SHUTDOWN;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
signalAndWait(500);
|
||||
connected = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* RemoteProcessProvider
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
RemoteProcessProvider::RemoteProcessProvider(
|
||||
uint32_t pid, const QString& processName,
|
||||
std::shared_ptr<IpcClient> ipc)
|
||||
: m_pid(pid)
|
||||
, m_processName(processName)
|
||||
, m_connected(ipc && ipc->connected)
|
||||
, m_base(0)
|
||||
, m_ipc(std::move(ipc))
|
||||
{
|
||||
if (m_connected)
|
||||
cacheModules();
|
||||
}
|
||||
|
||||
RemoteProcessProvider::~RemoteProcessProvider() = default;
|
||||
|
||||
bool RemoteProcessProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (!m_connected || len <= 0) return false;
|
||||
bool ok = m_ipc->readSingle(addr, buf, len);
|
||||
if (!ok) {
|
||||
memset(buf, 0, (size_t)len);
|
||||
/* update connectivity flag through mutable ipc */
|
||||
const_cast<RemoteProcessProvider*>(this)->m_connected = m_ipc->connected;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
int RemoteProcessProvider::size() const
|
||||
{
|
||||
return m_connected ? 0x10000 : 0;
|
||||
}
|
||||
|
||||
bool RemoteProcessProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (!m_connected || len <= 0) return false;
|
||||
bool ok = m_ipc->writeSingle(addr, buf, len);
|
||||
if (!ok) m_connected = m_ipc->connected;
|
||||
return ok;
|
||||
}
|
||||
|
||||
QString RemoteProcessProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (addr >= mod.base && addr < mod.base + mod.size) {
|
||||
uint64_t off = addr - mod.base;
|
||||
return QStringLiteral("%1+0x%2")
|
||||
.arg(mod.name)
|
||||
.arg(off, 0, 16, QChar('0'));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
uint64_t RemoteProcessProvider::symbolToAddress(const QString& n) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (mod.name.compare(n, Qt::CaseInsensitive) == 0)
|
||||
return mod.base;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RemoteProcessProvider::cacheModules()
|
||||
{
|
||||
m_modules = m_ipc->enumerateModules();
|
||||
if (!m_modules.isEmpty())
|
||||
m_base = m_modules.first().base;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Injection helpers
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
namespace {
|
||||
|
||||
/* Resolve payload DLL/SO path next to this plugin DLL/SO */
|
||||
static QString payloadPath()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
HMODULE hSelf = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&payloadPath), &hSelf);
|
||||
WCHAR buf[MAX_PATH];
|
||||
GetModuleFileNameW(hSelf, buf, MAX_PATH);
|
||||
QFileInfo fi(QString::fromWCharArray(buf));
|
||||
return fi.absolutePath() + QStringLiteral("/rcx_payload.dll");
|
||||
#else
|
||||
Dl_info info;
|
||||
dladdr(reinterpret_cast<void*>(&payloadPath), &info);
|
||||
QFileInfo fi(QString::fromUtf8(info.dli_fname));
|
||||
return fi.absolutePath() + QStringLiteral("/rcx_payload.so");
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* ── Windows injection: CreateRemoteThread + LoadLibraryA ─────────── */
|
||||
|
||||
static bool injectPayload(uint32_t pid, QString* errorMsg)
|
||||
{
|
||||
QString path = payloadPath();
|
||||
QByteArray pathUtf8 = QDir::toNativeSeparators(path).toLocal8Bit();
|
||||
|
||||
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
|
||||
if (!hProc) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("OpenProcess failed (error %1).\n"
|
||||
"Try running as Administrator.")
|
||||
.arg(GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
/* allocate + write path string in target */
|
||||
SIZE_T pathLen = (SIZE_T)(pathUtf8.size() + 1);
|
||||
void* remotePath = VirtualAllocEx(hProc, nullptr, pathLen,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (!remotePath) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("VirtualAllocEx failed.");
|
||||
CloseHandle(hProc);
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteProcessMemory(hProc, remotePath, pathUtf8.constData(), pathLen, nullptr);
|
||||
|
||||
/* Step 1: LoadLibraryA — loads the DLL (DllMain is minimal) */
|
||||
HMODULE hK32 = GetModuleHandleA("kernel32.dll");
|
||||
auto pLoadLib = reinterpret_cast<LPTHREAD_START_ROUTINE>(
|
||||
GetProcAddress(hK32, "LoadLibraryA"));
|
||||
|
||||
HANDLE hThread = CreateRemoteThread(hProc, nullptr, 0,
|
||||
pLoadLib, remotePath, 0, nullptr);
|
||||
if (!hThread) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("CreateRemoteThread failed (error %1).")
|
||||
.arg(GetLastError());
|
||||
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
|
||||
CloseHandle(hProc);
|
||||
return false;
|
||||
}
|
||||
|
||||
WaitForSingleObject(hThread, 10000);
|
||||
|
||||
DWORD exitCode = 0;
|
||||
GetExitCodeThread(hThread, &exitCode);
|
||||
CloseHandle(hThread);
|
||||
|
||||
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
|
||||
|
||||
if (exitCode == 0) {
|
||||
CloseHandle(hProc);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n"
|
||||
"Ensure rcx_payload.dll is in: %1").arg(path);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Step 2: Call RcxPayloadInit() — safe to create timer queues now
|
||||
(loader lock is no longer held after LoadLibrary returned) */
|
||||
HMODULE hPayloadRemote = (HMODULE)(uintptr_t)exitCode;
|
||||
auto pGetProcAddr = reinterpret_cast<FARPROC(WINAPI*)(HMODULE, LPCSTR)>(
|
||||
GetProcAddress(hK32, "GetProcAddress"));
|
||||
|
||||
/* Write "RcxPayloadInit\0" into target, call GetProcAddress remotely */
|
||||
const char initName[] = "RcxPayloadInit";
|
||||
void* remoteInitName = VirtualAllocEx(hProc, nullptr, sizeof(initName),
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (remoteInitName) {
|
||||
WriteProcessMemory(hProc, remoteInitName, initName, sizeof(initName), nullptr);
|
||||
|
||||
/* We need to call GetProcAddress(hPayload, "RcxPayloadInit") then call the result.
|
||||
Simpler approach: write small shellcode that does both calls. */
|
||||
uint8_t shellcode[128];
|
||||
int off = 0;
|
||||
|
||||
/* sub rsp, 40 ; shadow space + alignment */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xEC; shellcode[off++] = 0x28;
|
||||
/* mov rcx, hPayloadRemote ; first arg = module handle */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0xB9;
|
||||
uint64_t hMod = (uint64_t)(uintptr_t)hPayloadRemote;
|
||||
memcpy(shellcode + off, &hMod, 8); off += 8;
|
||||
/* mov rdx, remoteInitName ; second arg = "RcxPayloadInit" */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0xBA;
|
||||
uint64_t pName = (uint64_t)(uintptr_t)remoteInitName;
|
||||
memcpy(shellcode + off, &pName, 8); off += 8;
|
||||
/* mov rax, GetProcAddress */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0xB8;
|
||||
uint64_t pGPA = (uint64_t)(uintptr_t)pGetProcAddr;
|
||||
memcpy(shellcode + off, &pGPA, 8); off += 8;
|
||||
/* call rax ; rax = RcxPayloadInit */
|
||||
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
|
||||
/* test rax, rax */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0x85; shellcode[off++] = 0xC0;
|
||||
/* jz skip (jump over the call if null) */
|
||||
shellcode[off++] = 0x74; shellcode[off++] = 0x02;
|
||||
/* call rax ; RcxPayloadInit() */
|
||||
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
|
||||
/* skip: add rsp, 40 */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xC4; shellcode[off++] = 0x28;
|
||||
/* ret */
|
||||
shellcode[off++] = 0xC3;
|
||||
|
||||
void* remoteCode = VirtualAllocEx(hProc, nullptr, (SIZE_T)off,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||
if (remoteCode) {
|
||||
WriteProcessMemory(hProc, remoteCode, shellcode, (SIZE_T)off, nullptr);
|
||||
|
||||
HANDLE hThread2 = CreateRemoteThread(hProc, nullptr, 0,
|
||||
(LPTHREAD_START_ROUTINE)remoteCode, nullptr, 0, nullptr);
|
||||
if (hThread2) {
|
||||
WaitForSingleObject(hThread2, 10000);
|
||||
CloseHandle(hThread2);
|
||||
}
|
||||
VirtualFreeEx(hProc, remoteCode, 0, MEM_RELEASE);
|
||||
}
|
||||
VirtualFreeEx(hProc, remoteInitName, 0, MEM_RELEASE);
|
||||
}
|
||||
|
||||
CloseHandle(hProc);
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
/* ── Linux injection: ptrace + dlopen ─────────────────────────────── */
|
||||
|
||||
static uint64_t findLibBase(pid_t pid, const char* libName)
|
||||
{
|
||||
char mapsPath[64];
|
||||
snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid);
|
||||
FILE* f = fopen(mapsPath, "r");
|
||||
if (!f) return 0;
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (strstr(line, libName)) {
|
||||
uint64_t base;
|
||||
if (sscanf(line, "%lx-", &base) == 1) {
|
||||
fclose(f);
|
||||
return base;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t findSyscallInsn(pid_t pid)
|
||||
{
|
||||
char mapsPath[64];
|
||||
snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid);
|
||||
FILE* f = fopen(mapsPath, "r");
|
||||
if (!f) return 0;
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (strstr(line, "libc") && strstr(line, "r-xp")) {
|
||||
uint64_t start, end;
|
||||
if (sscanf(line, "%lx-%lx", &start, &end) != 2) continue;
|
||||
fclose(f);
|
||||
|
||||
/* scan for 0F 05 (syscall) */
|
||||
char memPath[64];
|
||||
snprintf(memPath, sizeof(memPath), "/proc/%d/mem", pid);
|
||||
int memFd = open(memPath, O_RDONLY);
|
||||
if (memFd < 0) return 0;
|
||||
|
||||
uint8_t buf[4096];
|
||||
for (uint64_t off = start; off < end; off += sizeof(buf)) {
|
||||
ssize_t n = pread(memFd, buf, sizeof(buf), (off_t)off);
|
||||
if (n <= 1) break;
|
||||
for (ssize_t i = 0; i + 1 < n; ++i) {
|
||||
if (buf[i] == 0x0F && buf[i + 1] == 0x05) {
|
||||
close(memFd);
|
||||
return off + (uint64_t)i;
|
||||
}
|
||||
}
|
||||
}
|
||||
close(memFd);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool writeTargetMem(pid_t pid, uint64_t addr, const void* src, size_t len)
|
||||
{
|
||||
const uint8_t* p = static_cast<const uint8_t*>(src);
|
||||
for (size_t i = 0; i < len; i += sizeof(long)) {
|
||||
long val = 0;
|
||||
size_t chunk = (len - i < sizeof(long)) ? (len - i) : sizeof(long);
|
||||
if (chunk < sizeof(long)) {
|
||||
errno = 0;
|
||||
val = ptrace(PTRACE_PEEKDATA, pid, (void*)(addr + i), nullptr);
|
||||
if (errno) return false;
|
||||
}
|
||||
memcpy(&val, p + i, chunk);
|
||||
if (ptrace(PTRACE_POKEDATA, pid, (void*)(addr + i), (void*)val) < 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool injectPayload(uint32_t pid, QString* errorMsg)
|
||||
{
|
||||
QString path = payloadPath();
|
||||
QByteArray pathUtf8 = path.toUtf8();
|
||||
|
||||
if (ptrace(PTRACE_ATTACH, (pid_t)pid, nullptr, nullptr) < 0) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("ptrace attach failed: %1\n"
|
||||
"Check /proc/sys/kernel/yama/ptrace_scope or run as root.")
|
||||
.arg(strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int status;
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
/* save registers */
|
||||
struct user_regs_struct savedRegs, regs;
|
||||
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, &savedRegs);
|
||||
regs = savedRegs;
|
||||
|
||||
/* find syscall instruction in target's libc */
|
||||
uint64_t syscallAddr = findSyscallInsn((pid_t)pid);
|
||||
if (!syscallAddr) {
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Could not find syscall instruction in target.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* find dlopen in target via libc offset technique */
|
||||
void* ourDlopen = dlsym(RTLD_DEFAULT, "dlopen");
|
||||
uint64_t ourLibcBase = findLibBase(getpid(), "libc");
|
||||
uint64_t targetLibcBase = findLibBase((pid_t)pid, "libc");
|
||||
|
||||
if (!ourDlopen || !ourLibcBase || !targetLibcBase) {
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Could not resolve dlopen address.");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t targetDlopen = targetLibcBase + ((uint64_t)ourDlopen - ourLibcBase);
|
||||
|
||||
/* call mmap in target via syscall: mmap(0, 4096, RWX, MAP_PRIVATE|MAP_ANON, -1, 0) */
|
||||
regs.rax = 9; /* __NR_mmap */
|
||||
regs.rdi = 0;
|
||||
regs.rsi = 4096;
|
||||
regs.rdx = 7; /* PROT_READ|PROT_WRITE|PROT_EXEC */
|
||||
regs.r10 = 0x22; /* MAP_PRIVATE|MAP_ANONYMOUS */
|
||||
regs.r8 = (uint64_t)-1;
|
||||
regs.r9 = 0;
|
||||
regs.rip = syscallAddr;
|
||||
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, ®s);
|
||||
ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr);
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, ®s);
|
||||
uint64_t mmapPage = regs.rax;
|
||||
|
||||
if ((int64_t)mmapPage < 0 || mmapPage == 0) {
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &savedRegs);
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("mmap in target failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* write path string at start of page */
|
||||
writeTargetMem((pid_t)pid, mmapPage, pathUtf8.constData(), (size_t)(pathUtf8.size() + 1));
|
||||
|
||||
/* write shellcode after path:
|
||||
* mov rdi, pathAddr (48 BF xxxxxxxx)
|
||||
* mov rsi, 2 (48 BE 02000000 00000000)
|
||||
* mov rax, dlopenAddr (48 B8 xxxxxxxx)
|
||||
* call rax (FF D0)
|
||||
* int3 (CC)
|
||||
*/
|
||||
uint64_t pathAddr = mmapPage;
|
||||
uint64_t codeAddr = mmapPage + ((pathUtf8.size() + 1 + 15) & ~15ULL);
|
||||
|
||||
uint8_t sc[64];
|
||||
int len = 0;
|
||||
/* mov rdi, imm64 */
|
||||
sc[len++] = 0x48; sc[len++] = 0xBF;
|
||||
memcpy(sc + len, &pathAddr, 8); len += 8;
|
||||
/* mov rsi, 2 (RTLD_NOW) */
|
||||
sc[len++] = 0x48; sc[len++] = 0xBE;
|
||||
uint64_t rtldNow = 2;
|
||||
memcpy(sc + len, &rtldNow, 8); len += 8;
|
||||
/* mov rax, dlopen */
|
||||
sc[len++] = 0x48; sc[len++] = 0xB8;
|
||||
memcpy(sc + len, &targetDlopen, 8); len += 8;
|
||||
/* call rax */
|
||||
sc[len++] = 0xFF; sc[len++] = 0xD0;
|
||||
/* int3 */
|
||||
sc[len++] = 0xCC;
|
||||
|
||||
writeTargetMem((pid_t)pid, codeAddr, sc, (size_t)len);
|
||||
|
||||
/* execute shellcode */
|
||||
regs = savedRegs;
|
||||
regs.rip = codeAddr;
|
||||
regs.rsp = (mmapPage + 4096) & ~0xFULL;
|
||||
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, ®s);
|
||||
ptrace(PTRACE_CONT, (pid_t)pid, nullptr, nullptr);
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
bool ok = false;
|
||||
if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
|
||||
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, ®s);
|
||||
ok = (regs.rax != 0);
|
||||
}
|
||||
|
||||
/* clean up: munmap the page via syscall */
|
||||
struct user_regs_struct cleanRegs = savedRegs;
|
||||
cleanRegs.rax = 11; /* __NR_munmap */
|
||||
cleanRegs.rdi = mmapPage;
|
||||
cleanRegs.rsi = 4096;
|
||||
cleanRegs.rip = syscallAddr;
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &cleanRegs);
|
||||
ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr);
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
/* restore and detach */
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &savedRegs);
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
|
||||
if (!ok && errorMsg)
|
||||
*errorMsg = QStringLiteral("dlopen failed in target.\n"
|
||||
"Ensure payload is at: %1").arg(path);
|
||||
return ok;
|
||||
}
|
||||
#endif /* _WIN32 / linux injection */
|
||||
|
||||
} /* anonymous namespace */
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* RemoteProcessMemoryPlugin
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
RemoteProcessMemoryPlugin::RemoteProcessMemoryPlugin() = default;
|
||||
RemoteProcessMemoryPlugin::~RemoteProcessMemoryPlugin() = default;
|
||||
|
||||
QIcon RemoteProcessMemoryPlugin::Icon() const
|
||||
{
|
||||
return qApp->style()->standardIcon(QStyle::SP_DriveNetIcon);
|
||||
}
|
||||
|
||||
bool RemoteProcessMemoryPlugin::canHandle(const QString& target) const
|
||||
{
|
||||
return target.startsWith(QStringLiteral("rpm:"));
|
||||
}
|
||||
|
||||
std::unique_ptr<rcx::Provider>
|
||||
RemoteProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
{
|
||||
/* target = "rpm:{pid}:{name}" */
|
||||
QStringList parts = target.split(':');
|
||||
if (parts.size() < 3 || parts[0] != QStringLiteral("rpm")) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
uint32_t pid = parts[1].toUInt(&ok);
|
||||
QString name = parts.mid(2).join(':'); /* name may contain colons */
|
||||
|
||||
if (!ok || pid == 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto ipc = getOrCreateConnection(pid, errorMsg);
|
||||
if (!ipc) return nullptr;
|
||||
|
||||
return std::make_unique<RemoteProcessProvider>(pid, name, ipc);
|
||||
}
|
||||
|
||||
uint64_t RemoteProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||
{
|
||||
/* Read imageBase directly from the shared-memory header -- zero IPC cost.
|
||||
The payload filled it at init from PEB->Ldr (Win) / /proc/self/maps (Linux). */
|
||||
QStringList parts = target.split(':');
|
||||
if (parts.size() < 2 || parts[0] != QStringLiteral("rpm"))
|
||||
return 0;
|
||||
|
||||
bool ok;
|
||||
uint32_t pid = parts[1].toUInt(&ok);
|
||||
if (!ok) return 0;
|
||||
|
||||
QMutexLocker lock(&m_connectionsMutex);
|
||||
auto it = m_connections.constFind(pid);
|
||||
if (it == m_connections.constEnd() || !(*it)->connected)
|
||||
return 0;
|
||||
|
||||
auto* hdr = static_cast<const RcxRpcHeader*>((*it)->mappedView);
|
||||
return hdr->imageBase;
|
||||
}
|
||||
|
||||
bool RemoteProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
/* ── 1. pick a process ── */
|
||||
QVector<PluginProcessInfo> pluginProcs = enumerateProcesses();
|
||||
QList<ProcessInfo> procs;
|
||||
for (const auto& pi : pluginProcs) {
|
||||
ProcessInfo info;
|
||||
info.pid = pi.pid;
|
||||
info.name = pi.name;
|
||||
info.path = pi.path;
|
||||
info.icon = pi.icon;
|
||||
procs.append(info);
|
||||
}
|
||||
|
||||
ProcessPicker picker(procs, parent);
|
||||
if (picker.exec() != QDialog::Accepted) return false;
|
||||
|
||||
uint32_t pid = picker.selectedProcessId();
|
||||
QString name = picker.selectedProcessName();
|
||||
|
||||
/* ── 2. ask inject or connect ── */
|
||||
QMessageBox box(parent);
|
||||
box.setWindowTitle(QStringLiteral("Remote Process Memory"));
|
||||
box.setText(QStringLiteral("Connect to %1 (PID %2)").arg(name).arg(pid));
|
||||
box.setInformativeText(QStringLiteral("Choose how to connect to the target:"));
|
||||
QAbstractButton* injectBtn = box.addButton(QStringLiteral("Inject Payload"), QMessageBox::ActionRole);
|
||||
QAbstractButton* connectBtn = box.addButton(QStringLiteral("Already Injected"), QMessageBox::ActionRole);
|
||||
box.addButton(QMessageBox::Cancel);
|
||||
box.exec();
|
||||
|
||||
QAbstractButton* clicked = box.clickedButton();
|
||||
if (clicked == injectBtn) {
|
||||
QString injectErr;
|
||||
if (!injectPayload(pid, &injectErr)) {
|
||||
QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr);
|
||||
return false;
|
||||
}
|
||||
|
||||
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
else if (clicked == connectBtn) {
|
||||
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<PluginProcessInfo> RemoteProcessMemoryPlugin::enumerateProcesses()
|
||||
{
|
||||
QVector<PluginProcessInfo> procs;
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snap == INVALID_HANDLE_VALUE) return procs;
|
||||
|
||||
PROCESSENTRY32W entry;
|
||||
entry.dwSize = sizeof(entry);
|
||||
|
||||
if (Process32FirstW(snap, &entry)) {
|
||||
do {
|
||||
PluginProcessInfo info;
|
||||
info.pid = entry.th32ProcessID;
|
||||
info.name = QString::fromWCharArray(entry.szExeFile);
|
||||
|
||||
HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE, entry.th32ProcessID);
|
||||
if (hProc) {
|
||||
wchar_t path[MAX_PATH * 2];
|
||||
DWORD pathLen = sizeof(path) / sizeof(wchar_t);
|
||||
if (QueryFullProcessImageNameW(hProc, 0, path, &pathLen)) {
|
||||
info.path = QString::fromWCharArray(path);
|
||||
SHFILEINFOW sfi = {};
|
||||
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi),
|
||||
SHGFI_ICON | SHGFI_SMALLICON) && sfi.hIcon) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
info.icon = QIcon(QPixmap::fromImage(QImage::fromHICON(sfi.hIcon)));
|
||||
#else
|
||||
info.icon = QIcon(QtWin::fromHICON(sfi.hIcon));
|
||||
#endif
|
||||
DestroyIcon(sfi.hIcon);
|
||||
}
|
||||
}
|
||||
CloseHandle(hProc);
|
||||
}
|
||||
procs.append(info);
|
||||
} while (Process32NextW(snap, &entry));
|
||||
}
|
||||
CloseHandle(snap);
|
||||
|
||||
#else
|
||||
QDir procDir(QStringLiteral("/proc"));
|
||||
QIcon defIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
|
||||
|
||||
for (const QString& entry : procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
bool ok;
|
||||
uint32_t pid = entry.toUInt(&ok);
|
||||
if (!ok || pid == 0) continue;
|
||||
|
||||
QFile commFile(QStringLiteral("/proc/%1/comm").arg(pid));
|
||||
if (!commFile.open(QIODevice::ReadOnly)) continue;
|
||||
QString procName = QString::fromUtf8(commFile.readAll()).trimmed();
|
||||
commFile.close();
|
||||
if (procName.isEmpty()) continue;
|
||||
|
||||
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
|
||||
if (::access(memPath.toUtf8().constData(), R_OK) != 0) continue;
|
||||
|
||||
QFileInfo exeInfo(QStringLiteral("/proc/%1/exe").arg(pid));
|
||||
PluginProcessInfo info;
|
||||
info.pid = pid;
|
||||
info.name = procName;
|
||||
info.path = exeInfo.exists() ? exeInfo.symLinkTarget() : QString();
|
||||
info.icon = defIcon;
|
||||
procs.append(info);
|
||||
}
|
||||
#endif
|
||||
|
||||
return procs;
|
||||
}
|
||||
|
||||
std::shared_ptr<IpcClient>
|
||||
RemoteProcessMemoryPlugin::getOrCreateConnection(
|
||||
uint32_t pid, QString* errorMsg)
|
||||
{
|
||||
QMutexLocker lock(&m_connectionsMutex);
|
||||
|
||||
auto it = m_connections.find(pid);
|
||||
if (it != m_connections.end() && (*it)->connected)
|
||||
return *it;
|
||||
|
||||
auto ipc = std::make_shared<IpcClient>();
|
||||
if (!ipc->connect(pid)) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Failed to connect IPC to PID %1.\n"
|
||||
"Is the payload running?").arg(pid);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_connections[pid] = ipc;
|
||||
return ipc;
|
||||
}
|
||||
|
||||
/* ── Plugin factory ───────────────────────────────────────────────── */
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new RemoteProcessMemoryPlugin();
|
||||
}
|
||||
86
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h
Normal file
86
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
#include "../../src/iplugin.h"
|
||||
#include "../../src/providers/provider.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <QMutex>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
|
||||
struct IpcClient; /* defined in .cpp */
|
||||
|
||||
/* ── Provider ─────────────────────────────────────────────────────── */
|
||||
|
||||
class RemoteProcessProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
struct ModuleInfo { QString name; uint64_t base; uint64_t size; };
|
||||
|
||||
RemoteProcessProvider(uint32_t pid, const QString& processName,
|
||||
std::shared_ptr<IpcClient> ipc);
|
||||
~RemoteProcessProvider() override;
|
||||
|
||||
/* required */
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
int size() const override;
|
||||
|
||||
/* optional */
|
||||
bool write(uint64_t addr, const void* buf, int len) override;
|
||||
bool isWritable() const override { return m_connected; }
|
||||
QString name() const override { return m_processName; }
|
||||
QString kind() const override { return QStringLiteral("RemoteProcess"); }
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
bool isReadable(uint64_t, int len) const override { return m_connected && len >= 0; }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
uint64_t symbolToAddress(const QString& n) const override;
|
||||
|
||||
uint32_t pid() const { return m_pid; }
|
||||
|
||||
private:
|
||||
void cacheModules();
|
||||
|
||||
uint32_t m_pid;
|
||||
QString m_processName;
|
||||
bool m_connected;
|
||||
uint64_t m_base;
|
||||
mutable std::shared_ptr<IpcClient> m_ipc;
|
||||
QVector<ModuleInfo> m_modules;
|
||||
};
|
||||
|
||||
/* ── Plugin ───────────────────────────────────────────────────────── */
|
||||
|
||||
class RemoteProcessMemoryPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
RemoteProcessMemoryPlugin();
|
||||
~RemoteProcessMemoryPlugin() override;
|
||||
|
||||
std::string Name() const override { return "Remote Process Memory"; }
|
||||
std::string Version() const override { return "1.0.0"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
std::string Description() const override {
|
||||
return "Read/write memory via injected payload (shared-memory IPC)";
|
||||
}
|
||||
k_ELoadType LoadType() const override { return k_ELoadTypeManual; }
|
||||
QIcon Icon() const override;
|
||||
|
||||
bool canHandle(const QString& target) const override;
|
||||
std::unique_ptr<rcx::Provider> createProvider(const QString& target,
|
||||
QString* errorMsg) override;
|
||||
uint64_t getInitialBaseAddress(const QString& target) const override;
|
||||
bool selectTarget(QWidget* parent, QString* target) override;
|
||||
|
||||
bool providesProcessList() const override { return true; }
|
||||
QVector<PluginProcessInfo> enumerateProcesses() override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<IpcClient> getOrCreateConnection(
|
||||
uint32_t pid, QString* errorMsg);
|
||||
|
||||
mutable QMutex m_connectionsMutex;
|
||||
QHash<uint32_t, std::shared_ptr<IpcClient>> m_connections;
|
||||
};
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||
612
plugins/RemoteProcessMemory/payload/rcx_payload.cpp
Normal file
612
plugins/RemoteProcessMemory/payload/rcx_payload.cpp
Normal file
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
* rcx_payload -- injected into target process.
|
||||
*
|
||||
* Pure Win32 / POSIX, NO Qt, minimal footprint.
|
||||
* Creates the main IPC channel (shared memory + events/semaphores)
|
||||
* using PID-only naming and uses a timer queue for polling.
|
||||
*/
|
||||
|
||||
#include "../rcx_rpc_protocol.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
/* ===================================================================
|
||||
* WINDOWS implementation
|
||||
* =================================================================== */
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
|
||||
/* ── globals ──────────────────────────────────────────────────────── */
|
||||
static HANDLE g_hShm = nullptr;
|
||||
static void* g_mappedView = nullptr;
|
||||
static HANDLE g_hReqEvent = nullptr;
|
||||
static HANDLE g_hRspEvent = nullptr;
|
||||
static HANDLE g_hTimerQueue = nullptr;
|
||||
static HANDLE g_hPollTimer = nullptr;
|
||||
static volatile LONG g_initialized = 0;
|
||||
|
||||
/* ── memory safety via VirtualQuery ────────────────────────────────── */
|
||||
|
||||
inline bool IsReadableProtect(DWORD p)
|
||||
{
|
||||
if (p & (PAGE_NOACCESS | PAGE_GUARD))
|
||||
return false;
|
||||
|
||||
const DWORD readable =
|
||||
PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
|
||||
PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
|
||||
return (p & readable) != 0;
|
||||
}
|
||||
|
||||
inline bool IsWritableProtect(DWORD p)
|
||||
{
|
||||
if (p & (PAGE_NOACCESS | PAGE_GUARD))
|
||||
return false;
|
||||
|
||||
const DWORD writable =
|
||||
PAGE_READWRITE | PAGE_WRITECOPY |
|
||||
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
|
||||
return (p & writable) != 0;
|
||||
}
|
||||
|
||||
/* Check that the full range [addr, addr+len) is covered by readable pages. */
|
||||
static bool IsRangeReadable(uintptr_t addr, uint32_t len)
|
||||
{
|
||||
uintptr_t end = addr + len;
|
||||
uintptr_t cur = addr;
|
||||
while (cur < end) {
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
|
||||
return false;
|
||||
if (mbi.State != MEM_COMMIT || !IsReadableProtect(mbi.Protect))
|
||||
return false;
|
||||
uintptr_t regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
|
||||
cur = regionEnd;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool IsRangeWritable(uintptr_t addr, uint32_t len)
|
||||
{
|
||||
uintptr_t end = addr + len;
|
||||
uintptr_t cur = addr;
|
||||
while (cur < end) {
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
|
||||
return false;
|
||||
if (mbi.State != MEM_COMMIT || !IsWritableProtect(mbi.Protect))
|
||||
return false;
|
||||
uintptr_t regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
|
||||
cur = regionEnd;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── command handlers ─────────────────────────────────────────────── */
|
||||
|
||||
static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
auto* entries = reinterpret_cast<RcxRpcReadEntry*>(data);
|
||||
for (uint32_t i = 0; i < hdr->requestCount; ++i) {
|
||||
uint8_t* dest = data + entries[i].dataOffset;
|
||||
uintptr_t src = static_cast<uintptr_t>(entries[i].address);
|
||||
if (IsRangeReadable(src, entries[i].length)) {
|
||||
memcpy(dest, reinterpret_cast<const void*>(src), entries[i].length);
|
||||
} else {
|
||||
memset(dest, 0, entries[i].length);
|
||||
hdr->status = RCX_RPC_STATUS_PARTIAL;
|
||||
}
|
||||
/* SEH fallback (commented out, kept for reference):
|
||||
__try {
|
||||
memcpy(dest, reinterpret_cast<const void*>(src), entries[i].length);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
memset(dest, 0, entries[i].length);
|
||||
hdr->status = RCX_RPC_STATUS_PARTIAL;
|
||||
}
|
||||
*/
|
||||
}
|
||||
hdr->responseCount = hdr->requestCount;
|
||||
}
|
||||
|
||||
static void handle_write(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
uintptr_t dst = static_cast<uintptr_t>(hdr->writeAddress);
|
||||
if (IsRangeWritable(dst, hdr->writeLength)) {
|
||||
memcpy(reinterpret_cast<void*>(dst), data, hdr->writeLength);
|
||||
} else {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
}
|
||||
/* SEH fallback (commented out, kept for reference):
|
||||
__try {
|
||||
memcpy(reinterpret_cast<void*>(dst), data, hdr->writeLength);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
HANDLE hProc = GetCurrentProcess();
|
||||
HMODULE mods[1024];
|
||||
DWORD needed = 0;
|
||||
if (!EnumProcessModules(hProc, mods, sizeof(mods), &needed)) {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
hdr->responseCount = 0;
|
||||
return;
|
||||
}
|
||||
int count = (int)(needed / sizeof(HMODULE));
|
||||
if (count > 1024) count = 1024;
|
||||
|
||||
uint32_t entryBytes = (uint32_t)(count * sizeof(RcxRpcModuleEntry));
|
||||
uint32_t nameDataOff = entryBytes;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
MODULEINFO mi{};
|
||||
WCHAR modName[MAX_PATH];
|
||||
GetModuleInformation(hProc, mods[i], &mi, sizeof(mi));
|
||||
int nameLen = (int)GetModuleBaseNameW(hProc, mods[i], modName, MAX_PATH);
|
||||
uint32_t nameBytes = (uint32_t)(nameLen * sizeof(WCHAR));
|
||||
|
||||
auto* entry = reinterpret_cast<RcxRpcModuleEntry*>(data + i * sizeof(RcxRpcModuleEntry));
|
||||
entry->base = reinterpret_cast<uint64_t>(mi.lpBaseOfDll);
|
||||
entry->size = static_cast<uint64_t>(mi.SizeOfImage);
|
||||
entry->nameOffset = nameDataOff;
|
||||
entry->nameLength = nameBytes;
|
||||
|
||||
if (nameDataOff + nameBytes <= RCX_RPC_DATA_SIZE) {
|
||||
memcpy(data + nameDataOff, modName, nameBytes);
|
||||
nameDataOff += nameBytes;
|
||||
}
|
||||
}
|
||||
|
||||
hdr->responseCount = (uint32_t)count;
|
||||
hdr->totalDataUsed = nameDataOff;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
/* forward declaration */
|
||||
void RcxPayloadCleanup();
|
||||
|
||||
/* ── timer callback (non-blocking poll) ───────────────────────────── */
|
||||
|
||||
static VOID CALLBACK RcxPollTimerCallback(PVOID, BOOLEAN)
|
||||
{
|
||||
if (!g_mappedView || !g_hReqEvent || !g_hRspEvent)
|
||||
return;
|
||||
|
||||
/* non-blocking check: is there a pending request? */
|
||||
DWORD rc = WaitForSingleObject(g_hReqEvent, 0);
|
||||
if (rc != WAIT_OBJECT_0)
|
||||
return;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
switch (static_cast<RcxRpcCommand>(hdr->command)) {
|
||||
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
|
||||
case RPC_CMD_WRITE: handle_write(hdr, data); break;
|
||||
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
|
||||
case RPC_CMD_PING: break;
|
||||
case RPC_CMD_SHUTDOWN:
|
||||
RcxPayloadCleanup();
|
||||
return;
|
||||
default:
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
SetEvent(g_hRspEvent);
|
||||
}
|
||||
|
||||
/* ── cleanup ──────────────────────────────────────────────────────── */
|
||||
|
||||
void RcxPayloadCleanup()
|
||||
{
|
||||
if (!InterlockedCompareExchange(&g_initialized, 0, 0))
|
||||
return;
|
||||
|
||||
/* stop the poll timer first */
|
||||
if (g_hTimerQueue) {
|
||||
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE); /* waits for callbacks */
|
||||
g_hTimerQueue = nullptr;
|
||||
g_hPollTimer = nullptr;
|
||||
}
|
||||
|
||||
/* mark not-ready */
|
||||
if (g_mappedView) {
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
|
||||
}
|
||||
|
||||
if (g_mappedView) { UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; }
|
||||
if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; }
|
||||
if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; }
|
||||
if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; }
|
||||
|
||||
InterlockedExchange(&g_initialized, 0);
|
||||
}
|
||||
|
||||
/* ── init (called AFTER DllMain returns — safe for timer queues) ── */
|
||||
|
||||
extern "C" __declspec(dllexport)
|
||||
bool RcxPayloadInit()
|
||||
{
|
||||
if (InterlockedCompareExchange(&g_initialized, 1, 0) != 0)
|
||||
return true; /* already initialized */
|
||||
|
||||
uint32_t pid = GetCurrentProcessId();
|
||||
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
|
||||
|
||||
g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
|
||||
PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName);
|
||||
if (!g_hShm) {
|
||||
InterlockedExchange(&g_initialized, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
|
||||
if (!g_mappedView) {
|
||||
CloseHandle(g_hShm); g_hShm = nullptr;
|
||||
InterlockedExchange(&g_initialized, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
hdr->version = RCX_RPC_VERSION;
|
||||
|
||||
/* image base from PEB */
|
||||
{
|
||||
uint64_t peb;
|
||||
asm volatile("mov %%gs:0x60, %0" : "=r"(peb));
|
||||
uint64_t ldr = *reinterpret_cast<uint64_t*>(peb + 0x18);
|
||||
uint64_t firstLink = *reinterpret_cast<uint64_t*>(ldr + 0x10);
|
||||
hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30);
|
||||
}
|
||||
|
||||
g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName);
|
||||
g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName);
|
||||
if (!g_hReqEvent || !g_hRspEvent) {
|
||||
RcxPayloadCleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* create dedicated timer queue + fast poll timer (10ms interval) */
|
||||
g_hTimerQueue = CreateTimerQueue();
|
||||
if (!g_hTimerQueue) {
|
||||
RcxPayloadCleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CreateTimerQueueTimer(&g_hPollTimer, g_hTimerQueue,
|
||||
RcxPollTimerCallback, nullptr,
|
||||
0, /* start immediately */
|
||||
10, /* 10ms repeat */
|
||||
WT_EXECUTEDEFAULT)) {
|
||||
RcxPayloadCleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* mark ready */
|
||||
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── DllMain — minimal, no heavy work under loader lock ───────────── */
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID)
|
||||
{
|
||||
if (reason == DLL_PROCESS_DETACH) {
|
||||
RcxPayloadCleanup();
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
#else
|
||||
/* ===================================================================
|
||||
* LINUX implementation
|
||||
* =================================================================== */
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <semaphore.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <signal.h>
|
||||
|
||||
/* ── globals ──────────────────────────────────────────────────────── */
|
||||
static int g_shmFd = -1;
|
||||
static void* g_mappedView = nullptr;
|
||||
static sem_t* g_reqSem = SEM_FAILED;
|
||||
static sem_t* g_rspSem = SEM_FAILED;
|
||||
static pthread_t g_thread;
|
||||
static volatile int g_shutdown = 0;
|
||||
static volatile int g_threadRunning = 0;
|
||||
static int g_memFd = -1; /* /proc/self/mem for safe access */
|
||||
static char g_shmName[128];
|
||||
static char g_reqName[128];
|
||||
static char g_rspName[128];
|
||||
|
||||
/* ── safe memory access via /proc/self/mem ────────────────────────── */
|
||||
|
||||
static void safe_read(uint64_t addr, void* dest, uint32_t len, uint32_t* status)
|
||||
{
|
||||
ssize_t n = pread(g_memFd, dest, len, (off_t)addr);
|
||||
if (n < (ssize_t)len) {
|
||||
if (n > 0)
|
||||
memset((uint8_t*)dest + n, 0, len - (uint32_t)n);
|
||||
else
|
||||
memset(dest, 0, len);
|
||||
*status = RCX_RPC_STATUS_PARTIAL;
|
||||
}
|
||||
}
|
||||
|
||||
static void safe_write(uint64_t addr, const void* src, uint32_t len, uint32_t* status)
|
||||
{
|
||||
ssize_t n = pwrite(g_memFd, src, len, (off_t)addr);
|
||||
if (n < (ssize_t)len)
|
||||
*status = RCX_RPC_STATUS_ERROR;
|
||||
}
|
||||
|
||||
/* ── command handlers ─────────────────────────────────────────────── */
|
||||
|
||||
static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
auto* entries = reinterpret_cast<RcxRpcReadEntry*>(data);
|
||||
for (uint32_t i = 0; i < hdr->requestCount; ++i) {
|
||||
uint8_t* dest = data + entries[i].dataOffset;
|
||||
safe_read(entries[i].address, dest, entries[i].length, &hdr->status);
|
||||
}
|
||||
hdr->responseCount = hdr->requestCount;
|
||||
}
|
||||
|
||||
static void handle_write(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
safe_write(hdr->writeAddress, data, hdr->writeLength, &hdr->status);
|
||||
}
|
||||
|
||||
static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
FILE* f = fopen("/proc/self/maps", "r");
|
||||
if (!f) {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
hdr->responseCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
/* first pass: collect unique file-backed mappings */
|
||||
struct ModRange { uint64_t base; uint64_t end; char path[512]; };
|
||||
static ModRange modules[512]; /* static to avoid large stack alloc */
|
||||
int modCount = 0;
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f) && modCount < 512) {
|
||||
uint64_t start, end;
|
||||
char perms[8] = {}, path[512] = {};
|
||||
if (sscanf(line, "%lx-%lx %7s %*x %*x:%*x %*u %511[^\n]",
|
||||
&start, &end, perms, path) < 4)
|
||||
continue;
|
||||
|
||||
/* skip non-file / special mappings */
|
||||
/* trim leading whitespace from path */
|
||||
char* p = path;
|
||||
while (*p == ' ' || *p == '\t') ++p;
|
||||
if (*p != '/') continue;
|
||||
if (strncmp(p, "/dev/", 5) == 0) continue;
|
||||
if (strncmp(p, "/memfd:", 7) == 0) continue;
|
||||
|
||||
bool found = false;
|
||||
for (int i = 0; i < modCount; ++i) {
|
||||
if (strcmp(modules[i].path, p) == 0) {
|
||||
if (start < modules[i].base) modules[i].base = start;
|
||||
if (end > modules[i].end) modules[i].end = end;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
modules[modCount].base = start;
|
||||
modules[modCount].end = end;
|
||||
strncpy(modules[modCount].path, p, 511);
|
||||
modules[modCount].path[511] = '\0';
|
||||
++modCount;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
/* write entries + name strings into data region */
|
||||
uint32_t entryBytes = (uint32_t)(modCount * sizeof(RcxRpcModuleEntry));
|
||||
uint32_t nameDataOff = entryBytes;
|
||||
|
||||
for (int i = 0; i < modCount; ++i) {
|
||||
const char* basename = strrchr(modules[i].path, '/');
|
||||
basename = basename ? basename + 1 : modules[i].path;
|
||||
uint32_t nameLen = (uint32_t)strlen(basename);
|
||||
|
||||
auto* entry = reinterpret_cast<RcxRpcModuleEntry*>(
|
||||
data + (uint32_t)i * sizeof(RcxRpcModuleEntry));
|
||||
entry->base = modules[i].base;
|
||||
entry->size = modules[i].end - modules[i].base;
|
||||
entry->nameOffset = nameDataOff;
|
||||
entry->nameLength = nameLen;
|
||||
|
||||
if (nameDataOff + nameLen <= RCX_RPC_DATA_SIZE) {
|
||||
memcpy(data + nameDataOff, basename, nameLen);
|
||||
nameDataOff += nameLen;
|
||||
}
|
||||
}
|
||||
|
||||
hdr->responseCount = (uint32_t)modCount;
|
||||
hdr->totalDataUsed = nameDataOff;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
/* ── server thread ────────────────────────────────────────────────── */
|
||||
|
||||
static void* server_thread_func(void*)
|
||||
{
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
__atomic_store_n(&hdr->payloadReady, 1, __ATOMIC_RELEASE);
|
||||
|
||||
while (!__atomic_load_n(&g_shutdown, __ATOMIC_ACQUIRE)) {
|
||||
/* timed wait: 250ms */
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_nsec += 250000000;
|
||||
if (ts.tv_nsec >= 1000000000) {
|
||||
ts.tv_sec += 1;
|
||||
ts.tv_nsec -= 1000000000;
|
||||
}
|
||||
|
||||
int rc = sem_timedwait(g_reqSem, &ts);
|
||||
if (rc != 0) {
|
||||
if (errno == ETIMEDOUT) continue;
|
||||
break;
|
||||
}
|
||||
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
switch (static_cast<RcxRpcCommand>(hdr->command)) {
|
||||
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
|
||||
case RPC_CMD_WRITE: handle_write(hdr, data); break;
|
||||
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
|
||||
case RPC_CMD_PING: break;
|
||||
case RPC_CMD_SHUTDOWN:
|
||||
__atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE);
|
||||
break;
|
||||
default:
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
sem_post(g_rspSem);
|
||||
|
||||
if (static_cast<RcxRpcCommand>(hdr->command) == RPC_CMD_SHUTDOWN)
|
||||
break;
|
||||
}
|
||||
|
||||
__atomic_store_n(&hdr->payloadReady, 0, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* ── init / cleanup ───────────────────────────────────────────────── */
|
||||
|
||||
static void payload_cleanup()
|
||||
{
|
||||
__atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE);
|
||||
|
||||
/* wake the thread if blocked */
|
||||
if (g_reqSem != SEM_FAILED) sem_post(g_reqSem);
|
||||
|
||||
if (__atomic_load_n(&g_threadRunning, __ATOMIC_ACQUIRE)) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_sec += 2;
|
||||
pthread_timedjoin_np(g_thread, nullptr, &ts);
|
||||
}
|
||||
|
||||
if (g_mappedView && g_mappedView != MAP_FAILED) {
|
||||
munmap(g_mappedView, RCX_RPC_SHM_SIZE);
|
||||
g_mappedView = nullptr;
|
||||
}
|
||||
if (g_shmFd >= 0) { close(g_shmFd); g_shmFd = -1; }
|
||||
if (g_reqSem != SEM_FAILED) { sem_close(g_reqSem); g_reqSem = SEM_FAILED; }
|
||||
if (g_rspSem != SEM_FAILED) { sem_close(g_rspSem); g_rspSem = SEM_FAILED; }
|
||||
|
||||
/* unlink named objects */
|
||||
if (g_shmName[0]) shm_unlink(g_shmName);
|
||||
if (g_reqName[0]) sem_unlink(g_reqName);
|
||||
if (g_rspName[0]) sem_unlink(g_rspName);
|
||||
|
||||
if (g_memFd >= 0) { close(g_memFd); g_memFd = -1; }
|
||||
}
|
||||
|
||||
__attribute__((constructor))
|
||||
static void payload_init()
|
||||
{
|
||||
uint32_t pid = (uint32_t)getpid();
|
||||
|
||||
/* ── open /proc/self/mem for safe access ── */
|
||||
g_memFd = open("/proc/self/mem", O_RDWR);
|
||||
if (g_memFd < 0) return;
|
||||
|
||||
/* ── create main shared memory (PID-only naming) ── */
|
||||
rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid);
|
||||
rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid);
|
||||
rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid);
|
||||
|
||||
g_shmFd = shm_open(g_shmName, O_CREAT | O_RDWR, 0600);
|
||||
if (g_shmFd < 0) return;
|
||||
if (ftruncate(g_shmFd, RCX_RPC_SHM_SIZE) != 0) {
|
||||
close(g_shmFd); g_shmFd = -1; return;
|
||||
}
|
||||
|
||||
g_mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, g_shmFd, 0);
|
||||
if (g_mappedView == MAP_FAILED) {
|
||||
g_mappedView = nullptr;
|
||||
close(g_shmFd); g_shmFd = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
hdr->version = RCX_RPC_VERSION;
|
||||
|
||||
/* image base from /proc/self/maps: first executable mapping */
|
||||
{
|
||||
FILE* f = fopen("/proc/self/maps", "r");
|
||||
if (f) {
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
uint64_t start;
|
||||
char perms[8] = {};
|
||||
if (sscanf(line, "%lx-%*x %7s", &start, perms) >= 2 && perms[2] == 'x') {
|
||||
hdr->imageBase = start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── create semaphores ── */
|
||||
g_reqSem = sem_open(g_reqName, O_CREAT, 0600, 0);
|
||||
g_rspSem = sem_open(g_rspName, O_CREAT, 0600, 0);
|
||||
if (g_reqSem == SEM_FAILED || g_rspSem == SEM_FAILED) {
|
||||
payload_cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── start server thread (it will set payloadReady = 1) ── */
|
||||
__atomic_store_n(&g_threadRunning, 1, __ATOMIC_RELEASE);
|
||||
if (pthread_create(&g_thread, nullptr, server_thread_func, nullptr) != 0) {
|
||||
__atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE);
|
||||
payload_cleanup();
|
||||
return;
|
||||
}
|
||||
pthread_detach(g_thread);
|
||||
}
|
||||
|
||||
__attribute__((destructor))
|
||||
static void payload_deinit()
|
||||
{
|
||||
payload_cleanup();
|
||||
}
|
||||
|
||||
#endif /* _WIN32 / linux */
|
||||
113
plugins/RemoteProcessMemory/rcx_rpc_protocol.h
Normal file
113
plugins/RemoteProcessMemory/rcx_rpc_protocol.h
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* RCX RPC Protocol -- shared between plugin DLL and payload DLL/SO.
|
||||
* No dependencies beyond standard C headers.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── constants ─────────────────────────────────────────────────────── */
|
||||
#define RCX_RPC_VERSION 1
|
||||
#define RCX_RPC_MAX_BATCH 256
|
||||
#define RCX_RPC_SHM_SIZE (1024 * 1024) /* 1 MB */
|
||||
#define RCX_RPC_HEADER_SIZE 4096
|
||||
#define RCX_RPC_DATA_OFFSET RCX_RPC_HEADER_SIZE
|
||||
#define RCX_RPC_DATA_SIZE (RCX_RPC_SHM_SIZE - RCX_RPC_DATA_OFFSET)
|
||||
|
||||
/* status codes */
|
||||
#define RCX_RPC_STATUS_OK 0
|
||||
#define RCX_RPC_STATUS_ERROR 1
|
||||
#define RCX_RPC_STATUS_PARTIAL 2
|
||||
|
||||
/* ── commands ──────────────────────────────────────────────────────── */
|
||||
#ifdef __cplusplus
|
||||
enum RcxRpcCommand : uint32_t {
|
||||
#else
|
||||
typedef uint32_t RcxRpcCommand;
|
||||
enum {
|
||||
#endif
|
||||
RPC_CMD_NONE = 0,
|
||||
RPC_CMD_READ_BATCH = 1, /* batch read: N {address, length} pairs */
|
||||
RPC_CMD_WRITE = 2, /* single write */
|
||||
RPC_CMD_ENUM_MODULES = 3, /* enumerate loaded modules */
|
||||
RPC_CMD_PING = 4, /* heartbeat */
|
||||
RPC_CMD_SHUTDOWN = 5, /* graceful teardown */
|
||||
};
|
||||
|
||||
/* ── wire structs (natural alignment, verified by static_assert) ─── */
|
||||
|
||||
struct RcxRpcReadEntry {
|
||||
uint64_t address;
|
||||
uint32_t length;
|
||||
uint32_t dataOffset; /* offset into data region for response bytes */
|
||||
};
|
||||
|
||||
struct RcxRpcModuleEntry {
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
uint32_t nameOffset; /* offset into data region, UTF-16 on Win, UTF-8 on Linux */
|
||||
uint32_t nameLength; /* in bytes */
|
||||
};
|
||||
|
||||
/*
|
||||
* Header -- lives at shared-memory offset 0, padded to 4096 bytes.
|
||||
*
|
||||
* offset field
|
||||
* ------ -----
|
||||
* 0 version (4)
|
||||
* 4 payloadReady (4)
|
||||
* 8 command (4)
|
||||
* 12 requestCount (4)
|
||||
* 16 writeAddress (8)
|
||||
* 24 writeLength (4)
|
||||
* 28 status (4)
|
||||
* 32 responseCount (4)
|
||||
* 36 totalDataUsed (4)
|
||||
* 40 imageBase (8) -- main module base from PEB / procfs
|
||||
* 48 _pad[4048]
|
||||
*/
|
||||
struct RcxRpcHeader {
|
||||
uint32_t version;
|
||||
uint32_t payloadReady; /* payload sets to 1 after init */
|
||||
uint32_t command; /* RcxRpcCommand */
|
||||
uint32_t requestCount;
|
||||
uint64_t writeAddress;
|
||||
uint32_t writeLength;
|
||||
uint32_t status; /* RCX_RPC_STATUS_* */
|
||||
uint32_t responseCount;
|
||||
uint32_t totalDataUsed;
|
||||
uint64_t imageBase; /* main module base (PEB on Win, /proc on Linux) */
|
||||
uint8_t _pad[RCX_RPC_HEADER_SIZE - 48];
|
||||
};
|
||||
|
||||
/* ── name formatting helpers (PID-only, no nonce) ─────────────────── */
|
||||
|
||||
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_SHM_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_shm_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_REQ_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_req_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_RSP_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_rsp_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
|
||||
#endif
|
||||
593
plugins/RemoteProcessMemory/tests/test_rpc_client.cpp
Normal file
593
plugins/RemoteProcessMemory/tests/test_rpc_client.cpp
Normal file
@@ -0,0 +1,593 @@
|
||||
/*
|
||||
* test_rpc_client -- connects to a running test_rpc_host (or spawns one),
|
||||
* exercises every RPC command, and benchmarks throughput.
|
||||
*
|
||||
* Usage:
|
||||
* test_rpc_client (auto-spawn host)
|
||||
* test_rpc_client <pid> [testbuf_hex testlen]
|
||||
*/
|
||||
|
||||
#include "../rcx_rpc_protocol.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <assert.h>
|
||||
#include <chrono>
|
||||
|
||||
#ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
# include <fcntl.h>
|
||||
# include <sys/mman.h>
|
||||
# include <semaphore.h>
|
||||
# include <libgen.h>
|
||||
# include <limits.h>
|
||||
#endif
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Minimal standalone IPC client (no Qt, mirrors plugin's IpcClient)
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
struct TestIpcClient {
|
||||
#ifdef _WIN32
|
||||
HANDLE hShm = nullptr;
|
||||
HANDLE hReqEvent = nullptr;
|
||||
HANDLE hRspEvent = nullptr;
|
||||
#else
|
||||
int shmFd = -1;
|
||||
sem_t* reqSem = SEM_FAILED;
|
||||
sem_t* rspSem = SEM_FAILED;
|
||||
#endif
|
||||
void* view = nullptr;
|
||||
bool ok = false;
|
||||
|
||||
bool connect(uint32_t pid, int timeoutMs = 5000)
|
||||
{
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
ULONGLONG deadline = GetTickCount64() + (ULONGLONG)timeoutMs;
|
||||
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
|
||||
if (GetTickCount64() >= deadline) return false;
|
||||
Sleep(10);
|
||||
}
|
||||
view = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
|
||||
if (!view) { CloseHandle(hShm); hShm = nullptr; return false; }
|
||||
|
||||
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
|
||||
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
|
||||
if (!hReqEvent || !hRspEvent) return false;
|
||||
#else
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (true) {
|
||||
shmFd = shm_open(shmName, O_RDWR, 0);
|
||||
if (shmFd >= 0) break;
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) return false;
|
||||
usleep(10000);
|
||||
}
|
||||
view = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, shmFd, 0);
|
||||
if (view == MAP_FAILED) { view = nullptr; close(shmFd); shmFd = -1; return false; }
|
||||
|
||||
reqSem = sem_open(reqName, 0);
|
||||
rspSem = sem_open(rspName, 0);
|
||||
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) return false;
|
||||
#endif
|
||||
/* wait for payloadReady */
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
#ifdef _WIN32
|
||||
while (!hdr->payloadReady) {
|
||||
if (GetTickCount64() >= deadline) return false;
|
||||
Sleep(5);
|
||||
}
|
||||
#else
|
||||
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) return false;
|
||||
usleep(5000);
|
||||
}
|
||||
#endif
|
||||
ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnect()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (view) { UnmapViewOfFile(view); view = nullptr; }
|
||||
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
|
||||
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
|
||||
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
|
||||
#else
|
||||
if (view) { munmap(view, RCX_RPC_SHM_SIZE); view = nullptr; }
|
||||
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
|
||||
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
|
||||
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
|
||||
#endif
|
||||
ok = false;
|
||||
}
|
||||
|
||||
bool signalAndWait(int timeoutMs = 2000)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
SetEvent(hReqEvent);
|
||||
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
|
||||
#else
|
||||
sem_post(reqSem);
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_sec += timeoutMs / 1000;
|
||||
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
|
||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||
return sem_timedwait(rspSem, &ts) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── RPC helpers ──────────────────────────────────────────────── */
|
||||
|
||||
bool rpc_ping()
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
hdr->command = RPC_CMD_PING;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
return signalAndWait();
|
||||
}
|
||||
|
||||
bool rpc_read(uint64_t addr, void* buf, uint32_t len)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_READ_BATCH;
|
||||
hdr->requestCount = 1;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
auto* entry = (RcxRpcReadEntry*)data;
|
||||
entry->address = addr;
|
||||
entry->length = len;
|
||||
entry->dataOffset = sizeof(RcxRpcReadEntry);
|
||||
|
||||
if (!signalAndWait()) return false;
|
||||
memcpy(buf, data + entry->dataOffset, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool rpc_read_batch(const uint64_t* addrs, const uint32_t* lens,
|
||||
uint32_t count, uint8_t* outBuf)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_READ_BATCH;
|
||||
hdr->requestCount = count;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
/* lay out entries, then data offsets after all entries */
|
||||
uint32_t entriesSize = count * (uint32_t)sizeof(RcxRpcReadEntry);
|
||||
uint32_t dataOff = entriesSize;
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry));
|
||||
e->address = addrs[i];
|
||||
e->length = lens[i];
|
||||
e->dataOffset = dataOff;
|
||||
dataOff += lens[i];
|
||||
}
|
||||
|
||||
if (!signalAndWait()) return false;
|
||||
|
||||
/* copy out response data */
|
||||
uint32_t off = 0;
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry));
|
||||
memcpy(outBuf + off, data + e->dataOffset, e->length);
|
||||
off += e->length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool rpc_write(uint64_t addr, const void* buf, uint32_t len)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_WRITE;
|
||||
hdr->writeAddress = addr;
|
||||
hdr->writeLength = len;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
memcpy(data, buf, len);
|
||||
|
||||
if (!signalAndWait()) return false;
|
||||
return hdr->status == RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
struct ModInfo { uint64_t base; uint64_t size; char name[256]; };
|
||||
|
||||
int rpc_enum_modules(ModInfo* out, int maxOut)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_ENUM_MODULES;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
if (!signalAndWait()) return -1;
|
||||
if (hdr->status != RCX_RPC_STATUS_OK) return -1;
|
||||
|
||||
int count = (int)hdr->responseCount;
|
||||
if (count > maxOut) count = maxOut;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto* entry = (RcxRpcModuleEntry*)(data + i * sizeof(RcxRpcModuleEntry));
|
||||
out[i].base = entry->base;
|
||||
out[i].size = entry->size;
|
||||
#ifdef _WIN32
|
||||
/* names are UTF-16 on Windows */
|
||||
int wchars = (int)(entry->nameLength / sizeof(wchar_t));
|
||||
WideCharToMultiByte(CP_UTF8, 0,
|
||||
(const wchar_t*)(data + entry->nameOffset), wchars,
|
||||
out[i].name, 255, nullptr, nullptr);
|
||||
out[i].name[255] = '\0';
|
||||
#else
|
||||
int nLen = (int)entry->nameLength;
|
||||
if (nLen > 255) nLen = 255;
|
||||
memcpy(out[i].name, data + entry->nameOffset, nLen);
|
||||
out[i].name[nLen] = '\0';
|
||||
#endif
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void rpc_shutdown()
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
hdr->command = RPC_CMD_SHUTDOWN;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
signalAndWait(500);
|
||||
}
|
||||
};
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Auto-spawn host
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
#ifdef _WIN32
|
||||
static HANDLE g_hostProcess = nullptr;
|
||||
#else
|
||||
static pid_t g_hostPid = 0;
|
||||
#endif
|
||||
static FILE* g_hostPipe = nullptr;
|
||||
|
||||
static bool spawn_host(uint32_t* outPid,
|
||||
uint64_t* outTestBuf, uint32_t* outTestLen)
|
||||
{
|
||||
/* resolve path to test_rpc_host next to ourselves */
|
||||
char cmd[2048];
|
||||
#ifdef _WIN32
|
||||
char exePath[MAX_PATH];
|
||||
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
|
||||
char* slash = strrchr(exePath, '\\');
|
||||
if (!slash) slash = strrchr(exePath, '/');
|
||||
if (slash) *(slash + 1) = '\0';
|
||||
snprintf(cmd, sizeof(cmd), "\"%stest_rpc_host.exe\" autotest", exePath);
|
||||
g_hostPipe = _popen(cmd, "r");
|
||||
#else
|
||||
char exePath[PATH_MAX];
|
||||
ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
|
||||
if (n <= 0) return false;
|
||||
exePath[n] = '\0';
|
||||
char* dir = dirname(exePath);
|
||||
snprintf(cmd, sizeof(cmd), "%s/test_rpc_host autotest", dir);
|
||||
g_hostPipe = popen(cmd, "r");
|
||||
#endif
|
||||
if (!g_hostPipe) {
|
||||
fprintf(stderr, "ERROR: cannot spawn host: %s\n", cmd);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* read READY line */
|
||||
char line[512];
|
||||
if (!fgets(line, sizeof(line), g_hostPipe)) {
|
||||
fprintf(stderr, "ERROR: no output from host\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* parse: READY pid=X testbuf=0xZ testlen=N */
|
||||
unsigned long long tbuf = 0;
|
||||
unsigned tlen = 0;
|
||||
if (sscanf(line, "READY pid=%u testbuf=0x%llx testlen=%u",
|
||||
outPid, &tbuf, &tlen) < 1) {
|
||||
fprintf(stderr, "ERROR: cannot parse host output: %s\n", line);
|
||||
return false;
|
||||
}
|
||||
*outTestBuf = (uint64_t)tbuf;
|
||||
*outTestLen = (uint32_t)tlen;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cleanup_host()
|
||||
{
|
||||
if (g_hostPipe) {
|
||||
#ifdef _WIN32
|
||||
_pclose(g_hostPipe);
|
||||
#else
|
||||
pclose(g_hostPipe);
|
||||
#endif
|
||||
g_hostPipe = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Printing helpers
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static void print_pass(const char* name) { printf(" [PASS] %s\n", name); }
|
||||
static void print_fail(const char* name) { printf(" [FAIL] %s\n", name); exit(1); }
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* main
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
uint32_t pid = 0;
|
||||
uint64_t testBuf = 0;
|
||||
uint32_t testLen = 0;
|
||||
bool autoMode = false;
|
||||
|
||||
if (argc >= 2) {
|
||||
pid = (uint32_t)atoi(argv[1]);
|
||||
if (argc >= 4) {
|
||||
testBuf = (uint64_t)strtoull(argv[2], nullptr, 0);
|
||||
testLen = (uint32_t)atoi(argv[3]);
|
||||
}
|
||||
} else {
|
||||
autoMode = true;
|
||||
printf("Auto-spawning test_rpc_host...\n");
|
||||
if (!spawn_host(&pid, &testBuf, &testLen)) return 1;
|
||||
}
|
||||
|
||||
printf("Connecting to PID=%u testbuf=0x%llx testlen=%u\n\n",
|
||||
pid, (unsigned long long)testBuf, testLen);
|
||||
|
||||
/* ── connect ── */
|
||||
TestIpcClient ipc;
|
||||
if (!ipc.connect(pid)) {
|
||||
fprintf(stderr, "ERROR: IPC connect failed\n");
|
||||
if (autoMode) cleanup_host();
|
||||
return 1;
|
||||
}
|
||||
printf("=== Functional Tests ===\n");
|
||||
|
||||
/* ── test: ping ── */
|
||||
if (ipc.rpc_ping()) print_pass("Ping");
|
||||
else print_fail("Ping");
|
||||
|
||||
/* ── test: enumerate modules ── */
|
||||
TestIpcClient::ModInfo mods[512];
|
||||
int modCount = ipc.rpc_enum_modules(mods, 512);
|
||||
if (modCount > 0) {
|
||||
printf(" [PASS] EnumModules (%d modules)\n", modCount);
|
||||
printf(" first: %s base=0x%llx size=0x%llx\n",
|
||||
mods[0].name,
|
||||
(unsigned long long)mods[0].base,
|
||||
(unsigned long long)mods[0].size);
|
||||
} else {
|
||||
print_fail("EnumModules");
|
||||
}
|
||||
|
||||
/* ── test: read module header (MZ / ELF magic) ── */
|
||||
if (modCount > 0) {
|
||||
uint8_t header[4] = {};
|
||||
if (ipc.rpc_read(mods[0].base, header, 4)) {
|
||||
#ifdef _WIN32
|
||||
if (header[0] == 'M' && header[1] == 'Z')
|
||||
print_pass("ReadModuleHeader (MZ)");
|
||||
else
|
||||
print_fail("ReadModuleHeader (expected MZ)");
|
||||
#else
|
||||
if (header[0] == 0x7F && header[1] == 'E' &&
|
||||
header[2] == 'L' && header[3] == 'F')
|
||||
print_pass("ReadModuleHeader (ELF)");
|
||||
else
|
||||
print_fail("ReadModuleHeader (expected ELF)");
|
||||
#endif
|
||||
} else {
|
||||
print_fail("ReadModuleHeader (read failed)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── test: read test buffer (known pattern) ── */
|
||||
if (testBuf && testLen >= 4096) {
|
||||
uint8_t buf[4096];
|
||||
if (ipc.rpc_read(testBuf, buf, 4096)) {
|
||||
bool good = true;
|
||||
for (int i = 0; i < 4096; ++i) {
|
||||
if (buf[i] != (uint8_t)(i & 0xFF)) { good = false; break; }
|
||||
}
|
||||
if (good) print_pass("ReadTestBuffer (4096 bytes, pattern verified)");
|
||||
else print_fail("ReadTestBuffer (pattern mismatch)");
|
||||
} else {
|
||||
print_fail("ReadTestBuffer (read failed)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── test: write ── */
|
||||
if (testBuf && testLen >= 16) {
|
||||
uint8_t patch[4] = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
if (ipc.rpc_write(testBuf, patch, 4)) {
|
||||
uint8_t verify[4] = {};
|
||||
ipc.rpc_read(testBuf, verify, 4);
|
||||
if (memcmp(verify, patch, 4) == 0)
|
||||
print_pass("Write + ReadBack (0xDEADBEEF)");
|
||||
else
|
||||
print_fail("Write + ReadBack (readback mismatch)");
|
||||
} else {
|
||||
print_fail("Write (write failed)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── test: batch read ── */
|
||||
if (testBuf && testLen >= 8192) {
|
||||
const uint32_t N = 4;
|
||||
uint64_t addrs[N];
|
||||
uint32_t lens[N];
|
||||
for (uint32_t i = 0; i < N; ++i) {
|
||||
addrs[i] = testBuf + i * 1024;
|
||||
lens[i] = 1024;
|
||||
}
|
||||
uint8_t out[4096];
|
||||
if (ipc.rpc_read_batch(addrs, lens, N, out)) {
|
||||
print_pass("BatchRead (4 x 1024 bytes)");
|
||||
} else {
|
||||
print_fail("BatchRead");
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n=== Benchmarks ===\n");
|
||||
|
||||
/* choose a valid address for benchmarking */
|
||||
uint64_t benchAddr = testBuf ? testBuf : (modCount > 0 ? mods[0].base : 0);
|
||||
if (!benchAddr) {
|
||||
printf(" (no valid address for benchmarks, skipping)\n");
|
||||
} else {
|
||||
|
||||
/* ── benchmark: single 4 KB reads ── */
|
||||
{
|
||||
const int ITERS = 10000;
|
||||
const int PAGE = 4096;
|
||||
uint8_t tmp[4096];
|
||||
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_read(benchAddr, tmp, PAGE);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0);
|
||||
|
||||
printf(" Single 4 KB reads:\n");
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f MB\n", totalMB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
|
||||
printf(" Avg latency: %.2f us/read\n", us / ITERS);
|
||||
}
|
||||
|
||||
/* ── benchmark: single 64 B reads (pointer-chase-size) ── */
|
||||
{
|
||||
const int ITERS = 50000;
|
||||
const int SZ = 64;
|
||||
uint8_t tmp[64];
|
||||
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_read(benchAddr, tmp, SZ);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalKB = (double)ITERS * SZ / 1024.0;
|
||||
|
||||
printf(" Single 64 B reads (pointer-chase):\n");
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f KB\n", totalKB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f KB/s\n", totalKB / secs);
|
||||
printf(" Avg latency: %.2f us/read\n", us / ITERS);
|
||||
}
|
||||
|
||||
/* ── benchmark: batch read (50 x 4 KB, simulating refresh) ── */
|
||||
{
|
||||
const int ITERS = 2000;
|
||||
const uint32_t BATCH = 50;
|
||||
const uint32_t PAGE = 4096;
|
||||
|
||||
uint64_t addrs[BATCH];
|
||||
uint32_t lens[BATCH];
|
||||
for (uint32_t i = 0; i < BATCH; ++i) {
|
||||
/* wrap within test buffer or module */
|
||||
addrs[i] = benchAddr + (i * PAGE) % 65536;
|
||||
lens[i] = PAGE;
|
||||
}
|
||||
|
||||
/* allocate response buffer */
|
||||
uint8_t* outBuf = (uint8_t*)malloc(BATCH * PAGE);
|
||||
if (!outBuf) {
|
||||
printf(" (batch malloc failed, skipping)\n");
|
||||
} else {
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_read_batch(addrs, lens, BATCH, outBuf);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalMB = (double)ITERS * BATCH * PAGE / (1024.0 * 1024.0);
|
||||
|
||||
printf(" Batch read (%u x %u B, simulating refresh):\n", BATCH, PAGE);
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f MB\n", totalMB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
|
||||
printf(" Avg latency: %.2f us/batch\n", us / ITERS);
|
||||
printf(" Per-page : %.2f us/page\n", us / (ITERS * BATCH));
|
||||
|
||||
free(outBuf);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── benchmark: write 4 KB ── */
|
||||
if (testBuf && testLen >= 4096) {
|
||||
const int ITERS = 10000;
|
||||
const int PAGE = 4096;
|
||||
uint8_t tmp[4096];
|
||||
memset(tmp, 0x42, sizeof(tmp));
|
||||
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_write(testBuf, tmp, PAGE);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0);
|
||||
|
||||
printf(" Write 4 KB:\n");
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f MB\n", totalMB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
|
||||
printf(" Avg latency: %.2f us/write\n", us / ITERS);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── shutdown ── */
|
||||
printf("\nSending shutdown...\n");
|
||||
ipc.rpc_shutdown();
|
||||
ipc.disconnect();
|
||||
|
||||
if (autoMode) {
|
||||
/* wait for host to exit */
|
||||
#ifdef _WIN32
|
||||
Sleep(500);
|
||||
#else
|
||||
usleep(500000);
|
||||
#endif
|
||||
cleanup_host();
|
||||
}
|
||||
|
||||
printf("Done.\n");
|
||||
return 0;
|
||||
}
|
||||
187
plugins/RemoteProcessMemory/tests/test_rpc_host.cpp
Normal file
187
plugins/RemoteProcessMemory/tests/test_rpc_host.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* test_rpc_host -- loads rcx_payload in-process, acts as the "target".
|
||||
*
|
||||
* Usage: test_rpc_host
|
||||
*
|
||||
* Prints a READY line (machine-parseable), then waits for the payload
|
||||
* to shut down (RPC_CMD_SHUTDOWN from the client).
|
||||
*/
|
||||
|
||||
#include "../rcx_rpc_protocol.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
# include <dlfcn.h>
|
||||
# include <fcntl.h>
|
||||
# include <sys/mman.h>
|
||||
# include <semaphore.h>
|
||||
# include <libgen.h>
|
||||
# include <limits.h>
|
||||
#endif
|
||||
|
||||
/* ── Helpers ──────────────────────────────────────────────────────── */
|
||||
|
||||
static uint32_t current_pid()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return (uint32_t)GetCurrentProcessId();
|
||||
#else
|
||||
return (uint32_t)getpid();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void sleep_ms(int ms)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
Sleep((DWORD)ms);
|
||||
#else
|
||||
usleep((useconds_t)ms * 1000);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Resolve payload path relative to this executable */
|
||||
static int payload_path(char* out, int outLen)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
char exePath[MAX_PATH];
|
||||
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
|
||||
char* slash = strrchr(exePath, '\\');
|
||||
if (!slash) slash = strrchr(exePath, '/');
|
||||
if (slash) *(slash + 1) = '\0';
|
||||
snprintf(out, outLen, "%srcx_payload.dll", exePath);
|
||||
#else
|
||||
char exePath[PATH_MAX];
|
||||
ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
|
||||
if (n <= 0) return -1;
|
||||
exePath[n] = '\0';
|
||||
char* dir = dirname(exePath);
|
||||
snprintf(out, outLen, "%s/rcx_payload.so", dir);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Open the main shared memory (read-only, just to monitor payloadReady) */
|
||||
static void* open_main_shm(uint32_t pid)
|
||||
{
|
||||
char shmName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE h = nullptr;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
h = OpenFileMappingA(FILE_MAP_READ, FALSE, shmName);
|
||||
if (h) break;
|
||||
sleep_ms(10);
|
||||
}
|
||||
if (!h) return nullptr;
|
||||
void* v = MapViewOfFile(h, FILE_MAP_READ, 0, 0, sizeof(RcxRpcHeader));
|
||||
return v;
|
||||
#else
|
||||
int fd = -1;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
fd = shm_open(shmName, O_RDONLY, 0);
|
||||
if (fd >= 0) break;
|
||||
sleep_ms(10);
|
||||
}
|
||||
if (fd < 0) return nullptr;
|
||||
void* v = mmap(nullptr, sizeof(RcxRpcHeader), PROT_READ, MAP_SHARED, fd, 0);
|
||||
close(fd);
|
||||
return (v == MAP_FAILED) ? nullptr : v;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── Test buffer (known pattern for client to verify reads/writes) ── */
|
||||
static uint8_t g_testBuf[65536];
|
||||
|
||||
/* ── main ─────────────────────────────────────────────────────────── */
|
||||
|
||||
int main(int, char**)
|
||||
{
|
||||
uint32_t pid = current_pid();
|
||||
|
||||
/* fill test buffer with known pattern */
|
||||
for (int i = 0; i < (int)sizeof(g_testBuf); ++i)
|
||||
g_testBuf[i] = (uint8_t)(i & 0xFF);
|
||||
|
||||
/* load payload */
|
||||
char plPath[1024];
|
||||
if (payload_path(plPath, sizeof(plPath)) != 0) {
|
||||
fprintf(stderr, "ERROR: cannot determine payload path\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
HMODULE hPayload = LoadLibraryA(plPath);
|
||||
if (!hPayload) {
|
||||
fprintf(stderr, "ERROR: LoadLibrary(%s) failed (%lu)\n",
|
||||
plPath, GetLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Call RcxPayloadInit() — DllMain is minimal, init must be explicit */
|
||||
typedef bool (*RcxPayloadInitFn)();
|
||||
auto pfnInit = (RcxPayloadInitFn)GetProcAddress(hPayload, "RcxPayloadInit");
|
||||
if (!pfnInit || !pfnInit()) {
|
||||
fprintf(stderr, "ERROR: RcxPayloadInit() failed or not found\n");
|
||||
FreeLibrary(hPayload);
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
void* hPayload = dlopen(plPath, RTLD_NOW);
|
||||
if (!hPayload) {
|
||||
fprintf(stderr, "ERROR: dlopen(%s): %s\n", plPath, dlerror());
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* open main shm and wait for payloadReady */
|
||||
void* shmView = open_main_shm(pid);
|
||||
if (!shmView) {
|
||||
fprintf(stderr, "ERROR: failed to open main shared memory\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
RcxRpcHeader* hdr = (RcxRpcHeader*)shmView;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
if (hdr->payloadReady) break;
|
||||
sleep_ms(10);
|
||||
}
|
||||
if (!hdr->payloadReady) {
|
||||
fprintf(stderr, "ERROR: payload did not become ready\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* print READY line for the client to parse */
|
||||
printf("READY pid=%u testbuf=0x%llx testlen=%u\n",
|
||||
pid,
|
||||
(unsigned long long)(uintptr_t)g_testBuf,
|
||||
(unsigned)sizeof(g_testBuf));
|
||||
fflush(stdout);
|
||||
|
||||
/* wait until payload shuts down */
|
||||
while (hdr->payloadReady)
|
||||
sleep_ms(100);
|
||||
|
||||
printf("Payload shut down, exiting.\n");
|
||||
|
||||
#ifdef _WIN32
|
||||
/* give the timer queue a moment to drain */
|
||||
Sleep(200);
|
||||
FreeLibrary(hPayload);
|
||||
if (shmView) UnmapViewOfFile(shmView);
|
||||
#else
|
||||
usleep(200000);
|
||||
dlclose(hPayload);
|
||||
if (shmView) munmap(shmView, sizeof(RcxRpcHeader));
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -197,53 +197,15 @@ void WinDbgMemoryProvider::querySessionInfo()
|
||||
}
|
||||
}
|
||||
|
||||
if (m_symbols) {
|
||||
ULONG numModules = 0, numUnloaded = 0;
|
||||
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
|
||||
qDebug() << "[WinDbg] GetNumberModules hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "loaded=" << numModules << "unloaded=" << numUnloaded;
|
||||
if (SUCCEEDED(hr) && numModules > 0) {
|
||||
char modName[256] = {};
|
||||
ULONG modSize = 0;
|
||||
hr = m_symbols->GetModuleNames(0, 0, nullptr, 0, nullptr,
|
||||
modName, sizeof(modName), &modSize,
|
||||
nullptr, 0, nullptr);
|
||||
if (SUCCEEDED(hr) && modSize > 0)
|
||||
m_name = QString::fromUtf8(modName);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_name.isEmpty())
|
||||
m_name = m_isLive ? QStringLiteral("DbgEng (Live)") : QStringLiteral("DbgEng (Dump)");
|
||||
|
||||
if (m_symbols) {
|
||||
ULONG numModules = 0, numUnloaded = 0;
|
||||
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
|
||||
if (SUCCEEDED(hr) && numModules > 0) {
|
||||
ULONG64 moduleBase = 0;
|
||||
hr = m_symbols->GetModuleByIndex(0, &moduleBase);
|
||||
qDebug() << "[WinDbg] Module 0 base=" << Qt::hex << moduleBase;
|
||||
if (SUCCEEDED(hr))
|
||||
m_base = moduleBase;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_base && m_dataSpaces) {
|
||||
uint8_t probe[2] = {};
|
||||
ULONG got = 0;
|
||||
hr = m_dataSpaces->ReadVirtual(m_base, probe, 2, &got);
|
||||
qDebug() << "[WinDbg] Probe read at" << Qt::hex << m_base
|
||||
<< "hr=" << (unsigned long)hr << "got=" << got
|
||||
<< "bytes:" << (int)probe[0] << (int)probe[1];
|
||||
if (FAILED(hr) || got == 0) {
|
||||
qWarning() << "[WinDbg] Probe read FAILED — cleaning up";
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// WinDbg provides access to the entire virtual address space.
|
||||
// Do NOT auto-select a module as base — let the user set their
|
||||
// own base address. m_base stays 0 so the controller won't
|
||||
// override tree.baseAddress.
|
||||
m_name = m_isLive ? QStringLiteral("WinDbg (Live)")
|
||||
: QStringLiteral("WinDbg (Dump)");
|
||||
|
||||
qDebug() << "[WinDbg] Ready. name=" << m_name
|
||||
<< "base=" << Qt::hex << m_base << "isLive=" << m_isLive;
|
||||
<< "isLive=" << m_isLive;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -305,8 +267,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;
|
||||
|
||||
@@ -83,6 +83,7 @@ private:
|
||||
bool m_isLive = false;
|
||||
bool m_writable = false;
|
||||
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
|
||||
mutable int m_readFailCount = 0;
|
||||
|
||||
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe
|
||||
// transport is thread-affine — all calls must happen on the thread
|
||||
|
||||
300
src/addressparser.cpp
Normal file
300
src/addressparser.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
#include "addressparser.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Address Expression Parser ──────────────────────────────────────────
|
||||
//
|
||||
// Parses expressions like:
|
||||
// "7FF66CCE0000" → plain hex address
|
||||
// "0x100 + 0x200" → arithmetic on hex values
|
||||
// "<Program.exe> + 0xDE" → module base + offset
|
||||
// "[<Program.exe> + 0xDE] - AB" → dereference pointer, then subtract
|
||||
// "7ff6`6cce0000" → WinDbg-style backtick separator (stripped before parsing)
|
||||
//
|
||||
// Grammar (standard operator precedence: *, / bind tighter than +, -):
|
||||
//
|
||||
// expr = term (('+' | '-') term)*
|
||||
// term = unary (('*' | '/') unary)*
|
||||
// unary = '-' unary | atom
|
||||
// atom = '[' expr ']' -- read pointer at address (dereference)
|
||||
// | '<' moduleName '>' -- resolve module base address
|
||||
// | '(' expr ')' -- grouping
|
||||
// | hexLiteral -- hex number, optional 0x prefix
|
||||
//
|
||||
// All numeric literals are hexadecimal (base 16).
|
||||
// Module names and pointer reads are resolved via optional callbacks.
|
||||
// Without callbacks, modules and dereferences evaluate to 0 (syntax-check mode).
|
||||
|
||||
class ExpressionParser {
|
||||
public:
|
||||
ExpressionParser(const QString& input, const AddressParserCallbacks* callbacks)
|
||||
: m_input(input), m_callbacks(callbacks) {}
|
||||
|
||||
AddressParseResult parse() {
|
||||
skipSpaces();
|
||||
if (atEnd())
|
||||
return error("empty expression");
|
||||
|
||||
uint64_t value = 0;
|
||||
if (!parseExpression(value))
|
||||
return error(m_error);
|
||||
|
||||
skipSpaces();
|
||||
if (!atEnd())
|
||||
return error(QStringLiteral("unexpected '%1'").arg(m_input[m_pos]));
|
||||
|
||||
return {true, value, {}, -1};
|
||||
}
|
||||
|
||||
private:
|
||||
const QString& m_input;
|
||||
const AddressParserCallbacks* m_callbacks;
|
||||
int m_pos = 0;
|
||||
QString m_error;
|
||||
int m_errorPos = 0;
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
bool atEnd() const { return m_pos >= m_input.size(); }
|
||||
|
||||
QChar peek() const { return atEnd() ? QChar('\0') : m_input[m_pos]; }
|
||||
|
||||
void advance() { m_pos++; }
|
||||
|
||||
void skipSpaces() {
|
||||
while (!atEnd() && m_input[m_pos].isSpace())
|
||||
m_pos++;
|
||||
}
|
||||
|
||||
AddressParseResult error(const QString& msg) const {
|
||||
return {false, 0, msg, m_errorPos};
|
||||
}
|
||||
|
||||
bool fail(const QString& msg) {
|
||||
m_error = msg;
|
||||
m_errorPos = m_pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool expect(QChar ch) {
|
||||
skipSpaces();
|
||||
if (peek() != ch)
|
||||
return fail(QStringLiteral("expected '%1'").arg(ch));
|
||||
advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool isHexDigit(QChar ch) {
|
||||
return (ch >= '0' && ch <= '9')
|
||||
|| (ch >= 'a' && ch <= 'f')
|
||||
|| (ch >= 'A' && ch <= 'F');
|
||||
}
|
||||
|
||||
// ── Recursive descent parsing ──
|
||||
|
||||
// expr = term (('+' | '-') term)*
|
||||
bool parseExpression(uint64_t& result) {
|
||||
if (!parseTerm(result))
|
||||
return false;
|
||||
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
QChar op = peek();
|
||||
if (op != '+' && op != '-')
|
||||
break;
|
||||
advance();
|
||||
|
||||
uint64_t rhs = 0;
|
||||
if (!parseTerm(rhs))
|
||||
return false;
|
||||
|
||||
result = (op == '+') ? result + rhs : result - rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// term = unary (('*' | '/') unary)*
|
||||
bool parseTerm(uint64_t& result) {
|
||||
if (!parseUnary(result))
|
||||
return false;
|
||||
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
QChar op = peek();
|
||||
if (op != '*' && op != '/')
|
||||
break;
|
||||
advance();
|
||||
|
||||
uint64_t rhs = 0;
|
||||
if (!parseUnary(rhs))
|
||||
return false;
|
||||
|
||||
if (op == '*') {
|
||||
result *= rhs;
|
||||
} else {
|
||||
if (rhs == 0)
|
||||
return fail("division by zero");
|
||||
result /= rhs;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// unary = '-' unary | atom
|
||||
bool parseUnary(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (peek() == '-') {
|
||||
advance();
|
||||
uint64_t inner = 0;
|
||||
if (!parseUnary(inner))
|
||||
return false;
|
||||
result = static_cast<uint64_t>(-static_cast<int64_t>(inner));
|
||||
return true;
|
||||
}
|
||||
return parseAtom(result);
|
||||
}
|
||||
|
||||
// atom = '[' expr ']' | '<' name '>' | '(' expr ')' | hexLiteral
|
||||
bool parseAtom(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (atEnd())
|
||||
return fail("unexpected end of expression");
|
||||
|
||||
QChar ch = peek();
|
||||
|
||||
if (ch == '[') return parseDereference(result);
|
||||
if (ch == '<') return parseModuleName(result);
|
||||
if (ch == '(') return parseGrouping(result);
|
||||
return parseHexNumber(result);
|
||||
}
|
||||
|
||||
// '[' expr ']' — read the pointer value at the computed address
|
||||
bool parseDereference(uint64_t& result) {
|
||||
advance(); // skip '['
|
||||
|
||||
uint64_t address = 0;
|
||||
if (!parseExpression(address))
|
||||
return false;
|
||||
if (!expect(']'))
|
||||
return false;
|
||||
|
||||
// Without a callback, just return 0 (syntax-check mode)
|
||||
if (!m_callbacks || !m_callbacks->readPointer) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
result = m_callbacks->readPointer(address, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("failed to read memory at 0x%1").arg(address, 0, 16));
|
||||
return true;
|
||||
}
|
||||
|
||||
// '<' moduleName '>' — resolve a module's base address (e.g. <Program.exe>)
|
||||
bool parseModuleName(uint64_t& result) {
|
||||
advance(); // skip '<'
|
||||
|
||||
int nameStart = m_pos;
|
||||
while (!atEnd() && peek() != '>')
|
||||
advance();
|
||||
if (atEnd())
|
||||
return fail("expected '>'");
|
||||
|
||||
QString name = m_input.mid(nameStart, m_pos - nameStart).trimmed();
|
||||
advance(); // skip '>'
|
||||
|
||||
if (name.isEmpty())
|
||||
return fail("empty module name");
|
||||
|
||||
// Without a callback, just return 0 (syntax-check mode)
|
||||
if (!m_callbacks || !m_callbacks->resolveModule) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
result = m_callbacks->resolveModule(name, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("module '%1' not found").arg(name));
|
||||
return true;
|
||||
}
|
||||
|
||||
// '(' expr ')' — parenthesized sub-expression for grouping
|
||||
bool parseGrouping(uint64_t& result) {
|
||||
advance(); // skip '('
|
||||
if (!parseExpression(result))
|
||||
return false;
|
||||
return expect(')');
|
||||
}
|
||||
|
||||
// Hex number with optional "0x" prefix. All literals are base-16.
|
||||
bool parseHexNumber(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (atEnd())
|
||||
return fail("unexpected end of expression");
|
||||
|
||||
int start = m_pos;
|
||||
|
||||
// Skip optional 0x/0X prefix
|
||||
if (m_pos + 1 < m_input.size()
|
||||
&& m_input[m_pos] == '0'
|
||||
&& (m_input[m_pos + 1] == 'x' || m_input[m_pos + 1] == 'X'))
|
||||
m_pos += 2;
|
||||
|
||||
// Consume hex digits
|
||||
int digitsStart = m_pos;
|
||||
while (!atEnd() && isHexDigit(peek()))
|
||||
advance();
|
||||
|
||||
if (m_pos == digitsStart) {
|
||||
m_errorPos = start;
|
||||
return fail("expected hex number");
|
||||
}
|
||||
|
||||
QString digits = m_input.mid(digitsStart, m_pos - digitsStart);
|
||||
bool ok = false;
|
||||
result = digits.toULongLong(&ok, 16);
|
||||
if (!ok) {
|
||||
m_errorPos = start;
|
||||
return fail("invalid hex number");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────
|
||||
|
||||
AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize,
|
||||
const AddressParserCallbacks* cb)
|
||||
{
|
||||
Q_UNUSED(ptrSize);
|
||||
|
||||
// WinDbg displays 64-bit addresses with backtick separators for readability,
|
||||
// e.g. "00007ff6`1a2b3c4d". Strip them so users can paste directly.
|
||||
// Also remove ' in case user uses it
|
||||
QString cleaned = formula;
|
||||
cleaned.remove('`');
|
||||
cleaned.remove('\'');
|
||||
|
||||
ExpressionParser parser(cleaned, cb);
|
||||
return parser.parse();
|
||||
}
|
||||
|
||||
QString AddressParser::validate(const QString& formula)
|
||||
{
|
||||
QString cleaned = formula;
|
||||
cleaned.remove('`');
|
||||
cleaned.remove('\'');
|
||||
cleaned = cleaned.trimmed();
|
||||
if (cleaned.isEmpty())
|
||||
return QStringLiteral("empty");
|
||||
|
||||
// Parse with no callbacks — modules and dereferences succeed but return 0.
|
||||
// This checks syntax only.
|
||||
ExpressionParser parser(cleaned, nullptr);
|
||||
auto result = parser.parse();
|
||||
return result.ok ? QString() : result.error;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
27
src/addressparser.h
Normal file
27
src/addressparser.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct AddressParseResult {
|
||||
bool ok;
|
||||
uint64_t value;
|
||||
QString error;
|
||||
int errorPos;
|
||||
};
|
||||
|
||||
struct AddressParserCallbacks {
|
||||
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
|
||||
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
|
||||
};
|
||||
|
||||
class AddressParser {
|
||||
public:
|
||||
static AddressParseResult evaluate(const QString& formula, int ptrSize = 8,
|
||||
const AddressParserCallbacks* cb = nullptr);
|
||||
static QString validate(const QString& formula);
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
146
src/compose.cpp
146
src/compose.cpp
@@ -22,6 +22,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 +105,13 @@ static inline uint64_t resolveAddr(const ComposeState& state,
|
||||
return state.absOffsets[nodeIdx];
|
||||
}
|
||||
|
||||
|
||||
static const QVector<int>& childIndices(const ComposeState& state, uint64_t parentId) {
|
||||
static const QVector<int> kEmpty;
|
||||
auto it = state.childMap.constFind(parentId);
|
||||
return it == state.childMap.constEnd() ? kEmpty : it.value();
|
||||
}
|
||||
|
||||
void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
const Provider& prov, int nodeIdx,
|
||||
int depth, uint64_t absAddr, uint64_t scopeId) {
|
||||
@@ -119,10 +127,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 +161,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 +171,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 +262,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,19 +272,69 @@ 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());
|
||||
|
||||
for (int mi = 0; mi < node.enumMembers.size(); mi++) {
|
||||
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.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;
|
||||
}
|
||||
|
||||
const QVector<int>& children = childIndices(state, node.id);
|
||||
|
||||
int childDepth = depth + 1;
|
||||
|
||||
@@ -294,16 +365,19 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,10 +403,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
if (node.kind == NodeKind::Struct && children.isEmpty() && node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
QVector<int> refChildren = state.childMap.value(node.refId);
|
||||
std::sort(refChildren.begin(), refChildren.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
const QVector<int>& refChildren = childIndices(state, node.refId);
|
||||
// Use the referenced struct's scope widths (children come from there)
|
||||
uint64_t refScopeId = node.refId;
|
||||
for (int childIdx : refChildren) {
|
||||
@@ -341,6 +412,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 +429,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,
|
||||
@@ -421,7 +494,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 +521,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 +560,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 +619,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 +669,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 +718,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,7 +736,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -678,10 +759,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
|
||||
|
||||
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 ──
|
||||
@@ -92,16 +93,22 @@ public:
|
||||
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 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 +124,7 @@ public:
|
||||
RcxDocument* document() const { return m_doc; }
|
||||
void setEditorFont(const QString& fontName);
|
||||
void setRefreshInterval(int ms);
|
||||
void setCompactColumns(bool v);
|
||||
|
||||
// MCP bridge accessors
|
||||
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
||||
@@ -124,11 +132,17 @@ public:
|
||||
const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; }
|
||||
int activeSourceIndex() const { return m_activeSourceIdx; }
|
||||
void switchSource(int idx) { switchToSavedSource(idx); }
|
||||
void clearSources();
|
||||
void selectSource(const QString& text);
|
||||
void copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx);
|
||||
|
||||
// Value tracking toggle (per-tab, off by default)
|
||||
bool trackValues() const { return m_trackValues; }
|
||||
void setTrackValues(bool on);
|
||||
|
||||
// Cross-tab type visibility: point at the project's full document list
|
||||
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
||||
|
||||
// Test accessor
|
||||
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||
|
||||
@@ -143,6 +157,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 ──
|
||||
@@ -165,13 +180,14 @@ private:
|
||||
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 ──
|
||||
|
||||
72
src/core.h
72
src/core.h
@@ -142,6 +142,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;
|
||||
@@ -184,8 +193,10 @@ struct Node {
|
||||
int strLen = 64;
|
||||
bool collapsed = false;
|
||||
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
|
||||
NodeKind elementKind = NodeKind::UInt8; // Array: element type
|
||||
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
|
||||
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
|
||||
int viewIndex = 0; // Array: current view offset (transient)
|
||||
QVector<QPair<QString, int64_t>> enumMembers; // Enum: name→value pairs
|
||||
|
||||
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
|
||||
int byteSize() const {
|
||||
@@ -217,6 +228,18 @@ struct Node {
|
||||
o["collapsed"] = collapsed;
|
||||
o["refId"] = QString::number(refId);
|
||||
o["elementKind"] = kindToString(elementKind);
|
||||
if (ptrDepth > 0)
|
||||
o["ptrDepth"] = ptrDepth;
|
||||
if (!enumMembers.isEmpty()) {
|
||||
QJsonArray arr;
|
||||
for (const auto& m : enumMembers) {
|
||||
QJsonObject em;
|
||||
em["name"] = m.first;
|
||||
em["value"] = QString::number(m.second);
|
||||
arr.append(em);
|
||||
}
|
||||
o["enumMembers"] = arr;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
static Node fromJson(const QJsonObject& o) {
|
||||
@@ -233,6 +256,15 @@ struct Node {
|
||||
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()});
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -254,6 +286,7 @@ struct Node {
|
||||
struct NodeTree {
|
||||
QVector<Node> nodes;
|
||||
uint64_t baseAddress = 0x00400000;
|
||||
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
|
||||
uint64_t m_nextId = 1;
|
||||
mutable QHash<uint64_t, int> m_idCache;
|
||||
|
||||
@@ -387,6 +420,8 @@ struct NodeTree {
|
||||
QJsonObject toJson() const {
|
||||
QJsonObject o;
|
||||
o["baseAddress"] = QString::number(baseAddress, 16);
|
||||
if (!baseAddressFormula.isEmpty())
|
||||
o["baseAddressFormula"] = baseAddressFormula;
|
||||
o["nextId"] = QString::number(m_nextId);
|
||||
QJsonArray arr;
|
||||
for (const auto& n : nodes) arr.append(n.toJson());
|
||||
@@ -397,6 +432,7 @@ struct NodeTree {
|
||||
static NodeTree fromJson(const QJsonObject& o) {
|
||||
NodeTree t;
|
||||
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
|
||||
t.baseAddressFormula = o["baseAddressFormula"].toString();
|
||||
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
||||
QJsonArray arr = o["nodes"].toArray();
|
||||
for (const auto& v : arr) {
|
||||
@@ -464,6 +500,17 @@ 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);
|
||||
}
|
||||
|
||||
struct LineMeta {
|
||||
int nodeIdx = -1;
|
||||
@@ -528,7 +575,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;
|
||||
@@ -571,6 +618,7 @@ 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 {};
|
||||
@@ -650,8 +698,11 @@ 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++;
|
||||
// Scan to next " · " separator (or end of line) to support formulas with spaces
|
||||
int nextSep = lineText.indexOf(QStringLiteral(" \u00B7"), start);
|
||||
int end = (nextSep >= 0) ? nextSep : lineText.size();
|
||||
// Trim trailing whitespace
|
||||
while (end > start && lineText[end - 1].isSpace()) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
}
|
||||
@@ -820,17 +871,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 +892,12 @@ 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);
|
||||
} // 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
|
||||
|
||||
167
src/editor.cpp
167
src/editor.cpp
@@ -488,8 +488,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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -785,6 +787,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 +816,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,9 +843,12 @@ 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();
|
||||
}
|
||||
|
||||
@@ -903,7 +920,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 +1075,33 @@ 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;
|
||||
int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1;
|
||||
uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
|
||||
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) {
|
||||
// Plain nodeId selection shouldn't highlight individual array elements
|
||||
// (the header line is enough)
|
||||
continue;
|
||||
}
|
||||
m_sci->markerAdd(ln, M_SELECTED);
|
||||
m_sci->markerAdd(ln, M_ACCENT);
|
||||
if (!isFooter)
|
||||
paintEditableSpans(i);
|
||||
paintEditableSpans(ln);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1082,28 +1114,63 @@ 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));
|
||||
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 and array elements 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);
|
||||
|
||||
// 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
|
||||
checkId = m_hoveredNodeId;
|
||||
if (m_currentSelIds.contains(checkId)) return;
|
||||
|
||||
if (hoveringFooter) {
|
||||
// Footer: only highlight this specific line
|
||||
if (hoveringFooter || hoveringArrayElem) {
|
||||
// Single-line highlight for footers and array elements
|
||||
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 +1195,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 {
|
||||
@@ -1515,7 +1587,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:
|
||||
@@ -1744,8 +1817,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 +1859,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);
|
||||
}
|
||||
@@ -2368,8 +2433,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 +2524,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 +2538,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 +2678,16 @@ 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
|
||||
checkId = lm->nodeId;
|
||||
return m_currentSelIds.contains(checkId);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QWidget>
|
||||
#include <QSet>
|
||||
#include <QPoint>
|
||||
#include <QHash>
|
||||
|
||||
class QsciScintilla;
|
||||
class QsciLexerCPP;
|
||||
@@ -69,7 +70,8 @@ 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);
|
||||
@@ -94,8 +96,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
|
||||
|
||||
353
src/examples/EPROCESS.rcx
Normal file
353
src/examples/EPROCESS.rcx
Normal file
@@ -0,0 +1,353 @@
|
||||
{
|
||||
"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":"Hex8","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
|
||||
}
|
||||
]
|
||||
}
|
||||
121
src/format.cpp
121
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
|
||||
@@ -139,8 +148,8 @@ 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"
|
||||
// Full type string: "struct TypeName", "union TypeName", "class TypeName", etc.
|
||||
QString base = node.resolvedClassKeyword();
|
||||
if (!node.structTypeName.isEmpty())
|
||||
return base + QStringLiteral(" ") + node.structTypeName;
|
||||
return base;
|
||||
@@ -148,11 +157,16 @@ QString structTypeName(const Node& node) {
|
||||
|
||||
// ── 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 +176,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 +188,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 +288,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);
|
||||
@@ -341,12 +386,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 +424,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 +696,18 @@ 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;
|
||||
|
||||
while (pos < s.size()) {
|
||||
// Skip whitespace
|
||||
while (pos < s.size() && s[pos].isSpace()) pos++;
|
||||
if (pos >= s.size()) break;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
return {};
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace rcx::fmt
|
||||
|
||||
@@ -315,7 +315,8 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
&& !ctx.forwardDeclared.contains(child.refId)) {
|
||||
QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]);
|
||||
QString fwdKw = ctx.tree.nodes[refIdx].resolvedClassKeyword();
|
||||
if (fwdKw == QStringLiteral("enum")) fwdKw = QStringLiteral("struct");
|
||||
if (fwdKw == QStringLiteral("enum") && ctx.tree.nodes[refIdx].enumMembers.isEmpty())
|
||||
fwdKw = QStringLiteral("struct");
|
||||
ctx.output += QStringLiteral("%1 %2;\n").arg(fwdKw, fwdName);
|
||||
ctx.forwardDeclared.insert(child.refId);
|
||||
}
|
||||
@@ -327,7 +328,21 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
||||
|
||||
QString kw = node.resolvedClassKeyword();
|
||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic
|
||||
|
||||
// Enum with members: emit as proper C enum
|
||||
if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
|
||||
ctx.output += QStringLiteral("enum %1 {\n").arg(typeName);
|
||||
for (const auto& m : node.enumMembers) {
|
||||
ctx.output += QStringLiteral(" %1 = %2,\n")
|
||||
.arg(sanitizeIdent(m.first))
|
||||
.arg(m.second);
|
||||
}
|
||||
ctx.output += QStringLiteral("};\n\n");
|
||||
ctx.visiting.remove(structId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum without members: fallback
|
||||
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
|
||||
|
||||
emitStructBody(ctx, structId);
|
||||
|
||||
1061
src/imports/import_pdb.cpp
Normal file
1061
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
|
||||
@@ -371,7 +371,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;
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "import_source.h"
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
#include <QRegularExpression>
|
||||
#include <QDebug>
|
||||
@@ -285,13 +286,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 +382,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 +467,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 +498,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 +771,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,6 +894,305 @@ static void emitHexPadding(NodeTree& tree, uint64_t parentId, int offset, int si
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bitfield grouping: emit a single hex node covering consecutive bitfields ──
|
||||
|
||||
static void emitBitfieldGroup(NodeTree& tree, uint64_t parentId, int offset, int totalBits) {
|
||||
int bytes = (totalBits + 7) / 8;
|
||||
// Round up to nearest power-of-2 hex node
|
||||
NodeKind hexKind;
|
||||
if (bytes <= 1) hexKind = NodeKind::Hex8;
|
||||
else if (bytes <= 2) hexKind = NodeKind::Hex16;
|
||||
else if (bytes <= 4) hexKind = NodeKind::Hex32;
|
||||
else hexKind = NodeKind::Hex64;
|
||||
Node n;
|
||||
n.kind = hexKind;
|
||||
n.parentId = parentId;
|
||||
n.offset = offset;
|
||||
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)
|
||||
};
|
||||
|
||||
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 single hex node
|
||||
if (field.bitfieldWidth >= 0) {
|
||||
int groupOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
groupOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
groupOffset = computedOffset;
|
||||
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, totalBits);
|
||||
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 = NodeKind::Pointer64;
|
||||
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 + 8;
|
||||
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) {
|
||||
@@ -775,7 +1210,7 @@ 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 {};
|
||||
}
|
||||
|
||||
@@ -798,13 +1233,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};
|
||||
|
||||
// Build nodes for each struct/enum
|
||||
for (const auto& ps : parser.structs) {
|
||||
Node structNode;
|
||||
structNode.kind = NodeKind::Struct;
|
||||
@@ -815,222 +1256,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 +1296,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();
|
||||
}
|
||||
}
|
||||
|
||||
909
src/main.cpp
909
src/main.cpp
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,8 @@
|
||||
#include <QTreeView>
|
||||
#include <QStandardItemModel>
|
||||
#include <QMap>
|
||||
#include <QButtonGroup>
|
||||
#include <QPushButton>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
|
||||
namespace rcx {
|
||||
@@ -51,6 +53,7 @@ private slots:
|
||||
void exportReclassXmlAction();
|
||||
void importFromSource();
|
||||
void importReclassXml();
|
||||
void importPdb();
|
||||
void showTypeAliasesDialog();
|
||||
void editTheme();
|
||||
void showOptionsDialog();
|
||||
@@ -67,11 +70,15 @@ private:
|
||||
|
||||
QMdiArea* m_mdiArea;
|
||||
QLabel* m_statusLabel;
|
||||
QButtonGroup* m_viewBtnGroup = nullptr;
|
||||
QPushButton* m_btnReclass = nullptr;
|
||||
QPushButton* m_btnRendered = nullptr;
|
||||
TitleBarWidget* m_titleBar = nullptr;
|
||||
QWidget* m_borderOverlay = nullptr;
|
||||
PluginManager m_pluginManager;
|
||||
McpBridge* m_mcp = nullptr;
|
||||
QAction* m_mcpAction = nullptr;
|
||||
QMenu* m_sourceMenu = nullptr;
|
||||
|
||||
struct SplitPane {
|
||||
QTabWidget* tabWidget = nullptr;
|
||||
@@ -89,11 +96,13 @@ private:
|
||||
int activePaneIdx = 0;
|
||||
};
|
||||
QMap<QMdiSubWindow*, TabState> m_tabs;
|
||||
|
||||
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
|
||||
void rebuildAllDocs();
|
||||
|
||||
void createMenus();
|
||||
void createStatusBar();
|
||||
void showPluginsDialog();
|
||||
void populateSourceMenu();
|
||||
QIcon makeIcon(const QString& svgPath);
|
||||
|
||||
RcxController* activeController() const;
|
||||
@@ -111,8 +120,8 @@ 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();
|
||||
@@ -121,6 +130,8 @@ private:
|
||||
QDockWidget* m_workspaceDock = nullptr;
|
||||
QTreeView* m_workspaceTree = nullptr;
|
||||
QStandardItemModel* m_workspaceModel = nullptr;
|
||||
QLabel* m_dockTitleLabel = nullptr;
|
||||
QToolButton* m_dockCloseBtn = nullptr;
|
||||
void createWorkspaceDock();
|
||||
void rebuildWorkspaceModel();
|
||||
void updateBorderColor(const QColor& color);
|
||||
|
||||
@@ -92,7 +92,8 @@ bool PluginManager::LoadPlugin(const QString& path)
|
||||
IProviderPlugin* provider = static_cast<IProviderPlugin*>(plugin);
|
||||
QString name = QString::fromStdString(plugin->Name());
|
||||
QString identifier = name.toLower().replace(" ", "");
|
||||
ProviderRegistry::instance().registerProvider(name, identifier, provider);
|
||||
QString dllFileName = QFileInfo(path).fileName();
|
||||
ProviderRegistry::instance().registerProvider(name, identifier, provider, dllFileName);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -6,7 +6,8 @@ ProviderRegistry& ProviderRegistry::instance() {
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin) {
|
||||
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier,
|
||||
IProviderPlugin* plugin, const QString& dllFileName) {
|
||||
// Check if already registered
|
||||
for (const auto& info : m_providers) {
|
||||
if (info.identifier == identifier) {
|
||||
@@ -14,8 +15,8 @@ void ProviderRegistry::registerProvider(const QString& name, const QString& iden
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_providers.append(ProviderInfo(name, identifier, plugin));
|
||||
|
||||
m_providers.append(ProviderInfo(name, identifier, plugin, dllFileName));
|
||||
qDebug() << "ProviderRegistry: Registered plugin provider:" << name << "(" << identifier << ")";
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,13 @@ public:
|
||||
IProviderPlugin* plugin; // Plugin (if plugin-based)
|
||||
BuiltinFactory factory; // Factory (if built-in)
|
||||
bool isBuiltin;
|
||||
|
||||
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p)
|
||||
: name(n), identifier(id), plugin(p), factory(nullptr), isBuiltin(false) {}
|
||||
|
||||
QString dllFileName; // Original DLL/SO filename (plugin-based only)
|
||||
|
||||
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p,
|
||||
const QString& dll = {})
|
||||
: name(n), identifier(id), plugin(p), factory(nullptr),
|
||||
isBuiltin(false), dllFileName(dll) {}
|
||||
|
||||
ProviderInfo(const QString& n, const QString& id, BuiltinFactory f)
|
||||
: name(n), identifier(id), plugin(nullptr), factory(f), isBuiltin(true) {}
|
||||
};
|
||||
@@ -36,7 +39,8 @@ public:
|
||||
static ProviderRegistry& instance();
|
||||
|
||||
// Register a plugin-based provider
|
||||
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin);
|
||||
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin,
|
||||
const QString& dllFileName = {});
|
||||
|
||||
// Register a built-in provider with a factory function
|
||||
void registerBuiltinProvider(const QString& name, const QString& identifier, BuiltinFactory factory);
|
||||
|
||||
@@ -47,6 +47,13 @@ public:
|
||||
return {};
|
||||
}
|
||||
|
||||
// Resolve a module/symbol name to its address (reverse of getSymbol).
|
||||
// Returns 0 if the name is not found.
|
||||
virtual uint64_t symbolToAddress(const QString& name) const {
|
||||
Q_UNUSED(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Derived convenience (non-virtual, never override) ---
|
||||
|
||||
bool isValid() const { return size() > 0; }
|
||||
|
||||
@@ -67,6 +67,9 @@ public:
|
||||
QString getSymbol(uint64_t addr) const override {
|
||||
return m_real ? m_real->getSymbol(addr) : QString();
|
||||
}
|
||||
uint64_t symbolToAddress(const QString& n) const override {
|
||||
return m_real ? m_real->symbolToAddress(n) : 0;
|
||||
}
|
||||
|
||||
bool write(uint64_t addr, const void* buf, int len) override {
|
||||
if (!m_real) return false;
|
||||
|
||||
@@ -51,5 +51,9 @@
|
||||
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
|
||||
<file alias="folder.svg">vsicons/folder.svg</file>
|
||||
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
|
||||
<file alias="server-process.svg">vsicons/server-process.svg</file>
|
||||
<file alias="remote.svg">vsicons/remote.svg</file>
|
||||
<file alias="plug.svg">vsicons/plug.svg</file>
|
||||
<file alias="clear-all.svg">vsicons/clear-all.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -76,14 +76,16 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(theme.textDim.name()));
|
||||
|
||||
// Menu bar styling — transparent background, themed text
|
||||
m_menuBar->setStyleSheet(
|
||||
QStringLiteral(
|
||||
"QMenuBar { background: transparent; border: none; }"
|
||||
"QMenuBar::item { background: transparent; color: %1; padding: 8px 8px 4px 8px; }"
|
||||
"QMenuBar::item:selected { background: %2; }"
|
||||
"QMenuBar::item:pressed { background: %2; }")
|
||||
.arg(theme.textDim.name(), theme.hover.name()));
|
||||
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
|
||||
// Set Window + Button to background so Fusion never paints a foreign color.
|
||||
{
|
||||
QPalette mbPal = m_menuBar->palette();
|
||||
mbPal.setColor(QPalette::Window, theme.background);
|
||||
mbPal.setColor(QPalette::Button, theme.background);
|
||||
mbPal.setColor(QPalette::ButtonText, theme.textDim);
|
||||
m_menuBar->setPalette(mbPal);
|
||||
m_menuBar->setAutoFillBackground(false);
|
||||
}
|
||||
|
||||
// Chrome buttons
|
||||
QString btnStyle = QStringLiteral(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -32,7 +32,8 @@ TypeSpec parseTypeSpec(const QString& text) {
|
||||
if (s.endsWith('*')) {
|
||||
spec.isPointer = true;
|
||||
s.chop(1);
|
||||
if (s.endsWith('*')) s.chop(1); // double pointer
|
||||
spec.ptrDepth = 1;
|
||||
if (s.endsWith('*')) { s.chop(1); spec.ptrDepth = 2; }
|
||||
spec.baseName = s.trimmed();
|
||||
return spec;
|
||||
}
|
||||
@@ -347,7 +348,6 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
m_arrayCountEdit->selectAll();
|
||||
}
|
||||
updateModifierPreview();
|
||||
applyFilter(m_filterEdit->text());
|
||||
});
|
||||
connect(m_arrayCountEdit, &QLineEdit::textChanged,
|
||||
this, [this]() { updateModifierPreview(); });
|
||||
@@ -516,22 +516,32 @@ void TypeSelectorPopup::setTitle(const QString& title) {
|
||||
|
||||
void TypeSelectorPopup::setMode(TypePopupMode mode) {
|
||||
m_mode = mode;
|
||||
// Show modifier toggles for modes where type modifiers make sense
|
||||
bool showMods = (mode == TypePopupMode::FieldType
|
||||
|| mode == TypePopupMode::ArrayElement);
|
||||
m_modRow->setVisible(showMods);
|
||||
// Reset to plain when showing
|
||||
if (showMods) {
|
||||
m_btnPlain->setChecked(true);
|
||||
m_arrayCountEdit->clear();
|
||||
m_arrayCountEdit->hide();
|
||||
}
|
||||
// Always reset to plain — prevents stale state from leaking across modes
|
||||
// (PointerTarget hides buttons but applyFilter still reads their state)
|
||||
m_btnPlain->setChecked(true);
|
||||
m_arrayCountEdit->clear();
|
||||
m_arrayCountEdit->hide();
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setCurrentNodeSize(int bytes) {
|
||||
m_currentNodeSize = bytes;
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setModifier(int modId, int arrayCount) {
|
||||
if (modId == 1) m_btnPtr->setChecked(true);
|
||||
else if (modId == 2) m_btnDblPtr->setChecked(true);
|
||||
else if (modId == 3) {
|
||||
m_btnArray->setChecked(true);
|
||||
m_arrayCountEdit->setText(QString::number(arrayCount));
|
||||
m_arrayCountEdit->show();
|
||||
} else {
|
||||
m_btnPlain->setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntry* current) {
|
||||
m_allTypes = types;
|
||||
if (current) {
|
||||
@@ -541,10 +551,8 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
|
||||
m_currentEntry = TypeEntry{};
|
||||
m_hasCurrent = false;
|
||||
}
|
||||
// Reset modifier toggles
|
||||
m_btnPlain->setChecked(true);
|
||||
m_arrayCountEdit->clear();
|
||||
m_arrayCountEdit->hide();
|
||||
// Don't reset modifier buttons here — setMode() already resets to plain,
|
||||
// and setModifier() may have preselected a button between setMode/setTypes.
|
||||
m_previewLabel->hide();
|
||||
|
||||
m_filterEdit->clear();
|
||||
@@ -630,27 +638,26 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
|
||||
QString filterBase = text.trimmed();
|
||||
|
||||
// Hide primitives when a pointer modifier (* or **) is active
|
||||
int modId = m_modGroup->checkedId();
|
||||
bool hideprimitives = (modId == 1 || modId == 2);
|
||||
|
||||
// Separate primitives and composites
|
||||
// Separate primitives and composites (all types shown regardless of modifier)
|
||||
QVector<TypeEntry> primitives, composites;
|
||||
for (const auto& t : m_allTypes) {
|
||||
if (t.entryKind == TypeEntry::Section) continue; // skip stale sections
|
||||
if (t.entryKind == TypeEntry::Section) continue;
|
||||
bool matchesFilter = filterBase.isEmpty()
|
||||
|| t.displayName.contains(filterBase, Qt::CaseInsensitive)
|
||||
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
|
||||
if (!matchesFilter) continue;
|
||||
|
||||
if (t.entryKind == TypeEntry::Primitive) {
|
||||
if (!hideprimitives)
|
||||
primitives.append(t);
|
||||
} else if (t.entryKind == TypeEntry::Composite)
|
||||
if (t.entryKind == TypeEntry::Primitive)
|
||||
primitives.append(t);
|
||||
else if (t.entryKind == TypeEntry::Composite)
|
||||
composites.append(t);
|
||||
}
|
||||
|
||||
// For non-Root modes, sort primitives: same-size first, then rest
|
||||
auto alphabetical = [](const TypeEntry& a, const TypeEntry& b) {
|
||||
return a.displayName.compare(b.displayName, Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
|
||||
// For non-Root modes, sort primitives: same-size first, then rest — alphabetical within each group
|
||||
if (m_mode != TypePopupMode::Root && m_currentNodeSize > 0 && !primitives.isEmpty()) {
|
||||
QVector<TypeEntry> sameSize, other;
|
||||
for (const auto& p : primitives) {
|
||||
@@ -659,7 +666,11 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
else
|
||||
other.append(p);
|
||||
}
|
||||
std::sort(sameSize.begin(), sameSize.end(), alphabetical);
|
||||
std::sort(other.begin(), other.end(), alphabetical);
|
||||
primitives = sameSize + other;
|
||||
} else {
|
||||
std::sort(primitives.begin(), primitives.end(), alphabetical);
|
||||
}
|
||||
|
||||
// Helper lambdas for appending sections
|
||||
|
||||
@@ -40,6 +40,7 @@ struct TypeEntry {
|
||||
struct TypeSpec {
|
||||
QString baseName;
|
||||
bool isPointer = false;
|
||||
int ptrDepth = 0; // 1 = *, 2 = ** (only meaningful when isPointer)
|
||||
int arrayCount = 0; // 0 = not array
|
||||
};
|
||||
|
||||
@@ -57,6 +58,7 @@ public:
|
||||
void setMode(TypePopupMode mode);
|
||||
void applyTheme(const Theme& theme);
|
||||
void setCurrentNodeSize(int bytes);
|
||||
void setModifier(int modId, int arrayCount = 0);
|
||||
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
|
||||
void popup(const QPoint& globalPos);
|
||||
|
||||
|
||||
82
tests/bench_import_pdb.cpp
Normal file
82
tests/bench_import_pdb.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include "core.h"
|
||||
#include "imports/import_pdb.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class BenchImportPdb : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void benchEnumerateAll();
|
||||
void benchImportAll();
|
||||
};
|
||||
|
||||
static const QString kPdbPath = QStringLiteral(
|
||||
"C:/Symbols/ntkrnlmp.pdb/0762CF42EF7F3E8116EF7329ADAA09A31/ntkrnlmp.pdb");
|
||||
|
||||
void BenchImportPdb::benchEnumerateAll() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
|
||||
qint64 elapsed = timer.elapsed();
|
||||
|
||||
QVERIFY2(!types.isEmpty(), qPrintable(err));
|
||||
qDebug() << "enumeratePdbTypes:" << types.size() << "types in" << elapsed << "ms";
|
||||
}
|
||||
|
||||
void BenchImportPdb::benchImportAll() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
// Phase 1: enumerate
|
||||
QString err;
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
|
||||
qint64 enumerateMs = timer.elapsed();
|
||||
QVERIFY2(!types.isEmpty(), qPrintable(err));
|
||||
|
||||
// Collect all type indices
|
||||
QVector<uint32_t> indices;
|
||||
indices.reserve(types.size());
|
||||
for (const auto& t : types)
|
||||
indices.append(t.typeIndex);
|
||||
|
||||
// Phase 2: import all
|
||||
timer.restart();
|
||||
int lastProgress = 0;
|
||||
NodeTree tree = importPdbSelected(kPdbPath, indices, &err,
|
||||
[&](int cur, int total) -> bool {
|
||||
// Report progress at 25% intervals
|
||||
int pct = (cur * 100) / total;
|
||||
if (pct >= lastProgress + 25) {
|
||||
qDebug() << " progress:" << cur << "/" << total
|
||||
<< "(" << pct << "%)";
|
||||
lastProgress = pct;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
qint64 importMs = timer.elapsed();
|
||||
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
|
||||
// Count root structs
|
||||
int rootCount = 0;
|
||||
for (const auto& n : tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
|
||||
|
||||
qDebug() << "";
|
||||
qDebug() << "=== PDB Import Benchmark (ntkrnlmp.pdb) ===";
|
||||
qDebug() << " Enumerate:" << types.size() << "types in" << enumerateMs << "ms";
|
||||
qDebug() << " Import all:" << rootCount << "root structs,"
|
||||
<< tree.nodes.size() << "total nodes in" << importMs << "ms";
|
||||
qDebug() << " Total:" << (enumerateMs + importMs) << "ms";
|
||||
qDebug() << "============================================";
|
||||
}
|
||||
|
||||
QTEST_MAIN(BenchImportPdb)
|
||||
#include "bench_import_pdb.moc"
|
||||
219
tests/test_addressparser.cpp
Normal file
219
tests/test_addressparser.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
#include "addressparser.h"
|
||||
#include <QTest>
|
||||
|
||||
using rcx::AddressParser;
|
||||
using rcx::AddressParserCallbacks;
|
||||
using rcx::AddressParseResult;
|
||||
|
||||
class TestAddressParser : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
// -- Hex literals --
|
||||
|
||||
void bareHex() { auto r = AddressParser::evaluate("AB"); QVERIFY(r.ok); QCOMPARE(r.value, 0xABULL); }
|
||||
void prefixedHex() { auto r = AddressParser::evaluate("0x1F4"); QVERIFY(r.ok); QCOMPARE(r.value, 0x1F4ULL); }
|
||||
void zeroLiteral() { auto r = AddressParser::evaluate("0"); QVERIFY(r.ok); QCOMPARE(r.value, 0ULL); }
|
||||
void large64bit() { auto r = AddressParser::evaluate("7FF66CCE0000");QVERIFY(r.ok); QCOMPARE(r.value, 0x7FF66CCE0000ULL); }
|
||||
|
||||
// -- Arithmetic --
|
||||
|
||||
void addition() {
|
||||
auto r = AddressParser::evaluate("0x100 + 0x200");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x300ULL);
|
||||
}
|
||||
void subtraction() {
|
||||
auto r = AddressParser::evaluate("0x300 - 0x100");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x200ULL);
|
||||
}
|
||||
void multiplication() {
|
||||
auto r = AddressParser::evaluate("0x10 * 4");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x40ULL);
|
||||
}
|
||||
void division() {
|
||||
auto r = AddressParser::evaluate("0x100 / 2");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x80ULL);
|
||||
}
|
||||
void precedence() {
|
||||
// 0x10 + 2*3 = 0x10 + 6 = 0x16
|
||||
auto r = AddressParser::evaluate("0x10 + 2 * 3");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x16ULL);
|
||||
}
|
||||
void parentheses() {
|
||||
// (0x10 + 2) * 3 = 0x12 * 3 = 0x36
|
||||
auto r = AddressParser::evaluate("(0x10 + 2) * 3");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x36ULL);
|
||||
}
|
||||
|
||||
// -- Unary minus --
|
||||
|
||||
void unaryMinus() {
|
||||
auto r = AddressParser::evaluate("-0x10 + 0x20");
|
||||
QVERIFY(r.ok); QCOMPARE(r.value, 0x10ULL);
|
||||
}
|
||||
|
||||
// -- Module resolution --
|
||||
|
||||
void moduleResolve() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "Program.exe");
|
||||
return *ok ? 0x140000000ULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("<Program.exe> + 0x123", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x140000123ULL);
|
||||
}
|
||||
|
||||
void moduleNotFound() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveModule = [](const QString&, bool* ok) -> uint64_t {
|
||||
*ok = false;
|
||||
return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("<NoSuch.dll>", 8, &cbs);
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("not found"));
|
||||
}
|
||||
|
||||
// -- Dereference --
|
||||
|
||||
void derefSimple() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
|
||||
*ok = (addr == 0x1000);
|
||||
return *ok ? 0xDEADBEEFULL : 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("[0x1000]", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xDEADBEEFULL);
|
||||
}
|
||||
|
||||
void derefNested() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "mod");
|
||||
return *ok ? 0x400000ULL : 0;
|
||||
};
|
||||
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
|
||||
*ok = true;
|
||||
if (addr == 0x400100) return 0x500000;
|
||||
if (addr == 0x900000) return 0xABCDEF;
|
||||
return 0;
|
||||
};
|
||||
// [<mod> + [<mod> + 0x100]] = [0x400000 + [0x400000+0x100]]
|
||||
// inner deref: [0x400100] = 0x500000
|
||||
// outer: [0x400000 + 0x500000] = [0x900000] = 0xABCDEF
|
||||
auto r = AddressParser::evaluate("[<mod> + [<mod> + 0x100]]", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0xABCDEFULL);
|
||||
}
|
||||
|
||||
void derefReadFailure() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.readPointer = [](uint64_t, bool* ok) -> uint64_t {
|
||||
*ok = false;
|
||||
return 0;
|
||||
};
|
||||
auto r = AddressParser::evaluate("[0x1000]", 8, &cbs);
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("failed to read"));
|
||||
}
|
||||
|
||||
// -- Complex expression from plan --
|
||||
|
||||
void complexExpr() {
|
||||
AddressParserCallbacks cbs;
|
||||
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
|
||||
*ok = (name == "Program.exe");
|
||||
return *ok ? 0x140000000ULL : 0;
|
||||
};
|
||||
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
|
||||
*ok = true;
|
||||
if (addr == 0x1400000DEULL) return 0x500000;
|
||||
return 0;
|
||||
};
|
||||
// [<Program.exe> + 0xDE] - AB = [0x1400000DE] - 0xAB = 0x500000 - 0xAB = 0x4FFF55
|
||||
auto r = AddressParser::evaluate("[<Program.exe> + 0xDE] - AB", 8, &cbs);
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x4FFF55ULL);
|
||||
}
|
||||
|
||||
// -- Errors --
|
||||
|
||||
void emptyInput() {
|
||||
auto r = AddressParser::evaluate("");
|
||||
QVERIFY(!r.ok);
|
||||
}
|
||||
void unmatchedBracket() {
|
||||
auto r = AddressParser::evaluate("[0x1000");
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("']'"));
|
||||
}
|
||||
void unmatchedAngle() {
|
||||
auto r = AddressParser::evaluate("<Program.exe");
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("'>'"));
|
||||
}
|
||||
void divisionByZero() {
|
||||
auto r = AddressParser::evaluate("0x100 / 0");
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("division by zero"));
|
||||
}
|
||||
void trailingGarbage() {
|
||||
auto r = AddressParser::evaluate("0x100 xyz");
|
||||
QVERIFY(!r.ok);
|
||||
QVERIFY(r.error.contains("unexpected"));
|
||||
}
|
||||
void trailingOperator() {
|
||||
auto r = AddressParser::evaluate("0x100 +");
|
||||
QVERIFY(!r.ok);
|
||||
}
|
||||
|
||||
// -- Validation --
|
||||
|
||||
void validateValid() {
|
||||
QCOMPARE(AddressParser::validate("0x100 + 0x200"), QString());
|
||||
QCOMPARE(AddressParser::validate("<Prog.exe> + [0x100]"), QString());
|
||||
}
|
||||
void validateInvalid() {
|
||||
QVERIFY(!AddressParser::validate("").isEmpty());
|
||||
QVERIFY(!AddressParser::validate("[0x100").isEmpty());
|
||||
QVERIFY(!AddressParser::validate("0x100 xyz").isEmpty());
|
||||
}
|
||||
|
||||
// -- Backtick stripping --
|
||||
|
||||
void backtickStripping() {
|
||||
auto r = AddressParser::evaluate("7ff6`6cce0000");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x7FF66CCE0000ULL);
|
||||
}
|
||||
|
||||
// -- Whitespace tolerance --
|
||||
|
||||
void whitespace() {
|
||||
auto r = AddressParser::evaluate(" 0x100 + 0x200 ");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x300ULL);
|
||||
}
|
||||
|
||||
// -- Legacy compat: simple hex --
|
||||
|
||||
void simpleHexAddress() {
|
||||
auto r = AddressParser::evaluate("140000000");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x140000000ULL);
|
||||
}
|
||||
|
||||
// -- Multiple additions --
|
||||
|
||||
void multipleAdditions() {
|
||||
auto r = AddressParser::evaluate("0x100 + 0x200 + 0x300");
|
||||
QVERIFY(r.ok);
|
||||
QCOMPARE(r.value, 0x600ULL);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestAddressParser)
|
||||
#include "test_addressparser.moc"
|
||||
@@ -1,185 +0,0 @@
|
||||
/**
|
||||
* test_com_security.cpp — DebugConnect transport diagnostic
|
||||
*
|
||||
* Tests EVERY transport to find what works from MinGW:
|
||||
* 1. TCP to WinDbg .server (port 5055)
|
||||
* 2. Named pipe to WinDbg .server
|
||||
* 3. TCP with various COM security configs
|
||||
* 4. DebugCreate local (baseline)
|
||||
*
|
||||
* SETUP: In WinDbg, run BOTH of these:
|
||||
* .server tcp:port=5055
|
||||
* .server npipe:pipe=reclass
|
||||
*
|
||||
* Then run this test.
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <objbase.h>
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
static void try_connect(const char* label, const char* connStr)
|
||||
{
|
||||
printf(" %-40s → ", label);
|
||||
fflush(stdout);
|
||||
|
||||
IDebugClient* client = nullptr;
|
||||
HRESULT hr = DebugConnect(connStr, IID_IDebugClient, (void**)&client);
|
||||
|
||||
if (SUCCEEDED(hr) && client) {
|
||||
printf("SUCCESS (hr=0x%08lX)\n", (unsigned long)hr);
|
||||
|
||||
// Try to get data spaces and read something
|
||||
IDebugDataSpaces* ds = nullptr;
|
||||
IDebugSymbols* sym = nullptr;
|
||||
IDebugControl* ctrl = nullptr;
|
||||
client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds);
|
||||
client->QueryInterface(IID_IDebugSymbols, (void**)&sym);
|
||||
client->QueryInterface(IID_IDebugControl, (void**)&ctrl);
|
||||
|
||||
if (ctrl) {
|
||||
HRESULT hrWait = ctrl->WaitForEvent(0, 5000);
|
||||
printf(" WaitForEvent: hr=0x%08lX\n", (unsigned long)hrWait);
|
||||
}
|
||||
|
||||
if (sym) {
|
||||
ULONG numMods = 0, numUnloaded = 0;
|
||||
sym->GetNumberModules(&numMods, &numUnloaded);
|
||||
printf(" Modules: %lu loaded\n", numMods);
|
||||
|
||||
if (numMods > 0 && ds) {
|
||||
ULONG64 base = 0;
|
||||
sym->GetModuleByIndex(0, &base);
|
||||
unsigned char buf[2] = {};
|
||||
ULONG got = 0;
|
||||
ds->ReadVirtual(base, buf, 2, &got);
|
||||
printf(" Read at 0x%llX: got=%lu bytes=[%02X %02X]\n",
|
||||
(unsigned long long)base, got, buf[0], buf[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (sym) sym->Release();
|
||||
if (ds) ds->Release();
|
||||
if (ctrl) ctrl->Release();
|
||||
client->Release();
|
||||
} else {
|
||||
char buf[256] = {};
|
||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, (DWORD)hr, 0, buf, sizeof(buf), nullptr);
|
||||
for (char* p = buf + strlen(buf) - 1; p >= buf && (*p == '\r' || *p == '\n'); --p)
|
||||
*p = '\0';
|
||||
printf("FAIL hr=0x%08lX (%s)\n", (unsigned long)hr, buf);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int main()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
char hostname[256] = {};
|
||||
DWORD hsize = sizeof(hostname);
|
||||
GetComputerNameA(hostname, &hsize);
|
||||
|
||||
printf("=== DebugConnect Transport Diagnostic ===\n");
|
||||
printf("Machine: %s\n\n", hostname);
|
||||
|
||||
// ── Baseline: DebugCreate (local) ──
|
||||
printf("[1] DebugCreate (local, no network)\n");
|
||||
{
|
||||
IDebugClient* client = nullptr;
|
||||
HRESULT hr = DebugCreate(IID_IDebugClient, (void**)&client);
|
||||
printf(" DebugCreate: %s (hr=0x%08lX)\n\n",
|
||||
SUCCEEDED(hr) ? "OK" : "FAIL", (unsigned long)hr);
|
||||
if (client) client->Release();
|
||||
}
|
||||
|
||||
// ── TCP variants ──
|
||||
printf("[2] TCP connections (need: .server tcp:port=5055)\n");
|
||||
try_connect("tcp:Port=5055,Server=localhost",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
try_connect("tcp:Port=5055,Server=127.0.0.1",
|
||||
"tcp:Port=5055,Server=127.0.0.1");
|
||||
{
|
||||
char conn[512];
|
||||
snprintf(conn, sizeof(conn), "tcp:Port=5055,Server=%s", hostname);
|
||||
try_connect(conn, conn);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
// ── Named pipe variants ──
|
||||
printf("[3] Named pipe connections (need: .server npipe:pipe=reclass)\n");
|
||||
try_connect("npipe:Pipe=reclass,Server=localhost",
|
||||
"npipe:Pipe=reclass,Server=localhost");
|
||||
{
|
||||
char conn[512];
|
||||
snprintf(conn, sizeof(conn), "npipe:Pipe=reclass,Server=%s", hostname);
|
||||
try_connect(conn, conn);
|
||||
}
|
||||
try_connect("npipe:Pipe=reclass",
|
||||
"npipe:Pipe=reclass");
|
||||
printf("\n");
|
||||
|
||||
// ── TCP with COM security ──
|
||||
printf("[4] TCP with explicit COM init (MTA + IMPERSONATE)\n");
|
||||
{
|
||||
// This runs in-process so CoInitialize affects subsequent calls
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
CoInitializeSecurity(
|
||||
nullptr, -1, nullptr, nullptr,
|
||||
RPC_C_AUTHN_LEVEL_DEFAULT,
|
||||
RPC_C_IMP_LEVEL_IMPERSONATE,
|
||||
nullptr, EOAC_NONE, nullptr);
|
||||
try_connect("tcp:Port=5055,Server=localhost (MTA+SEC)",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
try_connect("npipe:Pipe=reclass (MTA+SEC)",
|
||||
"npipe:Pipe=reclass,Server=localhost");
|
||||
CoUninitialize();
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
// ── Check if dbgeng.dll is the system one ──
|
||||
printf("[5] DbgEng DLL info\n");
|
||||
{
|
||||
HMODULE hmod = GetModuleHandleA("dbgeng.dll");
|
||||
if (hmod) {
|
||||
char path[MAX_PATH] = {};
|
||||
GetModuleFileNameA(hmod, path, MAX_PATH);
|
||||
printf(" dbgeng.dll loaded from: %s\n", path);
|
||||
|
||||
// Get version
|
||||
DWORD verSize = GetFileVersionInfoSizeA(path, nullptr);
|
||||
if (verSize > 0) {
|
||||
auto* verData = (char*)malloc(verSize);
|
||||
if (GetFileVersionInfoA(path, 0, verSize, verData)) {
|
||||
VS_FIXEDFILEINFO* fileInfo = nullptr;
|
||||
UINT len = 0;
|
||||
if (VerQueryValueA(verData, "\\", (void**)&fileInfo, &len)) {
|
||||
printf(" Version: %d.%d.%d.%d\n",
|
||||
HIWORD(fileInfo->dwFileVersionMS),
|
||||
LOWORD(fileInfo->dwFileVersionMS),
|
||||
HIWORD(fileInfo->dwFileVersionLS),
|
||||
LOWORD(fileInfo->dwFileVersionLS));
|
||||
}
|
||||
}
|
||||
free(verData);
|
||||
}
|
||||
} else {
|
||||
printf(" dbgeng.dll not loaded yet\n");
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n=== Done ===\n");
|
||||
return 0;
|
||||
#else
|
||||
printf("Windows only.\n");
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QFile>
|
||||
#include "core.h"
|
||||
|
||||
using namespace rcx;
|
||||
@@ -1984,6 +1986,371 @@ 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 "enum"
|
||||
QVERIFY(result.text.contains("enum"));
|
||||
}
|
||||
|
||||
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
|
||||
QVERIFY(result.text.contains("enum"));
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCompose)
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
#include <QPainter>
|
||||
#include <QCursor>
|
||||
#include <QScreen>
|
||||
#include <QMainWindow>
|
||||
#include <QStatusBar>
|
||||
#include <QPushButton>
|
||||
#include <QButtonGroup>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QScrollBar>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include <Qsci/qsciscintillabase.h>
|
||||
#include "editor.h"
|
||||
@@ -2045,6 +2053,467 @@ private slots:
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: status bar view toggle buttons (pixel-level) ──
|
||||
void testStatusBarViewToggleButtons() {
|
||||
// Mirror the production ViewTabButton from main.cpp
|
||||
static constexpr int kAccentH = 2;
|
||||
static constexpr int kPadLR = 12;
|
||||
static constexpr int kPadBot = 4;
|
||||
class VTB : public QPushButton {
|
||||
public:
|
||||
QColor colBg, colBgChecked, colBgHover, colBgPressed;
|
||||
QColor colText, colTextMuted, colAccent;
|
||||
explicit VTB(const QString& t, QWidget* p = nullptr) : QPushButton(t, p) {
|
||||
setCheckable(true); setFlat(true); setContentsMargins(0,0,0,0);
|
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Ignored);
|
||||
}
|
||||
QSize sizeHint() const override {
|
||||
QFontMetrics fm(font());
|
||||
return QSize(fm.horizontalAdvance(text()) + 2*kPadLR,
|
||||
fm.height() + kAccentH + kPadBot);
|
||||
}
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override {
|
||||
QPainter p(this);
|
||||
QColor bg = colBg;
|
||||
if (isDown()) bg = colBgPressed;
|
||||
else if (underMouse()) bg = colBgHover;
|
||||
else if (isChecked()) bg = colBgChecked;
|
||||
p.fillRect(rect(), bg);
|
||||
if (isChecked())
|
||||
p.fillRect(0, 0, width(), kAccentH, colAccent);
|
||||
p.setPen(isChecked() || underMouse() || isDown() ? colText : colTextMuted);
|
||||
p.setFont(font());
|
||||
QRect tr(kPadLR, kAccentH, width()-2*kPadLR, height()-kAccentH);
|
||||
p.drawText(tr, Qt::AlignVCenter|Qt::AlignLeft, text());
|
||||
}
|
||||
void enterEvent(QEnterEvent*) override { update(); }
|
||||
void leaveEvent(QEvent*) override { update(); }
|
||||
};
|
||||
|
||||
QColor bg(30,30,30), bgAlt(45,45,48), hover(62,62,66);
|
||||
QColor text(212,212,212), textMuted(128,128,128);
|
||||
QColor accent("#b180d7");
|
||||
QColor pressed = hover.darker(130);
|
||||
|
||||
auto setColors = [&](VTB* b) {
|
||||
b->colBg = bg; b->colBgChecked = bgAlt; b->colBgHover = hover;
|
||||
b->colBgPressed = pressed; b->colText = text;
|
||||
b->colTextMuted = textMuted; b->colAccent = accent;
|
||||
};
|
||||
|
||||
// Borderless status bar with manual layout (mirrors production FlatStatusBar)
|
||||
class FSB : public QStatusBar {
|
||||
public:
|
||||
QWidget* tabRow = nullptr;
|
||||
QLabel* label = nullptr;
|
||||
FSB() { setSizeGripEnabled(false); }
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override {
|
||||
QPainter p(this); p.fillRect(rect(), palette().window());
|
||||
}
|
||||
void resizeEvent(QResizeEvent* e) override {
|
||||
QStatusBar::resizeEvent(e);
|
||||
doLayout();
|
||||
}
|
||||
void showEvent(QShowEvent* e) override {
|
||||
QStatusBar::showEvent(e);
|
||||
doLayout();
|
||||
}
|
||||
private:
|
||||
void doLayout() {
|
||||
if (!tabRow || !label) return;
|
||||
int h = height(), tw = tabRow->sizeHint().width();
|
||||
tabRow->setGeometry(0, 0, tw, h);
|
||||
label->setGeometry(tw, 0, width() - tw, h);
|
||||
}
|
||||
};
|
||||
|
||||
QMainWindow win;
|
||||
win.resize(600, 400);
|
||||
QPalette pal; pal.setColor(QPalette::Window, bg);
|
||||
win.setPalette(pal);
|
||||
auto* sb = new FSB;
|
||||
win.setStatusBar(sb);
|
||||
sb->setPalette(pal);
|
||||
sb->setAutoFillBackground(true);
|
||||
if (win.layout()) {
|
||||
win.layout()->setSpacing(0);
|
||||
win.layout()->setContentsMargins(0,0,0,0);
|
||||
}
|
||||
|
||||
auto* btnGroup = new QButtonGroup(&win);
|
||||
btnGroup->setExclusive(true);
|
||||
auto* btnR = new VTB("Reclass");
|
||||
auto* btnC = new VTB("C/C++");
|
||||
setColors(btnR); setColors(btnC);
|
||||
btnR->setChecked(true);
|
||||
btnGroup->addButton(btnR, 0);
|
||||
btnGroup->addButton(btnC, 1);
|
||||
auto* tabRow = new QWidget(sb);
|
||||
auto* tabLay = new QHBoxLayout(tabRow);
|
||||
tabLay->setContentsMargins(0,0,0,0);
|
||||
tabLay->setSpacing(0);
|
||||
tabLay->addWidget(btnR);
|
||||
tabLay->addWidget(btnC);
|
||||
auto* lbl = new QLabel("Ready", sb);
|
||||
lbl->setContentsMargins(10,0,0,0);
|
||||
sb->tabRow = tabRow;
|
||||
sb->label = lbl;
|
||||
|
||||
win.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&win));
|
||||
QTest::qWait(100);
|
||||
|
||||
// ── Toggle logic ──
|
||||
QVERIFY(btnR->isChecked());
|
||||
QVERIFY(!btnC->isChecked());
|
||||
QTest::mouseClick(btnC, Qt::LeftButton);
|
||||
QVERIFY(btnC->isChecked());
|
||||
QVERIFY(!btnR->isChecked());
|
||||
QTest::mouseClick(btnR, Qt::LeftButton);
|
||||
QVERIFY(btnR->isChecked());
|
||||
QTest::qWait(50);
|
||||
|
||||
// ── Pixel: accent line on checked button at rows 0..(kAccentH-1) ──
|
||||
QImage imgR = btnR->grab().toImage().convertToFormat(QImage::Format_ARGB32);
|
||||
QVERIFY(imgR.height() >= kAccentH + 4);
|
||||
|
||||
// Every pixel in the top kAccentH rows (middle 80% width) must be accent
|
||||
int x0 = imgR.width() / 10, x1 = imgR.width() * 9 / 10;
|
||||
for (int y = 0; y < kAccentH; y++) {
|
||||
for (int x = x0; x < x1; x++) {
|
||||
QColor c(imgR.pixel(x, y));
|
||||
QVERIFY2(qAbs(c.red() - accent.red()) < 10
|
||||
&& qAbs(c.green() - accent.green()) < 10
|
||||
&& qAbs(c.blue() - accent.blue()) < 10,
|
||||
qPrintable(QString("Checked btn pixel(%1,%2)=%3 expected accent %4")
|
||||
.arg(x).arg(y).arg(c.name(), accent.name())));
|
||||
}
|
||||
}
|
||||
|
||||
// Mid-height row must NOT be accent (accent doesn't bleed into body)
|
||||
{
|
||||
int midY = imgR.height() / 2;
|
||||
QColor c(imgR.pixel(imgR.width()/2, midY));
|
||||
QVERIFY2(qAbs(c.red() - accent.red()) > 15
|
||||
|| qAbs(c.green() - accent.green()) > 15
|
||||
|| qAbs(c.blue() - accent.blue()) > 15,
|
||||
qPrintable(QString("Row %1 should be background, not accent: %2")
|
||||
.arg(midY).arg(c.name())));
|
||||
}
|
||||
|
||||
// ── Pixel: unchecked button has NO accent line ──
|
||||
QImage imgC = btnC->grab().toImage().convertToFormat(QImage::Format_ARGB32);
|
||||
for (int y = 0; y < kAccentH; y++) {
|
||||
QColor c(imgC.pixel(imgC.width()/2, y));
|
||||
QVERIFY2(qAbs(c.red() - accent.red()) > 15
|
||||
|| qAbs(c.green() - accent.green()) > 15
|
||||
|| qAbs(c.blue() - accent.blue()) > 15,
|
||||
qPrintable(QString("Unchecked btn row %1 has accent: %2")
|
||||
.arg(y).arg(c.name())));
|
||||
}
|
||||
|
||||
// ── Pixel: zero gap between the two buttons ──
|
||||
// Map to their shared parent (the tabRow container)
|
||||
QWidget* container = btnR->parentWidget();
|
||||
int rRight = btnR->mapTo(container, QPoint(btnR->width(), 0)).x();
|
||||
int cLeft = btnC->mapTo(container, QPoint(0, 0)).x();
|
||||
QVERIFY2(rRight == cLeft,
|
||||
qPrintable(QString("Gap between buttons: btnR right=%1 btnC left=%2 gap=%3")
|
||||
.arg(rRight).arg(cLeft).arg(cLeft - rRight)));
|
||||
|
||||
// ── Pressed color is darker than hover ──
|
||||
QVERIFY2(pressed.lightness() < hover.lightness(),
|
||||
qPrintable(QString("Pressed %1 should be darker than hover %2")
|
||||
.arg(pressed.name(), hover.name())));
|
||||
|
||||
// ── Button starts at x=0 in status bar (no left padding) ──
|
||||
QPoint btnTopLeft = tabRow->mapTo(sb, QPoint(0, 0));
|
||||
QVERIFY2(btnTopLeft.x() == 0,
|
||||
qPrintable(QString("Tab row left margin: x=%1, expected 0").arg(btnTopLeft.x())));
|
||||
|
||||
// ── Button starts at y=0 in status bar (no top padding) ──
|
||||
QVERIFY2(btnTopLeft.y() == 0,
|
||||
qPrintable(QString("Tab row top margin: y=%1, expected 0").arg(btnTopLeft.y())));
|
||||
|
||||
// ── Button takes full status bar height ──
|
||||
QVERIFY2(btnR->height() == sb->height(),
|
||||
qPrintable(QString("Button height=%1 sb height=%2")
|
||||
.arg(btnR->height()).arg(sb->height())));
|
||||
|
||||
// ── Accent at y=0 in status bar pixel coordinates (grab status bar) ──
|
||||
QImage sbImg = sb->grab().toImage().convertToFormat(QImage::Format_ARGB32);
|
||||
{
|
||||
QColor c(sbImg.pixel(btnR->width()/2, 0));
|
||||
QVERIFY2(qAbs(c.red() - accent.red()) < 10
|
||||
&& qAbs(c.green() - accent.green()) < 10
|
||||
&& qAbs(c.blue() - accent.blue()) < 10,
|
||||
qPrintable(QString("Status bar pixel(x,%1,0)=%2 expected accent %3")
|
||||
.arg(btnR->width()/2).arg(c.name(), accent.name())));
|
||||
}
|
||||
|
||||
qDebug() << QString("ViewTabButton: accent=%1 btnH=%2 sbH=%3 gap=%4 leftX=%5 topY=%6")
|
||||
.arg(accent.name()).arg(btnR->height()).arg(sb->height())
|
||||
.arg(cLeft - rRight).arg(btnTopLeft.x()).arg(btnTopLeft.y());
|
||||
}
|
||||
|
||||
// ── Test: resize grip dots are equidistant from right and bottom window edges ──
|
||||
// The grip is a direct child of the window positioned via move(), not inside
|
||||
// the status bar layout. This test verifies the dot placement is symmetric
|
||||
// regardless of font, and runs the check at two different font sizes to prove
|
||||
// font independence.
|
||||
// ── Test: horizontal scrollbar after long name rename ──
|
||||
void testHScrollResetAfterNameShrink() {
|
||||
// Use a dedicated narrow editor so content easily overflows the viewport
|
||||
auto* editor = new RcxEditor();
|
||||
editor->resize(200, 300);
|
||||
editor->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(editor));
|
||||
auto* sci = editor->scintilla();
|
||||
auto* hbar = sci->horizontalScrollBar();
|
||||
|
||||
auto makeTree = [](const QString& fieldName) {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.structTypeName = "MyStruct";
|
||||
root.name = "s";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node f;
|
||||
f.kind = NodeKind::Int32;
|
||||
f.name = fieldName;
|
||||
f.parentId = rootId;
|
||||
f.offset = 0;
|
||||
tree.addNode(f);
|
||||
return tree;
|
||||
};
|
||||
|
||||
BufferProvider prov(QByteArray(64, '\0'));
|
||||
|
||||
// ── Step 1: long name → wide content, scrollbar must appear ──
|
||||
QString longName = QString(120, QChar('W'));
|
||||
{
|
||||
NodeTree tree = makeTree(longName);
|
||||
ComposeResult cr = compose(tree, prov);
|
||||
editor->applyDocument(cr);
|
||||
QApplication::processEvents();
|
||||
QTest::qWait(50);
|
||||
}
|
||||
|
||||
int scrollW1 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETSCROLLWIDTH);
|
||||
int viewW = sci->viewport()->width();
|
||||
|
||||
qDebug() << QString("Long name: scrollW=%1 vpW=%2 hbar.visible=%3 "
|
||||
"hbar.max=%4 hbar.value=%5")
|
||||
.arg(scrollW1).arg(viewW)
|
||||
.arg(hbar->isVisible())
|
||||
.arg(hbar->maximum()).arg(hbar->value());
|
||||
|
||||
QVERIFY2(scrollW1 > viewW,
|
||||
qPrintable(QString("scrollW=%1 should exceed vpW=%2")
|
||||
.arg(scrollW1).arg(viewW)));
|
||||
|
||||
// Scrollbar must be visible when content overflows
|
||||
QVERIFY2(hbar->isVisible(),
|
||||
"Horizontal scrollbar should be visible when content overflows");
|
||||
QVERIFY2(hbar->maximum() > 0,
|
||||
qPrintable(QString("Scrollbar max should be >0, got %1")
|
||||
.arg(hbar->maximum())));
|
||||
|
||||
// Simulate user scrolled right
|
||||
sci->SendScintilla(QsciScintillaBase::SCI_SETXOFFSET, (unsigned long)(scrollW1 / 2));
|
||||
QApplication::processEvents();
|
||||
QTest::qWait(20);
|
||||
int xOff1 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETXOFFSET);
|
||||
QVERIFY2(xOff1 > 0, "X offset should be non-zero after scrolling right");
|
||||
|
||||
// ── Step 2: short name → narrower content ──
|
||||
{
|
||||
NodeTree tree = makeTree("x");
|
||||
ComposeResult cr = compose(tree, prov);
|
||||
editor->applyDocument(cr);
|
||||
QApplication::processEvents();
|
||||
QTest::qWait(50);
|
||||
}
|
||||
|
||||
int scrollW2 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETSCROLLWIDTH);
|
||||
int xOff2 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETXOFFSET);
|
||||
|
||||
qDebug() << QString("Short name: scrollW=%1 xOff=%2 vpW=%3 hbar.visible=%4 "
|
||||
"hbar.max=%5 hbar.value=%6")
|
||||
.arg(scrollW2).arg(xOff2).arg(viewW)
|
||||
.arg(hbar->isVisible())
|
||||
.arg(hbar->maximum()).arg(hbar->value());
|
||||
|
||||
// Scroll width should have shrunk
|
||||
QVERIFY2(scrollW2 < scrollW1,
|
||||
qPrintable(QString("scrollW should shrink: was %1, now %2")
|
||||
.arg(scrollW1).arg(scrollW2)));
|
||||
|
||||
// X offset must be clamped to max(0, scrollW - viewportW)
|
||||
int maxValidXOff = qMax(0, scrollW2 - viewW);
|
||||
QVERIFY2(xOff2 <= maxValidXOff,
|
||||
qPrintable(QString("xOffset=%1 exceeds max valid=%2 (scrollW=%3 vpW=%4)")
|
||||
.arg(xOff2).arg(maxValidXOff).arg(scrollW2).arg(viewW)));
|
||||
|
||||
// If content fits viewport entirely, offset must be 0
|
||||
if (scrollW2 <= viewW) {
|
||||
QCOMPARE(xOff2, 0);
|
||||
}
|
||||
|
||||
// If content still overflows, scrollbar must still be visible
|
||||
if (scrollW2 > viewW) {
|
||||
QVERIFY2(hbar->isVisible(),
|
||||
"Scrollbar should remain visible when content still overflows");
|
||||
}
|
||||
|
||||
// ── Step 3: apply long name again → scrollbar must reappear ──
|
||||
{
|
||||
NodeTree tree = makeTree(longName);
|
||||
ComposeResult cr = compose(tree, prov);
|
||||
editor->applyDocument(cr);
|
||||
QApplication::processEvents();
|
||||
QTest::qWait(50);
|
||||
}
|
||||
|
||||
int scrollW3 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETSCROLLWIDTH);
|
||||
int xOff3 = (int)sci->SendScintilla(QsciScintillaBase::SCI_GETXOFFSET);
|
||||
|
||||
qDebug() << QString("Long again: scrollW=%1 xOff=%2 hbar.visible=%3 hbar.max=%4")
|
||||
.arg(scrollW3).arg(xOff3)
|
||||
.arg(hbar->isVisible()).arg(hbar->maximum());
|
||||
|
||||
QVERIFY2(scrollW3 > viewW,
|
||||
qPrintable(QString("scrollW=%1 should exceed vpW=%2 after re-widen")
|
||||
.arg(scrollW3).arg(viewW)));
|
||||
QVERIFY2(hbar->isVisible(),
|
||||
"Scrollbar must reappear after content widens again");
|
||||
// After fresh apply with no prior scroll, xOffset should be 0
|
||||
QCOMPARE(xOff3, 0);
|
||||
|
||||
delete editor;
|
||||
}
|
||||
|
||||
void testResizeGripCornerSymmetry() {
|
||||
// Same constants as production ResizeGrip in main.cpp
|
||||
static constexpr int kSize = 16;
|
||||
static constexpr int kPad = 4;
|
||||
static constexpr double kInset = 4.0;
|
||||
|
||||
class Grip : public QWidget {
|
||||
public:
|
||||
explicit Grip(QWidget* p) : QWidget(p) { setFixedSize(kSize, kSize); }
|
||||
void reposition() {
|
||||
if (auto* w = parentWidget())
|
||||
move(w->width() - kSize - kPad, w->height() - kSize - kPad);
|
||||
}
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override {
|
||||
QPainter p(this);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(Qt::red);
|
||||
const double r = 1.0, s = 4.0;
|
||||
double bx = width() - kInset;
|
||||
double by = height() - kInset;
|
||||
p.drawEllipse(QPointF(bx, by), r, r);
|
||||
p.drawEllipse(QPointF(bx - s, by), r, r);
|
||||
p.drawEllipse(QPointF(bx - 2 * s, by), r, r);
|
||||
p.drawEllipse(QPointF(bx, by - s), r, r);
|
||||
p.drawEllipse(QPointF(bx - s, by - s), r, r);
|
||||
p.drawEllipse(QPointF(bx, by - 2 * s), r, r);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper: grab window, find bottommost-rightmost red pixel, measure gaps
|
||||
auto measureGaps = [](QWidget* win, int& gapRight, int& gapBottom) -> bool {
|
||||
QPixmap px = win->grab();
|
||||
QImage img = px.toImage().convertToFormat(QImage::Format_ARGB32);
|
||||
int W = img.width(), H = img.height();
|
||||
if (W < 50 || H < 50) return false;
|
||||
|
||||
int foundX = -1, foundY = -1;
|
||||
for (int y = H - 1; y >= H - 40 && foundY < 0; --y) {
|
||||
for (int x = W - 1; x >= W - 40; --x) {
|
||||
QColor c(img.pixel(x, y));
|
||||
if (c.red() > 180 && c.green() < 80 && c.blue() < 80) {
|
||||
foundX = x; foundY = y; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (foundX < 0) return false;
|
||||
gapRight = (W - 1) - foundX;
|
||||
gapBottom = (H - 1) - foundY;
|
||||
|
||||
// Save diagnostic image
|
||||
QImage diag = img.copy();
|
||||
QPainter dp(&diag);
|
||||
dp.setPen(QPen(Qt::cyan, 1));
|
||||
dp.drawRect(foundX - 3, foundY - 3, 6, 6);
|
||||
dp.setPen(QPen(Qt::yellow, 1));
|
||||
dp.drawLine(foundX, foundY, W - 1, foundY);
|
||||
dp.drawLine(foundX, foundY, foundX, H - 1);
|
||||
dp.end();
|
||||
diag.save("grip_corner_diag.png");
|
||||
return true;
|
||||
};
|
||||
|
||||
// --- Round 1: default system font ---
|
||||
QMainWindow win;
|
||||
win.resize(500, 375);
|
||||
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, QColor(30, 30, 30));
|
||||
win.setPalette(pal);
|
||||
win.statusBar()->setPalette(pal);
|
||||
win.statusBar()->setAutoFillBackground(true);
|
||||
|
||||
auto* grip = new Grip(&win);
|
||||
grip->raise();
|
||||
|
||||
win.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&win));
|
||||
grip->reposition();
|
||||
QTest::qWait(100);
|
||||
|
||||
int gapR1 = 0, gapB1 = 0;
|
||||
QVERIFY2(measureGaps(&win, gapR1, gapB1),
|
||||
"Could not find red grip dot (round 1)");
|
||||
QVERIFY2(gapR1 == gapB1,
|
||||
qPrintable(QString("Round 1 asymmetric: gapRight=%1 gapBottom=%2")
|
||||
.arg(gapR1).arg(gapB1)));
|
||||
|
||||
// --- Round 2: large font on status bar (must NOT change grip position) ---
|
||||
QFont bigFont("Arial", 24);
|
||||
win.statusBar()->setFont(bigFont);
|
||||
QTest::qWait(100);
|
||||
grip->reposition();
|
||||
QTest::qWait(100);
|
||||
|
||||
int gapR2 = 0, gapB2 = 0;
|
||||
QVERIFY2(measureGaps(&win, gapR2, gapB2),
|
||||
"Could not find red grip dot (round 2, big font)");
|
||||
QVERIFY2(gapR2 == gapB2,
|
||||
qPrintable(QString("Round 2 asymmetric: gapRight=%1 gapBottom=%2")
|
||||
.arg(gapR2).arg(gapB2)));
|
||||
|
||||
// Gaps must be identical across both font sizes
|
||||
QVERIFY2(gapR1 == gapR2 && gapB1 == gapB2,
|
||||
qPrintable(QString("Font changed grip position: "
|
||||
"round1=(%1,%2) round2=(%3,%4)")
|
||||
.arg(gapR1).arg(gapB1).arg(gapR2).arg(gapB2)));
|
||||
|
||||
qDebug() << "Grip corner symmetry:"
|
||||
<< QString("gapRight=%1 gapBottom=%2 (font-independent)")
|
||||
.arg(gapR1).arg(gapB1);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestEditor)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include <QTemporaryFile>
|
||||
#include "core.h"
|
||||
#include "export_reclass_xml.h"
|
||||
#include "import_reclass_xml.h"
|
||||
#include "imports/export_reclass_xml.h"
|
||||
#include "imports/import_reclass_xml.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
|
||||
237
tests/test_import_pdb.cpp
Normal file
237
tests/test_import_pdb.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include "core.h"
|
||||
#include "imports/import_pdb.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestImportPdb : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void missingFileReturnsError();
|
||||
void importKProcess();
|
||||
void verifyDispatcherHeader();
|
||||
void verifyListEntry();
|
||||
void importFilteredStruct();
|
||||
void enumerateTypes();
|
||||
void importSelected();
|
||||
};
|
||||
|
||||
static const QString kPdbPath = QStringLiteral(
|
||||
"C:/Symbols/ntkrnlmp.pdb/0762CF42EF7F3E8116EF7329ADAA09A31/ntkrnlmp.pdb");
|
||||
|
||||
// Find a root struct by structTypeName
|
||||
static int findRootStruct(const NodeTree& tree, const QString& name) {
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].parentId == 0 &&
|
||||
tree.nodes[i].kind == NodeKind::Struct &&
|
||||
tree.nodes[i].structTypeName == name)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Find a child of parentId by name
|
||||
static int findChildNode(const NodeTree& tree, uint64_t parentId, const QString& name) {
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].parentId == parentId && tree.nodes[i].name == name)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TestImportPdb::missingFileReturnsError() {
|
||||
QString err;
|
||||
NodeTree tree = importPdb(QStringLiteral("C:/nonexistent.pdb"), {}, &err);
|
||||
QVERIFY(tree.nodes.isEmpty());
|
||||
QVERIFY(!err.isEmpty());
|
||||
}
|
||||
|
||||
void TestImportPdb::importKProcess() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
|
||||
// Find _KPROCESS root struct
|
||||
int kpIdx = findRootStruct(tree, QStringLiteral("_KPROCESS"));
|
||||
QVERIFY2(kpIdx >= 0, "Expected _KPROCESS root struct");
|
||||
uint64_t kpId = tree.nodes[kpIdx].id;
|
||||
|
||||
// Verify Header field at offset 0 → embedded _DISPATCHER_HEADER
|
||||
int headerIdx = findChildNode(tree, kpId, QStringLiteral("Header"));
|
||||
QVERIFY2(headerIdx >= 0, "Expected 'Header' child of _KPROCESS");
|
||||
QCOMPARE(tree.nodes[headerIdx].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[headerIdx].structTypeName, QStringLiteral("_DISPATCHER_HEADER"));
|
||||
QCOMPARE(tree.nodes[headerIdx].offset, 0);
|
||||
|
||||
// Verify ProfileListHead at offset 0x18 → embedded _LIST_ENTRY
|
||||
int profileIdx = findChildNode(tree, kpId, QStringLiteral("ProfileListHead"));
|
||||
QVERIFY2(profileIdx >= 0, "Expected 'ProfileListHead' child of _KPROCESS");
|
||||
QCOMPARE(tree.nodes[profileIdx].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[profileIdx].structTypeName, QStringLiteral("_LIST_ENTRY"));
|
||||
QCOMPARE(tree.nodes[profileIdx].offset, 0x18);
|
||||
}
|
||||
|
||||
void TestImportPdb::verifyDispatcherHeader() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
|
||||
// _DISPATCHER_HEADER should be imported as a transitive dependency
|
||||
int dhIdx = findRootStruct(tree, QStringLiteral("_DISPATCHER_HEADER"));
|
||||
QVERIFY2(dhIdx >= 0, "_DISPATCHER_HEADER should be imported as a dependency");
|
||||
|
||||
uint64_t dhId = tree.nodes[dhIdx].id;
|
||||
auto kids = tree.childrenOf(dhId);
|
||||
QVERIFY2(!kids.isEmpty(), "_DISPATCHER_HEADER should have children (fields)");
|
||||
|
||||
// Look for WaitListHead — a _LIST_ENTRY at offset 0x10 in most builds
|
||||
int waitIdx = findChildNode(tree, dhId, QStringLiteral("WaitListHead"));
|
||||
QVERIFY2(waitIdx >= 0, "Expected 'WaitListHead' in _DISPATCHER_HEADER");
|
||||
QCOMPARE(tree.nodes[waitIdx].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[waitIdx].structTypeName, QStringLiteral("_LIST_ENTRY"));
|
||||
}
|
||||
|
||||
void TestImportPdb::verifyListEntry() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
|
||||
// _LIST_ENTRY should be imported (used by ProfileListHead and others)
|
||||
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
|
||||
QVERIFY2(leIdx >= 0, "_LIST_ENTRY should be imported");
|
||||
|
||||
uint64_t leId = tree.nodes[leIdx].id;
|
||||
|
||||
// Flink at offset 0 — pointer to _LIST_ENTRY
|
||||
int flinkIdx = findChildNode(tree, leId, QStringLiteral("Flink"));
|
||||
QVERIFY2(flinkIdx >= 0, "Expected 'Flink' in _LIST_ENTRY");
|
||||
QCOMPARE(tree.nodes[flinkIdx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[flinkIdx].offset, 0);
|
||||
|
||||
// Blink at offset 8 — pointer to _LIST_ENTRY
|
||||
int blinkIdx = findChildNode(tree, leId, QStringLiteral("Blink"));
|
||||
QVERIFY2(blinkIdx >= 0, "Expected 'Blink' in _LIST_ENTRY");
|
||||
QCOMPARE(tree.nodes[blinkIdx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[blinkIdx].offset, 8);
|
||||
|
||||
// Both should point back to _LIST_ENTRY (self-referencing)
|
||||
QCOMPARE(tree.nodes[flinkIdx].refId, leId);
|
||||
QCOMPARE(tree.nodes[blinkIdx].refId, leId);
|
||||
}
|
||||
|
||||
void TestImportPdb::importFilteredStruct() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_LIST_ENTRY"), &err);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
|
||||
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
|
||||
QVERIFY(leIdx >= 0);
|
||||
|
||||
// _LIST_ENTRY only references itself, so exactly 1 root struct
|
||||
int rootCount = 0;
|
||||
for (const auto& n : tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
|
||||
QCOMPARE(rootCount, 1);
|
||||
}
|
||||
|
||||
void TestImportPdb::enumerateTypes() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
QString err;
|
||||
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
|
||||
QVERIFY2(!types.isEmpty(), qPrintable(err));
|
||||
|
||||
// Should have hundreds of types in ntkrnlmp
|
||||
QVERIFY2(types.size() > 100,
|
||||
qPrintable(QStringLiteral("Expected >100 types, got %1").arg(types.size())));
|
||||
|
||||
// Verify _KPROCESS is present
|
||||
bool foundKProcess = false;
|
||||
bool foundListEntry = false;
|
||||
for (const auto& t : types) {
|
||||
if (t.name == QStringLiteral("_KPROCESS")) {
|
||||
foundKProcess = true;
|
||||
QVERIFY2(t.childCount > 0, "_KPROCESS should have children");
|
||||
QVERIFY2(t.size > 0, "_KPROCESS should have non-zero size");
|
||||
}
|
||||
if (t.name == QStringLiteral("_LIST_ENTRY")) {
|
||||
foundListEntry = true;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundKProcess, "_KPROCESS not found in enumerated types");
|
||||
QVERIFY2(foundListEntry, "_LIST_ENTRY not found in enumerated types");
|
||||
}
|
||||
|
||||
void TestImportPdb::importSelected() {
|
||||
if (!QFile::exists(kPdbPath))
|
||||
QSKIP("ntkrnlmp.pdb not found at expected path");
|
||||
|
||||
// First enumerate to find _LIST_ENTRY's type index
|
||||
QString err;
|
||||
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
|
||||
QVERIFY2(!types.isEmpty(), qPrintable(err));
|
||||
|
||||
uint32_t listEntryIdx = 0;
|
||||
bool found = false;
|
||||
for (const auto& t : types) {
|
||||
if (t.name == QStringLiteral("_LIST_ENTRY")) {
|
||||
listEntryIdx = t.typeIndex;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(found, "_LIST_ENTRY not found in enumeration");
|
||||
|
||||
// Import just _LIST_ENTRY
|
||||
QVector<uint32_t> indices = { listEntryIdx };
|
||||
int progressCalls = 0;
|
||||
NodeTree tree = importPdbSelected(kPdbPath, indices, &err,
|
||||
[&](int cur, int total) -> bool {
|
||||
progressCalls++;
|
||||
Q_UNUSED(total);
|
||||
Q_ASSERT(cur <= total);
|
||||
return true; // don't cancel
|
||||
});
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
|
||||
QVERIFY(progressCalls > 0);
|
||||
|
||||
// Verify _LIST_ENTRY root struct
|
||||
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
|
||||
QVERIFY2(leIdx >= 0, "_LIST_ENTRY should be imported");
|
||||
|
||||
// Flink and Blink
|
||||
uint64_t leId = tree.nodes[leIdx].id;
|
||||
int flinkIdx = findChildNode(tree, leId, QStringLiteral("Flink"));
|
||||
QVERIFY2(flinkIdx >= 0, "Expected 'Flink' in _LIST_ENTRY");
|
||||
QCOMPARE(tree.nodes[flinkIdx].kind, NodeKind::Pointer64);
|
||||
|
||||
int blinkIdx = findChildNode(tree, leId, QStringLiteral("Blink"));
|
||||
QVERIFY2(blinkIdx >= 0, "Expected 'Blink' in _LIST_ENTRY");
|
||||
QCOMPARE(tree.nodes[blinkIdx].kind, NodeKind::Pointer64);
|
||||
|
||||
// Self-referencing pointers
|
||||
QCOMPARE(tree.nodes[flinkIdx].refId, leId);
|
||||
QCOMPARE(tree.nodes[blinkIdx].refId, leId);
|
||||
|
||||
// Only 1 root struct
|
||||
int rootCount = 0;
|
||||
for (const auto& n : tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
|
||||
QCOMPARE(rootCount, 1);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestImportPdb)
|
||||
#include "test_import_pdb.moc"
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include "core.h"
|
||||
#include "import_source.h"
|
||||
#include "imports/import_source.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
@@ -49,7 +49,9 @@ private slots:
|
||||
void forwardDeclaration();
|
||||
|
||||
// Union handling
|
||||
void unionPickFirst();
|
||||
void unionContainer();
|
||||
void unionWithCommentOffsets();
|
||||
void namedUnion();
|
||||
|
||||
// Padding fields
|
||||
void paddingFieldExpansion();
|
||||
@@ -69,11 +71,19 @@ private slots:
|
||||
|
||||
// Edge cases
|
||||
void bitfieldSkipped();
|
||||
void bitfieldWithOffsetsEmitsHex();
|
||||
void hexArraySizes();
|
||||
void windowsStylePEB();
|
||||
void classKeyword();
|
||||
void inheritanceSkipped();
|
||||
|
||||
// Enum tests
|
||||
void enumBasic();
|
||||
void enumAutoValues();
|
||||
void enumHexValues();
|
||||
void enumInStruct();
|
||||
void enumClass();
|
||||
|
||||
// Round-trip test (requires generator.h)
|
||||
void basicRoundTrip();
|
||||
};
|
||||
@@ -575,7 +585,7 @@ void TestImportSource::forwardDeclaration() {
|
||||
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||
}
|
||||
|
||||
void TestImportSource::unionPickFirst() {
|
||||
void TestImportSource::unionContainer() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct WithUnion {\n"
|
||||
" union {\n"
|
||||
@@ -586,12 +596,85 @@ void TestImportSource::unionPickFirst() {
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// Should have 2 fields: asFloat (first union member) + after
|
||||
// Should have 2 direct children: union container + after
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("asFloat"));
|
||||
|
||||
// First child is the union container
|
||||
const auto& unionNode = tree.nodes[kids[0]];
|
||||
QCOMPARE(unionNode.kind, NodeKind::Struct);
|
||||
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
|
||||
QCOMPARE(unionNode.offset, 0);
|
||||
|
||||
// Union has 2 children, both at offset 0
|
||||
auto unionKids = childrenOf(tree, unionNode.id);
|
||||
QCOMPARE(unionKids.size(), 2);
|
||||
QCOMPARE(tree.nodes[unionKids[0]].kind, NodeKind::Float);
|
||||
QCOMPARE(tree.nodes[unionKids[0]].name, QStringLiteral("asFloat"));
|
||||
QCOMPARE(tree.nodes[unionKids[0]].offset, 0);
|
||||
QCOMPARE(tree.nodes[unionKids[1]].kind, NodeKind::UInt32);
|
||||
QCOMPARE(tree.nodes[unionKids[1]].name, QStringLiteral("asInt"));
|
||||
QCOMPARE(tree.nodes[unionKids[1]].offset, 0);
|
||||
|
||||
// structSpan of union = max member size = 4
|
||||
QCOMPARE(tree.structSpan(unionNode.id), 4);
|
||||
|
||||
// after field follows the union at offset 4
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
|
||||
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
}
|
||||
|
||||
void TestImportSource::unionWithCommentOffsets() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct S {\n"
|
||||
" uint64_t a; // 0x0\n"
|
||||
" union {\n"
|
||||
" uint32_t x; // 0x8\n"
|
||||
" float y; // 0x8\n"
|
||||
" };\n"
|
||||
" uint32_t b; // 0xC\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 3); // a + union + b
|
||||
|
||||
// Union at offset 0x8
|
||||
const auto& unionNode = tree.nodes[kids[1]];
|
||||
QCOMPARE(unionNode.kind, NodeKind::Struct);
|
||||
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
|
||||
QCOMPARE(unionNode.offset, 0x8);
|
||||
|
||||
// Union members at offset 0 (relative to union)
|
||||
auto unionKids = childrenOf(tree, unionNode.id);
|
||||
QCOMPARE(unionKids.size(), 2);
|
||||
QCOMPARE(tree.nodes[unionKids[0]].offset, 0);
|
||||
QCOMPARE(tree.nodes[unionKids[1]].offset, 0);
|
||||
|
||||
// b at 0xC
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 0xC);
|
||||
}
|
||||
|
||||
void TestImportSource::namedUnion() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct S {\n"
|
||||
" union {\n"
|
||||
" uint16_t shortVal;\n"
|
||||
" uint64_t longVal;\n"
|
||||
" } u3;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
|
||||
const auto& unionNode = tree.nodes[kids[0]];
|
||||
QCOMPARE(unionNode.kind, NodeKind::Struct);
|
||||
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
|
||||
QCOMPARE(unionNode.name, QStringLiteral("u3"));
|
||||
|
||||
auto unionKids = childrenOf(tree, unionNode.id);
|
||||
QCOMPARE(unionKids.size(), 2);
|
||||
// structSpan = max(2, 8) = 8
|
||||
QCOMPARE(tree.structSpan(unionNode.id), 8);
|
||||
}
|
||||
|
||||
void TestImportSource::paddingFieldExpansion() {
|
||||
@@ -697,6 +780,7 @@ void TestImportSource::structPrefixOnType() {
|
||||
}
|
||||
|
||||
void TestImportSource::bitfieldSkipped() {
|
||||
// Bitfields emit a hex placeholder covering the group
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct BF {\n"
|
||||
" uint32_t normal;\n"
|
||||
@@ -706,10 +790,38 @@ void TestImportSource::bitfieldSkipped() {
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// Bitfields should be skipped, only normal + after
|
||||
QCOMPARE(kids.size(), 2);
|
||||
// normal + Hex16 (16 bits → 2 bytes) + after
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex16);
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 6);
|
||||
}
|
||||
|
||||
void TestImportSource::bitfieldWithOffsetsEmitsHex() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct BF2 {\n"
|
||||
" uint32_t normal; // 0x0\n"
|
||||
" ULONGLONG Valid : 1; // 0x4\n"
|
||||
" ULONGLONG Dirty : 1; // 0x4\n"
|
||||
" ULONGLONG PageFrameNumber : 36; // 0x4\n"
|
||||
" ULONGLONG Reserved : 26; // 0x4\n"
|
||||
" uint32_t after; // 0xC\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// normal + hex64 (bitfield group: 64 bits) + after = 3
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
// Bitfield group emitted as Hex64 at offset 4
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex64);
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
// after at 0xC
|
||||
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 0xC);
|
||||
}
|
||||
|
||||
void TestImportSource::hexArraySizes() {
|
||||
@@ -842,5 +954,78 @@ void TestImportSource::basicRoundTrip() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Enum tests ──
|
||||
|
||||
void TestImportSource::enumBasic() {
|
||||
auto tree = importFromSource(QStringLiteral(
|
||||
"enum Color { Red = 0, Green = 1, Blue = 2 };"));
|
||||
QCOMPARE(countRoots(tree), 1);
|
||||
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("enum"));
|
||||
QCOMPARE(tree.nodes[0].structTypeName, QStringLiteral("Color"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers.size(), 3);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].first, QStringLiteral("Red"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0LL);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[1].first, QStringLiteral("Green"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers[1].second, 1LL);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[2].first, QStringLiteral("Blue"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers[2].second, 2LL);
|
||||
}
|
||||
|
||||
void TestImportSource::enumAutoValues() {
|
||||
auto tree = importFromSource(QStringLiteral(
|
||||
"enum Flags { A, B, C };"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers.size(), 3);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0LL);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[1].second, 1LL);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[2].second, 2LL);
|
||||
}
|
||||
|
||||
void TestImportSource::enumHexValues() {
|
||||
auto tree = importFromSource(QStringLiteral(
|
||||
"enum { X = 0x10, Y = 0x20 };"));
|
||||
// Anonymous enum has no name — parser skips it (unnamed enums are not added)
|
||||
// Actually, let's use a named enum with hex values
|
||||
tree = importFromSource(QStringLiteral(
|
||||
"enum Hex { X = 0x10, Y = 0x20 };"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers.size(), 2);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0x10LL);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[1].second, 0x20LL);
|
||||
}
|
||||
|
||||
void TestImportSource::enumInStruct() {
|
||||
auto tree = importFromSource(QStringLiteral(
|
||||
"enum PoolType { NonPaged = 0, Paged = 1 };\n"
|
||||
"struct Foo {\n"
|
||||
" PoolType pool; //0x0\n"
|
||||
" uint32_t size; //0x4\n"
|
||||
"};"));
|
||||
// Should have 2 roots: PoolType enum + Foo struct
|
||||
QCOMPARE(countRoots(tree), 2);
|
||||
|
||||
// Find Foo struct
|
||||
int fooIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == QStringLiteral("Foo")) { fooIdx = i; break; }
|
||||
}
|
||||
QVERIFY(fooIdx >= 0);
|
||||
auto kids = childrenOf(tree, tree.nodes[fooIdx].id);
|
||||
QCOMPARE(kids.size(), 2);
|
||||
// First child should be UInt32 (enum mapped to int) with refId to PoolType
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("pool"));
|
||||
QVERIFY(tree.nodes[kids[0]].refId != 0); // linked to enum definition
|
||||
}
|
||||
|
||||
void TestImportSource::enumClass() {
|
||||
auto tree = importFromSource(QStringLiteral(
|
||||
"enum class Scope : uint8_t { A = 1, B = 2 };"));
|
||||
QCOMPARE(countRoots(tree), 1);
|
||||
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("enum"));
|
||||
QCOMPARE(tree.nodes[0].structTypeName, QStringLiteral("Scope"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers.size(), 2);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].first, QStringLiteral("A"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].second, 1LL);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestImportSource)
|
||||
#include "test_import_source.moc"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include "core.h"
|
||||
#include "import_reclass_xml.h"
|
||||
#include "imports/import_reclass_xml.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
|
||||
246
tests/test_source_management.cpp
Normal file
246
tests/test_source_management.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QApplication>
|
||||
#include <QSplitter>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include "controller.h"
|
||||
#include "core.h"
|
||||
#include "providers/null_provider.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
static void buildTree(NodeTree& tree) {
|
||||
tree.baseAddress = 0x1000;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.structTypeName = "TestClass";
|
||||
root.name = "TestClass";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node f;
|
||||
f.kind = NodeKind::Hex64;
|
||||
f.name = "field_00";
|
||||
f.parentId = rootId;
|
||||
f.offset = 0;
|
||||
tree.addNode(f);
|
||||
}
|
||||
|
||||
class TestSourceManagement : public QObject {
|
||||
Q_OBJECT
|
||||
private:
|
||||
RcxDocument* m_doc = nullptr;
|
||||
RcxController* m_ctrl = nullptr;
|
||||
QSplitter* m_splitter = nullptr;
|
||||
|
||||
// Helper: write a temp binary file and return its path
|
||||
QString writeTempFile(const QString& name, const QByteArray& data) {
|
||||
QString path = QDir::tempPath() + "/" + name;
|
||||
QFile f(path);
|
||||
f.open(QIODevice::WriteOnly);
|
||||
f.write(data);
|
||||
f.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
// Helper: directly add a file source entry (bypasses QFileDialog)
|
||||
void addFileSource(const QString& path, const QString& displayName) {
|
||||
m_doc->loadData(path);
|
||||
SavedSourceEntry entry;
|
||||
entry.kind = QStringLiteral("File");
|
||||
entry.displayName = displayName;
|
||||
entry.filePath = path;
|
||||
entry.baseAddress = m_doc->tree.baseAddress;
|
||||
// Access saved sources through selectSource's internal mechanism
|
||||
// We manually add since selectSource("File") opens a dialog
|
||||
m_ctrl->document()->provider = std::make_shared<BufferProvider>(
|
||||
QFile(path).readAll().isEmpty() ? QByteArray(64, '\0') : QByteArray(64, '\0'));
|
||||
// Use the test accessor pattern from controller
|
||||
}
|
||||
|
||||
private slots:
|
||||
void init() {
|
||||
m_doc = new RcxDocument();
|
||||
buildTree(m_doc->tree);
|
||||
|
||||
m_splitter = new QSplitter();
|
||||
m_ctrl = new RcxController(m_doc, nullptr);
|
||||
m_ctrl->addSplitEditor(m_splitter);
|
||||
|
||||
m_splitter->resize(800, 600);
|
||||
m_splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(m_splitter));
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
delete m_ctrl; m_ctrl = nullptr;
|
||||
delete m_splitter; m_splitter = nullptr;
|
||||
delete m_doc; m_doc = nullptr;
|
||||
}
|
||||
|
||||
// ── Initial state: NullProvider, no saved sources ──
|
||||
|
||||
void testInitialProviderIsNull() {
|
||||
QVERIFY(m_doc->provider != nullptr);
|
||||
QCOMPARE(m_doc->provider->size(), 0);
|
||||
QVERIFY(!m_doc->provider->isValid());
|
||||
QCOMPARE(m_ctrl->savedSources().size(), 0);
|
||||
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
|
||||
}
|
||||
|
||||
// ── Loading binary data creates a valid provider ──
|
||||
|
||||
void testLoadDataCreatesValidProvider() {
|
||||
QByteArray data(128, '\xAB');
|
||||
m_doc->loadData(data);
|
||||
QApplication::processEvents();
|
||||
|
||||
QVERIFY(m_doc->provider->isValid());
|
||||
QCOMPARE(m_doc->provider->size(), 128);
|
||||
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0xAB);
|
||||
}
|
||||
|
||||
// ── clearSources resets to NullProvider ──
|
||||
|
||||
void testClearSourcesResetsToNull() {
|
||||
// Load some data first so provider is valid
|
||||
QByteArray data(64, '\xFF');
|
||||
m_doc->loadData(data);
|
||||
QApplication::processEvents();
|
||||
QVERIFY(m_doc->provider->isValid());
|
||||
|
||||
m_ctrl->clearSources();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Provider should be NullProvider
|
||||
QVERIFY(!m_doc->provider->isValid());
|
||||
QCOMPARE(m_doc->provider->size(), 0);
|
||||
|
||||
// Saved sources should be empty
|
||||
QCOMPARE(m_ctrl->savedSources().size(), 0);
|
||||
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
|
||||
}
|
||||
|
||||
// ── clearSources clears value history ──
|
||||
|
||||
void testClearSourcesClearsValueHistory() {
|
||||
// The value history is cleared via resetSnapshot inside clearSources
|
||||
m_ctrl->clearSources();
|
||||
QApplication::processEvents();
|
||||
|
||||
QVERIFY(m_ctrl->valueHistory().isEmpty());
|
||||
}
|
||||
|
||||
// ── clearSources clears dataPath ──
|
||||
|
||||
void testClearSourcesClearsDataPath() {
|
||||
QString path = writeTempFile("rcx_test_src.bin", QByteArray(64, '\xCC'));
|
||||
m_doc->loadData(path);
|
||||
QVERIFY(!m_doc->dataPath.isEmpty());
|
||||
|
||||
m_ctrl->clearSources();
|
||||
QApplication::processEvents();
|
||||
|
||||
QVERIFY(m_doc->dataPath.isEmpty());
|
||||
QFile::remove(path);
|
||||
}
|
||||
|
||||
// ── selectSource("#clear") calls clearSources ──
|
||||
|
||||
void testSelectSourceClearCommand() {
|
||||
QByteArray data(64, '\xFF');
|
||||
m_doc->loadData(data);
|
||||
QVERIFY(m_doc->provider->isValid());
|
||||
|
||||
m_ctrl->selectSource(QStringLiteral("#clear"));
|
||||
QApplication::processEvents();
|
||||
|
||||
QVERIFY(!m_doc->provider->isValid());
|
||||
QCOMPARE(m_ctrl->savedSources().size(), 0);
|
||||
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
|
||||
}
|
||||
|
||||
// ── clearSources then refresh still works (compose doesn't crash) ──
|
||||
|
||||
void testClearSourcesThenRefreshWorks() {
|
||||
m_ctrl->clearSources();
|
||||
QApplication::processEvents();
|
||||
|
||||
// refresh() is called internally by clearSources; verify it didn't crash
|
||||
// and the editor still has content (the tree structure is intact)
|
||||
auto* editor = m_ctrl->editors().first();
|
||||
QVERIFY(editor != nullptr);
|
||||
}
|
||||
|
||||
// ── Multiple clearSources calls are safe (idempotent) ──
|
||||
|
||||
void testMultipleClearSourcesIdempotent() {
|
||||
m_ctrl->clearSources();
|
||||
m_ctrl->clearSources();
|
||||
m_ctrl->clearSources();
|
||||
QApplication::processEvents();
|
||||
|
||||
QVERIFY(!m_doc->provider->isValid());
|
||||
QCOMPARE(m_ctrl->savedSources().size(), 0);
|
||||
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
|
||||
}
|
||||
|
||||
// ── switchToSavedSource with invalid index is no-op ──
|
||||
|
||||
void testSwitchInvalidIndexNoOp() {
|
||||
m_ctrl->switchSource(-1);
|
||||
m_ctrl->switchSource(999);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Should still be in initial state
|
||||
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
|
||||
}
|
||||
|
||||
// ── Provider read fails after clear (all zeros) ──
|
||||
|
||||
void testProviderReadFailsAfterClear() {
|
||||
QByteArray data(64, '\xAB');
|
||||
m_doc->loadData(data);
|
||||
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0xAB);
|
||||
|
||||
m_ctrl->clearSources();
|
||||
QApplication::processEvents();
|
||||
|
||||
// NullProvider: read returns false, readU8 returns 0
|
||||
uint8_t buf = 0xFF;
|
||||
QVERIFY(!m_doc->provider->read(0, &buf, 1));
|
||||
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0);
|
||||
}
|
||||
|
||||
// ── clearSources resets snapshot state ──
|
||||
|
||||
void testClearSourcesResetsSnapshot() {
|
||||
QByteArray data(64, '\x00');
|
||||
m_doc->loadData(data);
|
||||
QApplication::processEvents();
|
||||
|
||||
m_ctrl->clearSources();
|
||||
QApplication::processEvents();
|
||||
|
||||
// After clear, the value history should be empty (resetSnapshot was called)
|
||||
QVERIFY(m_ctrl->valueHistory().isEmpty());
|
||||
}
|
||||
|
||||
// ── NullProvider name is empty (triggers "source" placeholder in command row) ──
|
||||
|
||||
void testNullProviderNameEmpty() {
|
||||
m_ctrl->clearSources();
|
||||
QApplication::processEvents();
|
||||
|
||||
QVERIFY(m_doc->provider->name().isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestSourceManagement)
|
||||
#include "test_source_management.moc"
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QElapsedTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QToolButton>
|
||||
#include <QButtonGroup>
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QStringListModel>
|
||||
@@ -498,6 +499,7 @@ private slots:
|
||||
TypeSpec spec = parseTypeSpec("Ball*");
|
||||
QCOMPARE(spec.baseName, QString("Ball"));
|
||||
QVERIFY(spec.isPointer);
|
||||
QCOMPARE(spec.ptrDepth, 1);
|
||||
QCOMPARE(spec.arrayCount, 0);
|
||||
}
|
||||
|
||||
@@ -505,6 +507,7 @@ private slots:
|
||||
TypeSpec spec = parseTypeSpec("Ball**");
|
||||
QCOMPARE(spec.baseName, QString("Ball"));
|
||||
QVERIFY(spec.isPointer);
|
||||
QCOMPARE(spec.ptrDepth, 2);
|
||||
}
|
||||
|
||||
void testParseTypeSpecEmpty() {
|
||||
@@ -960,6 +963,508 @@ private slots:
|
||||
// Restore
|
||||
tm.setCurrent(origIdx);
|
||||
}
|
||||
|
||||
// ── parseTypeSpec: primitive pointer ptrDepth ──
|
||||
|
||||
void testParseTypeSpecPrimitiveStar() {
|
||||
TypeSpec spec = parseTypeSpec("int32_t*");
|
||||
QCOMPARE(spec.baseName, QString("int32_t"));
|
||||
QVERIFY(spec.isPointer);
|
||||
QCOMPARE(spec.ptrDepth, 1);
|
||||
QCOMPARE(spec.arrayCount, 0);
|
||||
}
|
||||
|
||||
void testParseTypeSpecPrimitiveDoubleStar() {
|
||||
TypeSpec spec = parseTypeSpec("f64**");
|
||||
QCOMPARE(spec.baseName, QString("f64"));
|
||||
QVERIFY(spec.isPointer);
|
||||
QCOMPARE(spec.ptrDepth, 2);
|
||||
QCOMPARE(spec.arrayCount, 0);
|
||||
}
|
||||
|
||||
// ── Primitive pointer creation via applyTypePopupResult path ──
|
||||
|
||||
void testPrimitivePointerCreation() {
|
||||
auto* doc = new RcxDocument();
|
||||
buildTwoRootTree(doc->tree);
|
||||
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(doc, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find the "x" field (Int32) inside Alpha
|
||||
int xIdx = -1;
|
||||
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||
}
|
||||
QVERIFY(xIdx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32);
|
||||
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||
|
||||
// Simulate the primitive-pointer path: Int32 → Pointer64 + elementKind=Int32 + ptrDepth=1
|
||||
doc->undoStack.beginMacro(QStringLiteral("Change to primitive pointer"));
|
||||
ctrl->changeNodeKind(xIdx, NodeKind::Pointer64);
|
||||
int idx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(idx >= 0);
|
||||
doc->tree.nodes[idx].elementKind = NodeKind::Int32;
|
||||
doc->tree.nodes[idx].ptrDepth = 1;
|
||||
doc->undoStack.endMacro();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Verify: Pointer64 with elementKind=Int32, ptrDepth=1, refId=0
|
||||
idx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Int32);
|
||||
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 1);
|
||||
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||
|
||||
// Undo reverses the macro
|
||||
doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
idx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Int32);
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
|
||||
void testDoublePointerCreation() {
|
||||
auto* doc = new RcxDocument();
|
||||
buildTwoRootTree(doc->tree);
|
||||
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(doc, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find the "x" field (Int32) inside Alpha
|
||||
int xIdx = -1;
|
||||
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||
}
|
||||
QVERIFY(xIdx >= 0);
|
||||
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||
|
||||
// Simulate: Int32 → Pointer64 + elementKind=Double + ptrDepth=2
|
||||
doc->undoStack.beginMacro(QStringLiteral("Change to double pointer"));
|
||||
ctrl->changeNodeKind(xIdx, NodeKind::Pointer64);
|
||||
int idx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(idx >= 0);
|
||||
doc->tree.nodes[idx].elementKind = NodeKind::Double;
|
||||
doc->tree.nodes[idx].ptrDepth = 2;
|
||||
doc->undoStack.endMacro();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Verify: Pointer64 with elementKind=Double, ptrDepth=2
|
||||
idx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Double);
|
||||
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 2);
|
||||
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
|
||||
// ── ptrDepth JSON round-trip ──
|
||||
|
||||
void testPtrDepthJsonRoundTrip() {
|
||||
Node n;
|
||||
n.kind = NodeKind::Pointer64;
|
||||
n.name = "pData";
|
||||
n.elementKind = NodeKind::Float;
|
||||
n.ptrDepth = 2;
|
||||
n.id = 42;
|
||||
|
||||
QJsonObject obj = n.toJson();
|
||||
QCOMPARE(obj["ptrDepth"].toInt(), 2);
|
||||
|
||||
Node restored = Node::fromJson(obj);
|
||||
QCOMPARE(restored.ptrDepth, 2);
|
||||
QCOMPARE(restored.elementKind, NodeKind::Float);
|
||||
QCOMPARE(restored.kind, NodeKind::Pointer64);
|
||||
}
|
||||
|
||||
void testPtrDepthJsonDefault() {
|
||||
// Nodes without ptrDepth in JSON should default to 0
|
||||
Node n;
|
||||
n.kind = NodeKind::Pointer64;
|
||||
n.name = "pVoid";
|
||||
n.id = 99;
|
||||
|
||||
QJsonObject obj = n.toJson();
|
||||
// ptrDepth==0 is not serialized
|
||||
QVERIFY(!obj.contains("ptrDepth"));
|
||||
|
||||
Node restored = Node::fromJson(obj);
|
||||
QCOMPARE(restored.ptrDepth, 0);
|
||||
}
|
||||
|
||||
// ── setMode always resets modifier buttons ──
|
||||
|
||||
void testSetModeResetsModifierInPointerTargetMode() {
|
||||
TypeSelectorPopup popup;
|
||||
|
||||
// Set FieldType mode and select * modifier
|
||||
popup.setMode(TypePopupMode::FieldType);
|
||||
popup.setModifier(1); // select *
|
||||
|
||||
// Now switch to PointerTarget mode — should reset to plain
|
||||
popup.setMode(TypePopupMode::PointerTarget);
|
||||
|
||||
// Verify: modifier buttons are hidden but internally reset to plain (modId=0)
|
||||
// This means primitives will be visible in applyFilter
|
||||
TypeEntry prim;
|
||||
prim.entryKind = TypeEntry::Primitive;
|
||||
prim.primitiveKind = NodeKind::Int32;
|
||||
prim.displayName = "int32_t";
|
||||
|
||||
TypeEntry voidEntry;
|
||||
voidEntry.entryKind = TypeEntry::Primitive;
|
||||
voidEntry.primitiveKind = NodeKind::Pointer64;
|
||||
voidEntry.displayName = "void";
|
||||
|
||||
popup.setTypes({prim, voidEntry});
|
||||
|
||||
// Both primitives should be visible (not filtered out)
|
||||
auto* listView = popup.findChild<QListView*>();
|
||||
QVERIFY(listView);
|
||||
int rowCount = listView->model()->rowCount();
|
||||
// Should have section header + 2 primitives = at least 3 rows
|
||||
QVERIFY2(rowCount >= 3,
|
||||
qPrintable(QString("Expected >=3 rows (header+2 prims), got %1").arg(rowCount)));
|
||||
}
|
||||
|
||||
// ── setModifier preselection ──
|
||||
|
||||
void testSetModifierPreselects() {
|
||||
TypeSelectorPopup popup;
|
||||
|
||||
// Test * preselection
|
||||
popup.setMode(TypePopupMode::FieldType);
|
||||
popup.setModifier(1);
|
||||
auto* btnGroup = popup.findChild<QButtonGroup*>();
|
||||
QVERIFY(btnGroup);
|
||||
QCOMPARE(btnGroup->checkedId(), 1);
|
||||
|
||||
// Test ** preselection
|
||||
popup.setMode(TypePopupMode::FieldType);
|
||||
popup.setModifier(2);
|
||||
QCOMPARE(btnGroup->checkedId(), 2);
|
||||
|
||||
// Test [n] preselection with count
|
||||
popup.setMode(TypePopupMode::FieldType);
|
||||
popup.setModifier(3, 8);
|
||||
QCOMPARE(btnGroup->checkedId(), 3);
|
||||
auto* countEdit = popup.findChild<QLineEdit*>(QStringLiteral("arrayCountEdit"));
|
||||
// Array count edit may not have objectName set; find via parent
|
||||
// Just verify button group is correct
|
||||
}
|
||||
|
||||
// ── isValidPrimitivePtrTarget ──
|
||||
|
||||
void testIsValidPrimitivePtrTarget() {
|
||||
// Hex types → NOT valid (deref shows same hex as void*)
|
||||
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex8));
|
||||
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex16));
|
||||
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex32));
|
||||
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex64));
|
||||
|
||||
// Pointer types → NOT valid (use composite * for chains)
|
||||
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Pointer32));
|
||||
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Pointer64));
|
||||
|
||||
// Function pointers → NOT valid
|
||||
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::FuncPtr32));
|
||||
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::FuncPtr64));
|
||||
|
||||
// Containers → NOT valid
|
||||
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Struct));
|
||||
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Array));
|
||||
|
||||
// Value types → valid
|
||||
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Int32));
|
||||
QVERIFY(isValidPrimitivePtrTarget(NodeKind::UInt64));
|
||||
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Float));
|
||||
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Double));
|
||||
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Bool));
|
||||
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Vec3));
|
||||
QVERIFY(isValidPrimitivePtrTarget(NodeKind::UTF8));
|
||||
}
|
||||
|
||||
// ── hex64* falls back to void* ──
|
||||
|
||||
void testHex64StarFallsBackToVoidPointer() {
|
||||
auto* doc = new RcxDocument();
|
||||
buildTwoRootTree(doc->tree);
|
||||
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(doc, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find the "x" field (Int32)
|
||||
int xIdx = -1;
|
||||
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||
}
|
||||
QVERIFY(xIdx >= 0);
|
||||
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||
|
||||
// Build a TypeEntry for hex64
|
||||
TypeEntry hexEntry;
|
||||
hexEntry.entryKind = TypeEntry::Primitive;
|
||||
hexEntry.primitiveKind = NodeKind::Hex64;
|
||||
hexEntry.displayName = "hex64";
|
||||
|
||||
// Apply it with pointer modifier (fullText = "hex64*")
|
||||
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
|
||||
hexEntry, QStringLiteral("hex64*"));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Should be a void pointer: Pointer64, ptrDepth=0, refId=0
|
||||
int idx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
|
||||
void testHex8StarFallsBackToVoidPointer() {
|
||||
auto* doc = new RcxDocument();
|
||||
buildTwoRootTree(doc->tree);
|
||||
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(doc, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
int xIdx = -1;
|
||||
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||
}
|
||||
QVERIFY(xIdx >= 0);
|
||||
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||
|
||||
TypeEntry hexEntry;
|
||||
hexEntry.entryKind = TypeEntry::Primitive;
|
||||
hexEntry.primitiveKind = NodeKind::Hex8;
|
||||
hexEntry.displayName = "hex8";
|
||||
|
||||
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
|
||||
hexEntry, QStringLiteral("hex8*"));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
|
||||
void testPtr64StarFallsBackToVoidPointer() {
|
||||
auto* doc = new RcxDocument();
|
||||
buildTwoRootTree(doc->tree);
|
||||
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(doc, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
int xIdx = -1;
|
||||
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||
}
|
||||
QVERIFY(xIdx >= 0);
|
||||
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||
|
||||
TypeEntry ptrEntry;
|
||||
ptrEntry.entryKind = TypeEntry::Primitive;
|
||||
ptrEntry.primitiveKind = NodeKind::Pointer64;
|
||||
ptrEntry.displayName = "ptr64";
|
||||
|
||||
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
|
||||
ptrEntry, QStringLiteral("ptr64*"));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
|
||||
// ── Valid primitive pointers still work ──
|
||||
|
||||
void testInt32StarStillCreatesPrimitivePointer() {
|
||||
auto* doc = new RcxDocument();
|
||||
buildTwoRootTree(doc->tree);
|
||||
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(doc, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
int xIdx = -1;
|
||||
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||
}
|
||||
QVERIFY(xIdx >= 0);
|
||||
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||
|
||||
TypeEntry intEntry;
|
||||
intEntry.entryKind = TypeEntry::Primitive;
|
||||
intEntry.primitiveKind = NodeKind::Int32;
|
||||
intEntry.displayName = "int32_t";
|
||||
|
||||
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
|
||||
intEntry, QStringLiteral("int32_t*"));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 1);
|
||||
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Int32);
|
||||
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
|
||||
void testDoubleDoubleStarStillCreatesPrimitivePointer() {
|
||||
auto* doc = new RcxDocument();
|
||||
buildTwoRootTree(doc->tree);
|
||||
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(doc, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
int xIdx = -1;
|
||||
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
|
||||
}
|
||||
QVERIFY(xIdx >= 0);
|
||||
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||
|
||||
TypeEntry dblEntry;
|
||||
dblEntry.entryKind = TypeEntry::Primitive;
|
||||
dblEntry.primitiveKind = NodeKind::Double;
|
||||
dblEntry.displayName = "double";
|
||||
|
||||
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
|
||||
dblEntry, QStringLiteral("double**"));
|
||||
QApplication::processEvents();
|
||||
|
||||
int idx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 2);
|
||||
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Double);
|
||||
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
|
||||
// ── Defense: compose/format treat invalid ptrDepth as void* ──
|
||||
|
||||
void testComposeShowsVoidPtrForHexPtrDepth() {
|
||||
// If a node somehow has ptrDepth>0 with hex elementKind
|
||||
// (e.g. from old JSON), compose should show "void*" not "hex64*"
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x1000;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Test";
|
||||
root.structTypeName = "Test";
|
||||
root.parentId = 0;
|
||||
tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
|
||||
Node ptr;
|
||||
ptr.kind = NodeKind::Pointer64;
|
||||
ptr.name = "badPtr";
|
||||
ptr.parentId = rootId;
|
||||
ptr.offset = 0;
|
||||
ptr.ptrDepth = 1;
|
||||
ptr.elementKind = NodeKind::Hex64; // invalid target
|
||||
tree.addNode(ptr);
|
||||
|
||||
QByteArray buf(0x100, '\0');
|
||||
BufferProvider prov(buf);
|
||||
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// The composed text should NOT contain "hex64*" — the invalid target
|
||||
// should fall through to normal void pointer display
|
||||
QVERIFY2(!result.text.contains("hex64*"),
|
||||
qPrintable("Should not show 'hex64*', got: " + result.text));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestTypeSelector)
|
||||
|
||||
332
tests/test_type_visibility.cpp
Normal file
332
tests/test_type_visibility.cpp
Normal file
@@ -0,0 +1,332 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QApplication>
|
||||
#include <QSplitter>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include "controller.h"
|
||||
#include "typeselectorpopup.h"
|
||||
#include "core.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
static QByteArray makeBuffer() { return QByteArray(0x200, '\0'); }
|
||||
|
||||
// Build a tree with one root struct + a Pointer64 field
|
||||
static void buildPointerTree(NodeTree& tree, const QString& rootName) {
|
||||
tree.baseAddress = 0;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "instance";
|
||||
root.structTypeName = rootName;
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node ptr;
|
||||
ptr.kind = NodeKind::Pointer64;
|
||||
ptr.name = "ptr";
|
||||
ptr.parentId = rootId;
|
||||
ptr.offset = 0;
|
||||
tree.addNode(ptr);
|
||||
}
|
||||
|
||||
class TestTypeVisibility : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
|
||||
// ── 1. New types created via createNewTypeRequested get a default name ──
|
||||
|
||||
void testCreateNewTypeGetsDefaultName() {
|
||||
auto* doc = new RcxDocument();
|
||||
buildPointerTree(doc->tree, "Main");
|
||||
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(doc, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
int nodesBefore = doc->tree.nodes.size();
|
||||
|
||||
// Simulate what createNewTypeRequested does: create struct with default name
|
||||
// (The actual handler is a lambda; we test the result via tree inspection)
|
||||
{
|
||||
bool wasSuppressed = ctrl->document() != nullptr; Q_UNUSED(wasSuppressed);
|
||||
|
||||
// Generate unique default name — same logic as the handler
|
||||
QString baseName = QStringLiteral("NewClass");
|
||||
QString typeName = baseName;
|
||||
int counter = 1;
|
||||
QSet<QString> existing;
|
||||
for (const auto& nd : doc->tree.nodes) {
|
||||
if (nd.kind == NodeKind::Struct && !nd.structTypeName.isEmpty())
|
||||
existing.insert(nd.structTypeName);
|
||||
}
|
||||
while (existing.contains(typeName))
|
||||
typeName = baseName + QString::number(counter++);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.structTypeName = typeName;
|
||||
n.name = QStringLiteral("instance");
|
||||
n.parentId = 0;
|
||||
n.offset = 0;
|
||||
n.id = doc->tree.reserveId();
|
||||
doc->undoStack.push(new RcxCommand(ctrl, cmd::Insert{n}));
|
||||
}
|
||||
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Verify new struct was created with a name
|
||||
QCOMPARE(doc->tree.nodes.size(), nodesBefore + 1);
|
||||
bool found = false;
|
||||
for (const auto& n : doc->tree.nodes) {
|
||||
if (n.structTypeName == "NewClass") { found = true; break; }
|
||||
}
|
||||
QVERIFY2(found, "New struct should have structTypeName 'NewClass'");
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
|
||||
// ── 2. Second new type gets incremented name ──
|
||||
|
||||
void testCreateNewTypeIncrementsName() {
|
||||
auto* doc = new RcxDocument();
|
||||
buildPointerTree(doc->tree, "Main");
|
||||
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
// Add a struct already named "NewClass"
|
||||
{
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.structTypeName = "NewClass";
|
||||
n.name = "instance";
|
||||
n.parentId = 0;
|
||||
n.offset = 0;
|
||||
doc->tree.addNode(n);
|
||||
}
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(doc, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Generate name using same logic
|
||||
QString baseName = QStringLiteral("NewClass");
|
||||
QString typeName = baseName;
|
||||
int counter = 1;
|
||||
QSet<QString> existing;
|
||||
for (const auto& nd : doc->tree.nodes) {
|
||||
if (nd.kind == NodeKind::Struct && !nd.structTypeName.isEmpty())
|
||||
existing.insert(nd.structTypeName);
|
||||
}
|
||||
while (existing.contains(typeName))
|
||||
typeName = baseName + QString::number(counter++);
|
||||
|
||||
QCOMPARE(typeName, QStringLiteral("NewClass1"));
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
|
||||
// ── 3. Cross-tab: types from other documents visible via project docs ──
|
||||
|
||||
void testCrossTabTypesVisible() {
|
||||
// Doc A: has "Alpha" struct with a Pointer64 field
|
||||
auto* docA = new RcxDocument();
|
||||
buildPointerTree(docA->tree, "Alpha");
|
||||
docA->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
// Doc B: has "Beta" struct
|
||||
auto* docB = new RcxDocument();
|
||||
buildPointerTree(docB->tree, "Beta");
|
||||
docB->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
// Shared doc list (simulates MainWindow::m_allDocs)
|
||||
QVector<RcxDocument*> allDocs;
|
||||
allDocs << docA << docB;
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(docA, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
ctrl->setProjectDocuments(&allDocs);
|
||||
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find the Pointer64 node in docA
|
||||
int ptrIdx = -1;
|
||||
for (int i = 0; i < docA->tree.nodes.size(); i++) {
|
||||
if (docA->tree.nodes[i].kind == NodeKind::Pointer64) {
|
||||
ptrIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(ptrIdx >= 0);
|
||||
|
||||
// Apply an external type (structId=0, displayName="Beta") as pointer target
|
||||
TypeEntry extEntry;
|
||||
extEntry.entryKind = TypeEntry::Composite;
|
||||
extEntry.structId = 0; // external sentinel
|
||||
extEntry.displayName = QStringLiteral("Beta");
|
||||
ctrl->applyTypePopupResult(TypePopupMode::PointerTarget, ptrIdx,
|
||||
extEntry, QString());
|
||||
QApplication::processEvents();
|
||||
|
||||
// "Beta" should now exist in docA as a local struct (imported)
|
||||
bool found = false;
|
||||
uint64_t betaLocalId = 0;
|
||||
for (const auto& n : docA->tree.nodes) {
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct
|
||||
&& n.structTypeName == "Beta") {
|
||||
found = true;
|
||||
betaLocalId = n.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(found, "Beta struct should be imported into docA");
|
||||
|
||||
// The pointer's refId should point at the local Beta
|
||||
int ptrIdx2 = -1;
|
||||
for (int i = 0; i < docA->tree.nodes.size(); i++) {
|
||||
if (docA->tree.nodes[i].kind == NodeKind::Pointer64
|
||||
&& docA->tree.nodes[i].name == "ptr") {
|
||||
ptrIdx2 = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(ptrIdx2 >= 0);
|
||||
QCOMPARE(docA->tree.nodes[ptrIdx2].refId, betaLocalId);
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete docA;
|
||||
delete docB;
|
||||
}
|
||||
|
||||
// ── 4. findOrCreateStructByName reuses existing local struct ──
|
||||
|
||||
void testFindOrCreateReusesExisting() {
|
||||
auto* doc = new RcxDocument();
|
||||
buildPointerTree(doc->tree, "Main");
|
||||
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
// Add "Target" struct manually
|
||||
Node target;
|
||||
target.kind = NodeKind::Struct;
|
||||
target.structTypeName = "Target";
|
||||
target.name = "instance";
|
||||
target.parentId = 0;
|
||||
target.offset = 0;
|
||||
int ti = doc->tree.addNode(target);
|
||||
uint64_t targetId = doc->tree.nodes[ti].id;
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(doc, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
int nodesBefore = doc->tree.nodes.size();
|
||||
|
||||
// Apply external entry with name "Target" — should reuse existing
|
||||
int ptrIdx = -1;
|
||||
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||
if (doc->tree.nodes[i].kind == NodeKind::Pointer64) {
|
||||
ptrIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(ptrIdx >= 0);
|
||||
|
||||
TypeEntry extEntry;
|
||||
extEntry.entryKind = TypeEntry::Composite;
|
||||
extEntry.structId = 0;
|
||||
extEntry.displayName = QStringLiteral("Target");
|
||||
ctrl->applyTypePopupResult(TypePopupMode::PointerTarget, ptrIdx,
|
||||
extEntry, QString());
|
||||
QApplication::processEvents();
|
||||
|
||||
// Should NOT have created a new struct — reused existing one
|
||||
QCOMPARE(doc->tree.nodes.size(), nodesBefore);
|
||||
|
||||
// Pointer should reference the existing Target
|
||||
int ptrIdx2 = -1;
|
||||
for (int i = 0; i < doc->tree.nodes.size(); i++) {
|
||||
if (doc->tree.nodes[i].kind == NodeKind::Pointer64
|
||||
&& doc->tree.nodes[i].name == "ptr") {
|
||||
ptrIdx2 = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(ptrIdx2 >= 0);
|
||||
QCOMPARE(doc->tree.nodes[ptrIdx2].refId, targetId);
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
|
||||
// ── 5. External types skip duplicates already in local doc ──
|
||||
|
||||
void testExternalTypesSkipLocalDuplicates() {
|
||||
// Both docs have "Shared" type — should not appear twice
|
||||
auto* docA = new RcxDocument();
|
||||
buildPointerTree(docA->tree, "Shared");
|
||||
docA->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
auto* docB = new RcxDocument();
|
||||
buildPointerTree(docB->tree, "Shared");
|
||||
docB->provider = std::make_unique<BufferProvider>(makeBuffer());
|
||||
|
||||
QVector<RcxDocument*> allDocs;
|
||||
allDocs << docA << docB;
|
||||
|
||||
auto* splitter = new QSplitter();
|
||||
auto* ctrl = new RcxController(docA, nullptr);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
ctrl->setProjectDocuments(&allDocs);
|
||||
splitter->resize(800, 600);
|
||||
splitter->show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(splitter));
|
||||
ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Count how many "Shared" entries exist in local doc's root structs
|
||||
int sharedCount = 0;
|
||||
for (const auto& n : docA->tree.nodes) {
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct
|
||||
&& n.structTypeName == "Shared")
|
||||
sharedCount++;
|
||||
}
|
||||
QCOMPARE(sharedCount, 1); // only the local one
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete docA;
|
||||
delete docB;
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestTypeVisibility)
|
||||
#include "test_type_visibility.moc"
|
||||
@@ -483,7 +483,7 @@ private slots:
|
||||
QVERIFY(!fmt::validateBaseAddress("").isEmpty()); // empty
|
||||
QVERIFY(!fmt::validateBaseAddress(" ").isEmpty()); // whitespace only - no hex digits
|
||||
QVERIFY(!fmt::validateBaseAddress("0xGGGG").isEmpty());
|
||||
QVERIFY(!fmt::validateBaseAddress("0x1000 * 2").isEmpty()); // multiplication not supported
|
||||
QVERIFY(fmt::validateBaseAddress("0x1000 * 2").isEmpty()); // multiplication supported
|
||||
QVERIFY(!fmt::validateBaseAddress("0x1000 ++ 0x100").isEmpty()); // double operator
|
||||
QVERIFY(!fmt::validateBaseAddress("hello").isEmpty());
|
||||
}
|
||||
@@ -1028,7 +1028,7 @@ private slots:
|
||||
|
||||
// Test the validation function directly
|
||||
QVERIFY(!fmt::validateBaseAddress("0x1000 ** 2").isEmpty());
|
||||
QVERIFY(!fmt::validateBaseAddress("0x1000 / 2").isEmpty());
|
||||
QVERIFY(fmt::validateBaseAddress("0x1000 / 2").isEmpty()); // division supported
|
||||
QVERIFY(!fmt::validateBaseAddress("abc xyz").isEmpty());
|
||||
|
||||
// Original base should be unchanged
|
||||
|
||||
@@ -256,8 +256,9 @@ private slots:
|
||||
{
|
||||
WinDbgMemoryProvider prov(m_connString);
|
||||
QVERIFY(prov.isValid());
|
||||
QVERIFY2(prov.base() != 0, "Should have a non-zero base from first module");
|
||||
qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16);
|
||||
// WinDbg provider no longer auto-selects a module base — it returns 0
|
||||
// so the controller doesn't override the user's chosen base address.
|
||||
QCOMPARE(prov.base(), (uint64_t)0);
|
||||
}
|
||||
|
||||
// ── Read: MZ header on main thread ──
|
||||
@@ -446,6 +447,139 @@ private slots:
|
||||
QCOMPARE(raw->Name(), std::string("WinDbg Memory"));
|
||||
delete raw;
|
||||
}
|
||||
|
||||
// ── Kernel session tests ──
|
||||
// Requires a WinDbg instance with a kernel dump loaded and
|
||||
// .server tcp:port=5055 running. Skipped automatically if
|
||||
// no server is available. Override with WINDBG_KERNEL_CONN env var.
|
||||
|
||||
void provider_kernel_connect()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available (set WINDBG_KERNEL_CONN)");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY2(prov.isValid(), "Should connect to kernel debug server");
|
||||
QCOMPARE(prov.kind(), QStringLiteral("WinDbg"));
|
||||
|
||||
qDebug() << "Kernel provider name:" << prov.name();
|
||||
qDebug() << "Kernel provider base:" << QString("0x%1").arg(prov.base(), 0, 16);
|
||||
qDebug() << "Kernel provider isLive:" << prov.isLive();
|
||||
|
||||
// Name should not be an arbitrary user-mode DLL
|
||||
QVERIFY2(!prov.name().contains("WS2_32", Qt::CaseInsensitive),
|
||||
qPrintable("Name should not be 'WS2_32', got: " + prov.name()));
|
||||
}
|
||||
|
||||
void provider_kernel_read_base()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
// Provider no longer auto-selects a base. Use a known kernel address
|
||||
// from env, or skip.
|
||||
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||
if (addrStr.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
|
||||
|
||||
bool ok = false;
|
||||
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
||||
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
|
||||
|
||||
uint8_t buf[16] = {};
|
||||
ok = prov.read(addr, buf, 16);
|
||||
QVERIFY2(ok, "Should read from kernel address");
|
||||
|
||||
bool allZero = true;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
if (buf[i] != 0) { allZero = false; break; }
|
||||
}
|
||||
QVERIFY2(!allZero, "Kernel read returned all zeros");
|
||||
}
|
||||
|
||||
void provider_kernel_read_high_address()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
// Use env var for a specific kernel address (e.g. _EPROCESS),
|
||||
// otherwise fall back to the provider's base.
|
||||
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||
uint64_t addr = 0;
|
||||
if (!addrStr.isEmpty()) {
|
||||
bool ok = false;
|
||||
addr = addrStr.toULongLong(&ok, 16);
|
||||
if (!ok) addr = 0;
|
||||
}
|
||||
if (addr == 0) addr = prov.base();
|
||||
|
||||
uint8_t buf[64] = {};
|
||||
bool ok = prov.read(addr, buf, 64);
|
||||
QVERIFY2(ok, qPrintable(QString("Should read kernel addr 0x%1")
|
||||
.arg(addr, 0, 16)));
|
||||
|
||||
bool allZero = true;
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
if (buf[i] != 0) { allZero = false; break; }
|
||||
}
|
||||
QVERIFY2(!allZero, "Kernel high-address read returned all zeros");
|
||||
|
||||
qDebug() << "Read 64 bytes at" << QString("0x%1").arg(addr, 0, 16)
|
||||
<< "first 8:" << QString("%1 %2 %3 %4 %5 %6 %7 %8")
|
||||
.arg(buf[0], 2, 16, QChar('0'))
|
||||
.arg(buf[1], 2, 16, QChar('0'))
|
||||
.arg(buf[2], 2, 16, QChar('0'))
|
||||
.arg(buf[3], 2, 16, QChar('0'))
|
||||
.arg(buf[4], 2, 16, QChar('0'))
|
||||
.arg(buf[5], 2, 16, QChar('0'))
|
||||
.arg(buf[6], 2, 16, QChar('0'))
|
||||
.arg(buf[7], 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
void provider_kernel_read_backgroundThread()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available");
|
||||
|
||||
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||
if (addrStr.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
|
||||
|
||||
bool ok = false;
|
||||
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
||||
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
// Simulate the controller's async refresh pattern
|
||||
QFuture<QByteArray> future = QtConcurrent::run([&prov, addr]() -> QByteArray {
|
||||
return prov.readBytes(addr, 4096);
|
||||
});
|
||||
future.waitForFinished();
|
||||
QByteArray data = future.result();
|
||||
|
||||
QCOMPARE(data.size(), 4096);
|
||||
bool allZero = true;
|
||||
for (int i = 0; i < data.size(); ++i) {
|
||||
if (data[i] != '\0') { allZero = false; break; }
|
||||
}
|
||||
QVERIFY2(!allZero, "Kernel background read returned all zeros");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestWinDbgProvider)
|
||||
|
||||
431
third_party/raw_pdb/.gitignore
vendored
Normal file
431
third_party/raw_pdb/.gitignore
vendored
Normal file
@@ -0,0 +1,431 @@
|
||||
# CLion
|
||||
.idea/
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.env
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
|
||||
[Dd]ebug/x64/
|
||||
[Dd]ebugPublic/x64/
|
||||
[Rr]elease/x64/
|
||||
[Rr]eleases/x64/
|
||||
bin/x64/
|
||||
obj/x64/
|
||||
|
||||
[Dd]ebug/x86/
|
||||
[Dd]ebugPublic/x86/
|
||||
[Rr]elease/x86/
|
||||
[Rr]eleases/x86/
|
||||
bin/x86/
|
||||
obj/x86/
|
||||
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
[Aa][Rr][Mm]64[Ee][Cc]/
|
||||
bld/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Build results on 'Bin' directories
|
||||
**/[Bb]in/*
|
||||
# Uncomment if you have tasks that rely on *.refresh files to move binaries
|
||||
# (https://github.com/github/gitignore/pull/3736)
|
||||
#!**/[Bb]in/*.refresh
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
*.trx
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Approval Tests result files
|
||||
*.received.*
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.idb
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
**/.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
**/.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
**/.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
#tools/**
|
||||
#!tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
MSBuild_Logs/
|
||||
|
||||
# AWS SAM Build and Temporary Artifacts folder
|
||||
.aws-sam
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
**/.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
**/.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
**/.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
9
third_party/raw_pdb/CMakeLists.txt
vendored
Normal file
9
third_party/raw_pdb/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(raw_pdb)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
|
||||
|
||||
add_subdirectory(src)
|
||||
25
third_party/raw_pdb/LICENSE
vendored
Normal file
25
third_party/raw_pdb/LICENSE
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
190
third_party/raw_pdb/README.md
vendored
Normal file
190
third_party/raw_pdb/README.md
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
# RawPDB
|
||||
|
||||
**RawPDB** is a C++11 library that directly reads Microsoft Program DataBase PDB files. The code is extracted almost directly from <a href="https://liveplusplus.tech/">Live++ 2</a>, a battle-tested hot-reload tool for C++.
|
||||
|
||||
## Design
|
||||
|
||||
**RawPDB** gives you direct access to the stream data contained in a PDB file. It does not attempt to offer abstractions for iterating symbols, translation units, contributions, etc.
|
||||
|
||||
Building a high-level abstraction over the provided low-level data is an ill-fated attempt that can never really be performant for everybody, because different tools like debuggers, hot-reload tools (e.g. <a href="https://liveplusplus.tech/">Live++</a>), profilers (e.g. <a href="https://superluminal.eu/">Superluminal</a>), need to perform different queries against the stored data.
|
||||
|
||||
We therefore believe the best solution is to offer direct access to the underlying data, with applications bringing that data into their own structures.
|
||||
|
||||
## Goal
|
||||
|
||||
Eventually, we want **RawPDB** to become the de-facto replacement of <a href="https://docs.microsoft.com/en-us/visualstudio/debugger/debug-interface-access/debug-interface-access-sdk">Microsoft's DIA SDK</a> that most C++ developers (have to) use.
|
||||
|
||||
## Features
|
||||
|
||||
* Fast - **RawPDB** works directly with memory-mapped data, so only the data from the streams you touch affect performance. It is orders of magnitudes faster than the DIA SDK, and faster than comparable LLVM code
|
||||
* Scalable - **RawPDB's** API gives you access to individual streams that can all be read concurrently in a trivial fashion, since all returned data structures are immutable. There are no locks or waits anywhere inside the library
|
||||
* Lightweight - **RawPDB** is small and compiles in roughly 1 second
|
||||
* Allocation-friendly - **RawPDB** performs only a few allocations, and those can be overridden easily by changing the underlying macro
|
||||
* No STL - **RawPDB** does not need any STL containers or algorithms
|
||||
* No exceptions - **RawPDB** does not use exceptions
|
||||
* No RTTI - **RawPDB** does not need RTTI or use class hierarchies
|
||||
* High-quality code - **RawPDB** compiles clean under -Wall
|
||||
|
||||
## Building
|
||||
|
||||
The code compiles clean under Visual Studio 2015, 2017, 2019, or 2022. A solution for Visual Studio 2019 is included.
|
||||
|
||||
## Performance
|
||||
|
||||
Running the **Symbols** and **Contributions** examples on a 1GiB PDB yields the following output:
|
||||
|
||||
<pre>
|
||||
Opening PDB file C:\Development\llvm-project\build\tools\clang\unittests\Tooling\RelWithDebInfo\ToolingTests.pdb
|
||||
|
||||
Running example "Symbols"
|
||||
| Reading image section stream
|
||||
| ---> done in 0.066ms
|
||||
| Reading module info stream
|
||||
| ---> done in 0.562ms
|
||||
| Reading symbol record stream
|
||||
| ---> done in 25.185ms
|
||||
| Reading public symbol stream
|
||||
| ---> done in 1.133ms
|
||||
| Storing public symbols
|
||||
| ---> done in 46.171ms (212023 elements)
|
||||
| Reading global symbol stream
|
||||
| ---> done in 1.381ms
|
||||
| Storing global symbols
|
||||
| ---> done in 12.769ms (448957 elements)
|
||||
| Storing symbols from modules
|
||||
| ---> done in 145.849ms (2243 elements)
|
||||
---> done in 233.694ms (539611 elements)
|
||||
</pre>
|
||||
|
||||
<pre>
|
||||
Opening PDB file C:\Development\llvm-project\build\tools\clang\unittests\Tooling\RelWithDebInfo\ToolingTests.pdb
|
||||
|
||||
Running example "Contributions"
|
||||
| Reading image section stream
|
||||
| ---> done in 0.066ms
|
||||
| Reading module info stream
|
||||
| ---> done in 0.594ms
|
||||
| Reading section contribution stream
|
||||
| ---> done in 9.839ms
|
||||
| Storing contributions
|
||||
| ---> done in 67.346ms (630924 elements)
|
||||
| std::sort contributions
|
||||
| ---> done in 19.218ms
|
||||
---> done in 97.283ms
|
||||
20 largest contributions:
|
||||
1: 1896496 bytes from LLVMAMDGPUCodeGen.dir\RelWithDebInfo\AMDGPUInstructionSelector.obj
|
||||
2: 1700720 bytes from LLVMHexagonCodeGen.dir\RelWithDebInfo\HexagonInstrInfo.obj
|
||||
3: 1536470 bytes from LLVMRISCVCodeGen.dir\RelWithDebInfo\RISCVISelDAGToDAG.obj
|
||||
4: 1441408 bytes from LLVMAArch64CodeGen.dir\RelWithDebInfo\AArch64InstructionSelector.obj
|
||||
5: 1187048 bytes from LLVMRISCVCodeGen.dir\RelWithDebInfo\RISCVInstructionSelector.obj
|
||||
6: 1026504 bytes from LLVMARMCodeGen.dir\RelWithDebInfo\ARMInstructionSelector.obj
|
||||
7: 952080 bytes from LLVMAMDGPUDesc.dir\RelWithDebInfo\AMDGPUMCTargetDesc.obj
|
||||
8: 849888 bytes from LLVMX86Desc.dir\RelWithDebInfo\X86MCTargetDesc.obj
|
||||
9: 712176 bytes from LLVMHexagonCodeGen.dir\RelWithDebInfo\HexagonInstrInfo.obj
|
||||
10: 679035 bytes from LLVMX86CodeGen.dir\RelWithDebInfo\X86ISelDAGToDAG.obj
|
||||
11: 525174 bytes from LLVMAMDGPUDesc.dir\RelWithDebInfo\AMDGPUMCTargetDesc.obj
|
||||
12: 523035 bytes from * Linker *
|
||||
13: 519312 bytes from LLVMRISCVDesc.dir\RelWithDebInfo\RISCVMCTargetDesc.obj
|
||||
14: 512496 bytes from LLVMVEDesc.dir\RelWithDebInfo\VEMCTargetDesc.obj
|
||||
15: 498768 bytes from LLVMX86CodeGen.dir\RelWithDebInfo\X86InstructionSelector.obj
|
||||
16: 483528 bytes from LLVMMipsCodeGen.dir\RelWithDebInfo\MipsInstructionSelector.obj
|
||||
17: 449472 bytes from LLVMAMDGPUCodeGen.dir\RelWithDebInfo\AMDGPUISelDAGToDAG.obj
|
||||
18: 444246 bytes from C:\Development\llvm-project\build\tools\clang\lib\Basic\obj.clangBasic.dir\RelWithDebInfo\DiagnosticIDs.obj
|
||||
19: 371584 bytes from LLVMAArch64CodeGen.dir\RelWithDebInfo\AArch64ISelDAGToDAG.obj
|
||||
20: 370272 bytes from LLVMNVPTXDesc.dir\RelWithDebInfo\NVPTXMCTargetDesc.obj
|
||||
</pre>
|
||||
|
||||
This is at least an order of magnitude faster than DIA, even though the example code is completely serial and uses std::vector, std::string, and std::sort, which are used for illustration purposes only.
|
||||
|
||||
When reading streams in a concurrent fashion, you will most likely be limited by the speed at which the OS can bring the data into your process.
|
||||
|
||||
Running the **Lines** example on a 1.37 GiB PDB yields the following output:
|
||||
|
||||
<pre>
|
||||
|
||||
Opening PDB file C:\pdb-test-files\clang-debug.pdb
|
||||
Version 20000404, signature 1658696914, age 1, GUID 563dd8f1-f32b-459b-8c2beae0e70bc19b
|
||||
|
||||
Running example "Lines"
|
||||
| Reading image section stream
|
||||
| ---> done in 0.313ms
|
||||
| Reading module info stream
|
||||
| ---> done in 0.403ms
|
||||
| Reading names stream
|
||||
| ---> done in 0.126ms
|
||||
| Storing lines from modules
|
||||
| ---> done in 306.720ms (1847 elements)
|
||||
| std::sort sections
|
||||
| ---> done in 103.090ms (4023680 elements)
|
||||
|
||||
</pre>
|
||||
|
||||
## Supported streams
|
||||
|
||||
**RawPDB** gives you access to the following PDB stream data:
|
||||
|
||||
* DBI stream data
|
||||
* Public symbols
|
||||
* Global symbols
|
||||
* Modules
|
||||
* Module symbols
|
||||
* Module lines (C13 line information)
|
||||
* Image sections
|
||||
* Info stream
|
||||
* "/names" stream
|
||||
* Section contributions
|
||||
* Source files
|
||||
|
||||
* IPI stream data
|
||||
|
||||
* TPI stream data
|
||||
|
||||
Furthermore, PDBs linked using /DEBUG:FASTLINK are not supported. These PDBs do not contain much information, since private symbol information is distributed among object files and library files.
|
||||
|
||||
## Documentation
|
||||
|
||||
If you are unfamiliar with the basic structure of a PDB file, the <a href="https://llvm.org/docs/PDB/index.html">LLVM documentation</a> serves as a good introduction.
|
||||
|
||||
Consult the example code to see how to read and parse the PDB streams.
|
||||
|
||||
## Directory structure
|
||||
|
||||
* bin: contains final binary output files (.exe and .pdb)
|
||||
* build: contains Visual Studio 2019 solution and project files
|
||||
* lib: contains the RawPDB library output files (.lib and .pdb)
|
||||
* src: contains the RawPDB source code, as well as example code
|
||||
* temp: contains intermediate build artefacts
|
||||
|
||||
## Examples
|
||||
|
||||
### Symbols (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleSymbols.cpp">ExampleSymbols.cpp</a>)
|
||||
|
||||
A basic example that shows how to load symbols from public, global, and module streams.
|
||||
|
||||
### Contributions (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleContributions.cpp">ExampleContributions.cpp</a>)
|
||||
|
||||
A basic example that shows how to load contributions, sort them by size, and output the 20 largest ones along with the object file they originated from.
|
||||
|
||||
### Function symbols (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleFunctionSymbols.cpp">ExampleFunctionSymbols.cpp</a>)
|
||||
|
||||
An example intended for profiler developers that shows how to enumerate all function symbols and retrieve or compute their code size.
|
||||
|
||||
### Function variables (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleFunctionVariables.cpp">ExampleFunctionVariables.cpp</a>)
|
||||
|
||||
An example intended for debugger developers that shows how to enumerate all function records needed for displaying function variables.
|
||||
|
||||
### Lines (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleLines.cpp">ExampleLines.cpp</a>)
|
||||
|
||||
An example that shows to how to load line information for all modules.
|
||||
|
||||
### Types (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExampleTypes.cpp">ExampleTypes.cpp</a>)
|
||||
|
||||
An example that prints all type records.
|
||||
|
||||
### PDBSize (<a href="https://github.com/MolecularMatters/raw_pdb/blob/main/src/Examples/ExamplePDBSize.cpp">ExamplePDBSize.cpp</a>)
|
||||
|
||||
An example that could serve as a starting point for people wanting to investigate and optimize the size of their PDBs.
|
||||
|
||||
## Sponsoring or supporting RawPDB
|
||||
|
||||
We have chosen a very liberal license to let **RawPDB** be used in as many scenarios as possible, including commercial applications. If you would like to support its development, consider licensing <a href="https://liveplusplus.tech/">Live++</a> instead. Not only do you give something back, but get a great productivity enhancement on top!
|
||||
12
third_party/raw_pdb/raw_pdb.natvis
vendored
Normal file
12
third_party/raw_pdb/raw_pdb.natvis
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
|
||||
<Type Name="PDB::ArrayView<*>">
|
||||
<DisplayString>{{ size={m_length} }}</DisplayString>
|
||||
<Expand>
|
||||
<ArrayItems>
|
||||
<Size>m_length</Size>
|
||||
<ValuePointer>m_data</ValuePointer>
|
||||
</ArrayItems>
|
||||
</Expand>
|
||||
</Type>
|
||||
</AutoVisualizer>
|
||||
112
third_party/raw_pdb/src/CMakeLists.txt
vendored
Normal file
112
third_party/raw_pdb/src/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
set(SOURCES
|
||||
Foundation/PDB_ArrayView.h
|
||||
Foundation/PDB_Assert.h
|
||||
Foundation/PDB_BitOperators.h
|
||||
Foundation/PDB_BitUtil.h
|
||||
Foundation/PDB_CRT.h
|
||||
Foundation/PDB_Forward.h
|
||||
Foundation/PDB_Log.h
|
||||
Foundation/PDB_Macros.h
|
||||
Foundation/PDB_Memory.h
|
||||
Foundation/PDB_Move.h
|
||||
Foundation/PDB_Platform.h
|
||||
Foundation/PDB_PointerUtil.h
|
||||
Foundation/PDB_TypeTraits.h
|
||||
Foundation/PDB_Warnings.h
|
||||
|
||||
PDB.cpp
|
||||
PDB.h
|
||||
PDB_CoalescedMSFStream.cpp
|
||||
PDB_CoalescedMSFStream.h
|
||||
PDB_DBIStream.cpp
|
||||
PDB_DBIStream.h
|
||||
PDB_DBITypes.cpp
|
||||
PDB_DBITypes.h
|
||||
PDB_DirectMSFStream.cpp
|
||||
PDB_DirectMSFStream.h
|
||||
PDB_ErrorCodes.h
|
||||
PDB_GlobalSymbolStream.cpp
|
||||
PDB_GlobalSymbolStream.h
|
||||
PDB_ImageSectionStream.cpp
|
||||
PDB_ImageSectionStream.h
|
||||
PDB_InfoStream.cpp
|
||||
PDB_InfoStream.h
|
||||
PDB_IPIStream.cpp
|
||||
PDB_IPIStream.h
|
||||
PDB_IPITypes.h
|
||||
PDB_ModuleInfoStream.cpp
|
||||
PDB_ModuleInfoStream.h
|
||||
PDB_ModuleLineStream.cpp
|
||||
PDB_ModuleLineStream.h
|
||||
PDB_ModuleSymbolStream.cpp
|
||||
PDB_ModuleSymbolStream.h
|
||||
PDB_NamesStream.cpp
|
||||
PDB_NamesStream.h
|
||||
PDB_PCH.cpp
|
||||
PDB_PCH.h
|
||||
PDB_PublicSymbolStream.cpp
|
||||
PDB_PublicSymbolStream.h
|
||||
PDB_RawFile.cpp
|
||||
PDB_RawFile.h
|
||||
PDB_SectionContributionStream.cpp
|
||||
PDB_SectionContributionStream.h
|
||||
PDB_SourceFileStream.cpp
|
||||
PDB_SourceFileStream.h
|
||||
PDB_TPIStream.cpp
|
||||
PDB_TPIStream.h
|
||||
PDB_TPITypes.h
|
||||
PDB_Types.cpp
|
||||
PDB_Types.h
|
||||
PDB_Util.h
|
||||
)
|
||||
|
||||
source_group(src FILES
|
||||
${SOURCES}
|
||||
)
|
||||
|
||||
add_library(raw_pdb
|
||||
${SOURCES}
|
||||
)
|
||||
|
||||
target_include_directories(raw_pdb
|
||||
PUBLIC
|
||||
.
|
||||
)
|
||||
|
||||
target_precompile_headers(raw_pdb
|
||||
PRIVATE
|
||||
PDB_PCH.h
|
||||
)
|
||||
|
||||
option(RAWPDB_BUILD_EXAMPLES "Build Examples" ON)
|
||||
|
||||
if (RAWPDB_BUILD_EXAMPLES)
|
||||
add_subdirectory(Examples)
|
||||
endif()
|
||||
|
||||
if (UNIX)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
install(
|
||||
TARGETS raw_pdb
|
||||
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE HEADER_FILES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/*.h"
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE HEADER_FILES_FOUNDATION
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Foundation/*.h"
|
||||
)
|
||||
|
||||
install(
|
||||
FILES ${HEADER_FILES}
|
||||
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/raw_pdb/"
|
||||
)
|
||||
|
||||
install(
|
||||
FILES ${HEADER_FILES_FOUNDATION}
|
||||
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/raw_pdb/Foundation"
|
||||
)
|
||||
endif (UNIX)
|
||||
39
third_party/raw_pdb/src/Examples/CMakeLists.txt
vendored
Normal file
39
third_party/raw_pdb/src/Examples/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
project(Examples)
|
||||
|
||||
set(SOURCES
|
||||
ExampleContributions.cpp
|
||||
ExampleFunctionSymbols.cpp
|
||||
ExampleFunctionVariables.cpp
|
||||
ExampleIPI.cpp
|
||||
ExampleLines.cpp
|
||||
ExampleMain.cpp
|
||||
ExampleMemoryMappedFile.cpp
|
||||
ExampleMemoryMappedFile.h
|
||||
ExamplePDBSize.cpp
|
||||
Examples_PCH.cpp
|
||||
Examples_PCH.h
|
||||
ExampleSymbols.cpp
|
||||
ExampleTimedScope.cpp
|
||||
ExampleTimedScope.h
|
||||
ExampleTypes.cpp
|
||||
ExampleTypeTable.cpp
|
||||
ExampleTypeTable.h
|
||||
)
|
||||
|
||||
source_group(src FILES
|
||||
${SOURCES}
|
||||
)
|
||||
|
||||
add_executable(Examples
|
||||
${SOURCES}
|
||||
)
|
||||
|
||||
target_link_libraries(Examples
|
||||
PUBLIC
|
||||
raw_pdb
|
||||
)
|
||||
|
||||
target_precompile_headers(Examples
|
||||
PUBLIC
|
||||
Examples_PCH.h
|
||||
)
|
||||
96
third_party/raw_pdb/src/Examples/ExampleContributions.cpp
vendored
Normal file
96
third_party/raw_pdb/src/Examples/ExampleContributions.cpp
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleTimedScope.h"
|
||||
#include "PDB_RawFile.h"
|
||||
#include "PDB_DBIStream.h"
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
// we don't have to store std::string in the contributions, since all the data is memory-mapped anyway.
|
||||
// we do it in this example to ensure that we don't "cheat" when reading the PDB file. memory-mapped data will only
|
||||
// be faulted into the process once it's touched, so actually copying the string data makes us touch the needed data,
|
||||
// giving us a real performance measurement.
|
||||
struct Contribution
|
||||
{
|
||||
std::string objectFile;
|
||||
uint32_t rva;
|
||||
uint32_t size;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
void ExampleContributions(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream);
|
||||
void ExampleContributions(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream)
|
||||
{
|
||||
TimedScope total("\nRunning example \"Contributions\"");
|
||||
|
||||
// in order to keep the example easy to understand, we load the PDB data serially.
|
||||
// note that this can be improved a lot by reading streams concurrently.
|
||||
|
||||
// prepare the image section stream first. it is needed for converting section + offset into an RVA
|
||||
TimedScope sectionScope("Reading image section stream");
|
||||
const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawPdbFile);
|
||||
sectionScope.Done();
|
||||
|
||||
|
||||
// prepare the module info stream for matching contributions against files
|
||||
TimedScope moduleScope("Reading module info stream");
|
||||
const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
|
||||
moduleScope.Done();
|
||||
|
||||
|
||||
// read contribution stream
|
||||
TimedScope contributionScope("Reading section contribution stream");
|
||||
const PDB::SectionContributionStream sectionContributionStream = dbiStream.CreateSectionContributionStream(rawPdbFile);
|
||||
contributionScope.Done();
|
||||
|
||||
std::vector<Contribution> contributions;
|
||||
{
|
||||
TimedScope scope("Storing contributions");
|
||||
|
||||
const PDB::ArrayView<PDB::DBI::SectionContribution> sectionContributions = sectionContributionStream.GetContributions();
|
||||
const size_t count = sectionContributions.GetLength();
|
||||
|
||||
contributions.reserve(count);
|
||||
|
||||
for (const PDB::DBI::SectionContribution& contribution : sectionContributions)
|
||||
{
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(contribution.section, contribution.offset);
|
||||
if (rva == 0u)
|
||||
{
|
||||
printf("Contribution has invalid RVA\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
const PDB::ModuleInfoStream::Module& module = moduleInfoStream.GetModule(contribution.moduleIndex);
|
||||
|
||||
contributions.push_back(Contribution { module.GetName().Decay(), rva, contribution.size });
|
||||
}
|
||||
|
||||
scope.Done(count);
|
||||
}
|
||||
|
||||
TimedScope sortScope("std::sort contributions");
|
||||
std::sort(contributions.begin(), contributions.end(), [](const Contribution& lhs, const Contribution& rhs)
|
||||
{
|
||||
return lhs.size > rhs.size;
|
||||
});
|
||||
sortScope.Done();
|
||||
|
||||
total.Done();
|
||||
|
||||
// log the 20 largest contributions
|
||||
{
|
||||
printf("20 largest contributions:\n");
|
||||
|
||||
const size_t countToShow = std::min<size_t>(20ul, contributions.size());
|
||||
for (size_t i = 0u; i < countToShow; ++i)
|
||||
{
|
||||
const Contribution& contribution = contributions[i];
|
||||
printf("%zu: %u bytes from %s\n", i + 1u, contribution.size, contribution.objectFile.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
262
third_party/raw_pdb/src/Examples/ExampleFunctionSymbols.cpp
vendored
Normal file
262
third_party/raw_pdb/src/Examples/ExampleFunctionSymbols.cpp
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleTimedScope.h"
|
||||
#include "PDB_RawFile.h"
|
||||
#include "PDB_DBIStream.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// in this example, we are only interested in function symbols: function name, RVA, and size.
|
||||
// this is what most profilers need, they aren't interested in any other data.
|
||||
struct FunctionSymbol
|
||||
{
|
||||
std::string name;
|
||||
uint32_t rva;
|
||||
uint32_t size;
|
||||
const PDB::CodeView::DBI::Record* frameProc;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
void ExampleFunctionSymbols(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream);
|
||||
void ExampleFunctionSymbols(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream)
|
||||
{
|
||||
TimedScope total("\nRunning example \"Function symbols\"");
|
||||
|
||||
// in order to keep the example easy to understand, we load the PDB data serially.
|
||||
// note that this can be improved a lot by reading streams concurrently.
|
||||
|
||||
// prepare the image section stream first. it is needed for converting section + offset into an RVA
|
||||
TimedScope sectionScope("Reading image section stream");
|
||||
const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawPdbFile);
|
||||
sectionScope.Done();
|
||||
|
||||
|
||||
// prepare the module info stream for grabbing function symbols from modules
|
||||
TimedScope moduleScope("Reading module info stream");
|
||||
const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
|
||||
moduleScope.Done();
|
||||
|
||||
|
||||
// prepare symbol record stream needed by the public stream
|
||||
TimedScope symbolStreamScope("Reading symbol record stream");
|
||||
const PDB::CoalescedMSFStream symbolRecordStream = dbiStream.CreateSymbolRecordStream(rawPdbFile);
|
||||
symbolStreamScope.Done();
|
||||
|
||||
|
||||
// note that we only use unordered_set in order to keep the example code easy to understand.
|
||||
// using other hash set implementations like e.g. abseil's Swiss Tables (https://abseil.io/about/design/swisstables) is *much* faster.
|
||||
std::vector<FunctionSymbol> functionSymbols;
|
||||
std::unordered_set<uint32_t> seenFunctionRVAs;
|
||||
|
||||
// start by reading the module stream, grabbing every function symbol we can find.
|
||||
// in most cases, this gives us ~90% of all function symbols already, along with their size.
|
||||
{
|
||||
TimedScope scope("Storing function symbols from modules");
|
||||
|
||||
const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = moduleInfoStream.GetModules();
|
||||
|
||||
for (const PDB::ModuleInfoStream::Module& module : modules)
|
||||
{
|
||||
if (!module.HasSymbolStream())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const PDB::ModuleSymbolStream moduleSymbolStream = module.CreateSymbolStream(rawPdbFile);
|
||||
moduleSymbolStream.ForEachSymbol([&functionSymbols, &seenFunctionRVAs, &imageSectionStream](const PDB::CodeView::DBI::Record* record)
|
||||
{
|
||||
// only grab function symbols from the module streams
|
||||
const char* name = nullptr;
|
||||
uint32_t rva = 0u;
|
||||
uint32_t size = 0u;
|
||||
if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_FRAMEPROC)
|
||||
{
|
||||
functionSymbols[functionSymbols.size() - 1].frameProc = record;
|
||||
return;
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_THUNK32)
|
||||
{
|
||||
if (record->data.S_THUNK32.thunk == PDB::CodeView::DBI::ThunkOrdinal::TrampolineIncremental)
|
||||
{
|
||||
// we have never seen incremental linking thunks stored inside a S_THUNK32 symbol, but better safe than sorry
|
||||
name = "ILT";
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_THUNK32.section, record->data.S_THUNK32.offset);
|
||||
size = 5u;
|
||||
}
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_TRAMPOLINE)
|
||||
{
|
||||
// incremental linking thunks are stored in the linker module
|
||||
name = "ILT";
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_TRAMPOLINE.thunkSection, record->data.S_TRAMPOLINE.thunkOffset);
|
||||
size = 5u;
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32)
|
||||
{
|
||||
name = record->data.S_LPROC32.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LPROC32.section, record->data.S_LPROC32.offset);
|
||||
size = record->data.S_LPROC32.codeSize;
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32)
|
||||
{
|
||||
name = record->data.S_GPROC32.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GPROC32.section, record->data.S_GPROC32.offset);
|
||||
size = record->data.S_GPROC32.codeSize;
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32_ID)
|
||||
{
|
||||
name = record->data.S_LPROC32_ID.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LPROC32_ID.section, record->data.S_LPROC32_ID.offset);
|
||||
size = record->data.S_LPROC32_ID.codeSize;
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32_ID)
|
||||
{
|
||||
name = record->data.S_GPROC32_ID.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GPROC32_ID.section, record->data.S_GPROC32_ID.offset);
|
||||
size = record->data.S_GPROC32_ID.codeSize;
|
||||
}
|
||||
|
||||
if (rva == 0u)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
functionSymbols.push_back(FunctionSymbol { name, rva, size, nullptr });
|
||||
seenFunctionRVAs.emplace(rva);
|
||||
});
|
||||
}
|
||||
|
||||
scope.Done(modules.GetLength());
|
||||
}
|
||||
|
||||
// we don't need to touch global symbols in this case.
|
||||
// most of the data we need can be obtained from the module symbol streams, and the global symbol stream only offers data symbols on top of that, which we are not interested in.
|
||||
// however, there can still be public function symbols we haven't seen yet in any of the modules, especially for PDBs that don't provide module-specific information.
|
||||
|
||||
// read public symbols
|
||||
TimedScope publicScope("Reading public symbol stream");
|
||||
const PDB::PublicSymbolStream publicSymbolStream = dbiStream.CreatePublicSymbolStream(rawPdbFile);
|
||||
publicScope.Done();
|
||||
{
|
||||
TimedScope scope("Storing public function symbols");
|
||||
|
||||
const PDB::ArrayView<PDB::HashRecord> hashRecords = publicSymbolStream.GetRecords();
|
||||
const size_t count = hashRecords.GetLength();
|
||||
|
||||
for (const PDB::HashRecord& hashRecord : hashRecords)
|
||||
{
|
||||
const PDB::CodeView::DBI::Record* record = publicSymbolStream.GetRecord(symbolRecordStream, hashRecord);
|
||||
if (record->header.kind != PDB::CodeView::DBI::SymbolRecordKind::S_PUB32)
|
||||
{
|
||||
// normally, a PDB only contains S_PUB32 symbols in the public symbol stream, but we have seen PDBs that also store S_CONSTANT as public symbols.
|
||||
// ignore these.
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((PDB_AS_UNDERLYING(record->data.S_PUB32.flags) & PDB_AS_UNDERLYING(PDB::CodeView::DBI::PublicSymbolFlags::Function)) == 0u)
|
||||
{
|
||||
// ignore everything that is not a function
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_PUB32.section, record->data.S_PUB32.offset);
|
||||
if (rva == 0u)
|
||||
{
|
||||
// certain symbols (e.g. control-flow guard symbols) don't have a valid RVA, ignore those
|
||||
continue;
|
||||
}
|
||||
|
||||
// check whether we already know this symbol from one of the module streams
|
||||
const auto it = seenFunctionRVAs.find(rva);
|
||||
if (it != seenFunctionRVAs.end())
|
||||
{
|
||||
// we know this symbol already, ignore it
|
||||
continue;
|
||||
}
|
||||
|
||||
// this is a new function symbol, so store it.
|
||||
// note that we don't know its size yet.
|
||||
functionSymbols.push_back(FunctionSymbol { record->data.S_PUB32.name, rva, 0u, nullptr });
|
||||
}
|
||||
|
||||
scope.Done(count);
|
||||
}
|
||||
|
||||
|
||||
// we still need to find the size of the public function symbols.
|
||||
// this can be deduced by sorting the symbols by their RVA, and then computing the distance between the current and the next symbol.
|
||||
// this works since functions are always mapped to executable pages, so they aren't interleaved by any data symbols.
|
||||
TimedScope sortScope("std::sort function symbols");
|
||||
std::sort(functionSymbols.begin(), functionSymbols.end(), [](const FunctionSymbol& lhs, const FunctionSymbol& rhs)
|
||||
{
|
||||
return lhs.rva < rhs.rva;
|
||||
});
|
||||
sortScope.Done();
|
||||
|
||||
const size_t symbolCount = functionSymbols.size();
|
||||
if (symbolCount != 0u)
|
||||
{
|
||||
TimedScope computeScope("Computing function symbol sizes");
|
||||
|
||||
size_t foundCount = 0u;
|
||||
|
||||
// we have at least 1 symbol.
|
||||
// compute missing symbol sizes by computing the distance from this symbol to the next.
|
||||
// note that this includes "int 3" padding after the end of a function. if you don't want that, but the actual number of bytes of
|
||||
// the function's code, your best bet is to use a disassembler instead.
|
||||
for (size_t i = 0u; i < symbolCount - 1u; ++i)
|
||||
{
|
||||
FunctionSymbol& currentSymbol = functionSymbols[i];
|
||||
if (currentSymbol.size != 0u)
|
||||
{
|
||||
// the symbol's size is already known
|
||||
continue;
|
||||
}
|
||||
|
||||
const FunctionSymbol& nextSymbol = functionSymbols[i + 1u];
|
||||
const size_t size = nextSymbol.rva - currentSymbol.rva;
|
||||
(void)size; // unused
|
||||
++foundCount;
|
||||
}
|
||||
|
||||
// we know have the sizes of all symbols, except the last.
|
||||
// this can be found by going through the contributions, if needed.
|
||||
FunctionSymbol& lastSymbol = functionSymbols[symbolCount - 1u];
|
||||
if (lastSymbol.size == 0u)
|
||||
{
|
||||
// bad luck, we can't deduce the last symbol's size, so have to consult the contributions instead.
|
||||
// we do a linear search in this case to keep the code simple.
|
||||
const PDB::SectionContributionStream sectionContributionStream = dbiStream.CreateSectionContributionStream(rawPdbFile);
|
||||
const PDB::ArrayView<PDB::DBI::SectionContribution> sectionContributions = sectionContributionStream.GetContributions();
|
||||
for (const PDB::DBI::SectionContribution& contribution : sectionContributions)
|
||||
{
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(contribution.section, contribution.offset);
|
||||
if (rva == 0u)
|
||||
{
|
||||
printf("Contribution has invalid RVA\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rva == lastSymbol.rva)
|
||||
{
|
||||
lastSymbol.size = contribution.size;
|
||||
break;
|
||||
}
|
||||
|
||||
if (rva > lastSymbol.rva)
|
||||
{
|
||||
// should have found the contribution by now
|
||||
printf("Unknown contribution for symbol %s at RVA 0x%X", lastSymbol.name.c_str(), lastSymbol.rva);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computeScope.Done(foundCount);
|
||||
}
|
||||
|
||||
total.Done(functionSymbols.size());
|
||||
}
|
||||
382
third_party/raw_pdb/src/Examples/ExampleFunctionVariables.cpp
vendored
Normal file
382
third_party/raw_pdb/src/Examples/ExampleFunctionVariables.cpp
vendored
Normal file
@@ -0,0 +1,382 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleTimedScope.h"
|
||||
#include "ExampleTypeTable.h"
|
||||
#include "PDB_RawFile.h"
|
||||
#include "PDB_DBIStream.h"
|
||||
#include "PDB_TPIStream.h"
|
||||
|
||||
using SymbolRecordKind = PDB::CodeView::DBI::SymbolRecordKind;
|
||||
|
||||
static std::string GetVariableTypeName(const TypeTable& typeTable, uint32_t typeIndex)
|
||||
{
|
||||
// Defined in ExampleTypes.cpp
|
||||
extern std::string GetTypeName(const TypeTable & typeTable, uint32_t typeIndex);
|
||||
|
||||
std::string typeName = GetTypeName(typeTable, typeIndex);
|
||||
|
||||
// Remove any '%s' substring used to insert a variable/field name.
|
||||
const uint64_t markerPos = typeName.find("%s");
|
||||
if (markerPos != typeName.npos)
|
||||
{
|
||||
typeName.erase(markerPos, 2);
|
||||
}
|
||||
|
||||
return typeName;
|
||||
}
|
||||
|
||||
static void Printf(uint32_t indent, const char* format, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
|
||||
printf("%*s", indent * 4, "");
|
||||
vprintf(format, args);
|
||||
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void ExampleFunctionVariables(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::TPIStream& tpiStream);
|
||||
void ExampleFunctionVariables(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::TPIStream& tpiStream)
|
||||
{
|
||||
TimedScope total("\nRunning example \"Function variables\"");
|
||||
|
||||
TimedScope typeTableScope("Create TypeTable");
|
||||
TypeTable typeTable(tpiStream);
|
||||
typeTableScope.Done();
|
||||
|
||||
// in order to keep the example easy to understand, we load the PDB data serially.
|
||||
// note that this can be improved a lot by reading streams concurrently.
|
||||
|
||||
// prepare the image section stream first. it is needed for converting section + offset into an RVA
|
||||
TimedScope sectionScope("Reading image section stream");
|
||||
const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawPdbFile);
|
||||
sectionScope.Done();
|
||||
|
||||
// prepare the module info stream for grabbing function symbols from modules
|
||||
TimedScope moduleScope("Reading module info stream");
|
||||
const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
|
||||
moduleScope.Done();
|
||||
|
||||
// prepare symbol record stream needed by the public stream
|
||||
TimedScope symbolStreamScope("Reading symbol record stream");
|
||||
const PDB::CoalescedMSFStream symbolRecordStream = dbiStream.CreateSymbolRecordStream(rawPdbFile);
|
||||
symbolStreamScope.Done();
|
||||
|
||||
{
|
||||
TimedScope scope("Printing function variable records from modules\n");
|
||||
|
||||
const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = moduleInfoStream.GetModules();
|
||||
|
||||
uint32_t blockLevel = 0;
|
||||
uint32_t recordCount = 0;
|
||||
|
||||
for (const PDB::ModuleInfoStream::Module& module : modules)
|
||||
{
|
||||
if (!module.HasSymbolStream())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const PDB::ModuleSymbolStream moduleSymbolStream = module.CreateSymbolStream(rawPdbFile);
|
||||
moduleSymbolStream.ForEachSymbol([&typeTable, &imageSectionStream, &blockLevel, &recordCount](const PDB::CodeView::DBI::Record* record)
|
||||
{
|
||||
const SymbolRecordKind kind = record->header.kind;
|
||||
const PDB::CodeView::DBI::Record::Data& data = record->data;
|
||||
|
||||
if (kind == SymbolRecordKind::S_END)
|
||||
{
|
||||
PDB_ASSERT(blockLevel > 0, "Block level for S_END is 0");
|
||||
blockLevel--;
|
||||
Printf(blockLevel, "S_END\n");
|
||||
|
||||
if (blockLevel == 0)
|
||||
{
|
||||
Printf(0, "\n");
|
||||
}
|
||||
}
|
||||
else if(kind == SymbolRecordKind::S_SKIP)
|
||||
{
|
||||
Printf(blockLevel, "S_SKIP\n");
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_BLOCK32)
|
||||
{
|
||||
const uint32_t offset = imageSectionStream.ConvertSectionOffsetToRVA(data.S_BLOCK32.section, data.S_BLOCK32.offset);
|
||||
|
||||
Printf(blockLevel, "S_BLOCK32: '%s' | Code Offset 0x%X\n", data.S_BLOCK32.name, offset);
|
||||
blockLevel++;
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_LABEL32)
|
||||
{
|
||||
Printf(blockLevel, "S_LABEL32: '%s' | Offset 0x%X\n", data.S_LABEL32.name, data.S_LABEL32.offset);
|
||||
}
|
||||
else if(kind == SymbolRecordKind::S_CONSTANT)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_CONSTANT.typeIndex);
|
||||
|
||||
Printf(blockLevel, "S_CONSTANT: '%s' -> '%s' | Value 0x%X\n", typeName.c_str(), data.S_CONSTANT.name, data.S_CONSTANT.value);
|
||||
}
|
||||
else if(kind == SymbolRecordKind::S_LOCAL)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_LOCAL.typeIndex);
|
||||
Printf(blockLevel, "S_LOCAL: '%s' -> '%s' | Param: %s | Optimized Out: %s\n", typeName.c_str(), data.S_LOCAL.name, data.S_LOCAL.flags.fIsParam ? "True" : "False", data.S_LOCAL.flags.fIsOptimizedOut ? "True" : "False");
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_DEFRANGE_REGISTER)
|
||||
{
|
||||
Printf(blockLevel, "S_DEFRANGE_REGISTER: Register 0x%X\n", data.S_DEFRANGE_REGISTER.reg);
|
||||
}
|
||||
else if(kind == SymbolRecordKind::S_DEFRANGE_FRAMEPOINTER_REL)
|
||||
{
|
||||
Printf(blockLevel, "S_DEFRANGE_FRAMEPOINTER_REL: Frame Pointer Offset 0x%X | Range Start 0x%X | Range Section Start 0x%X | Range Length %u\n",
|
||||
data.S_DEFRANGE_FRAMEPOINTER_REL.offsetFramePointer,
|
||||
data.S_DEFRANGE_FRAMEPOINTER_REL.range.offsetStart,
|
||||
data.S_DEFRANGE_FRAMEPOINTER_REL.range.isectionStart,
|
||||
data.S_DEFRANGE_FRAMEPOINTER_REL.range.length);
|
||||
}
|
||||
else if(kind == SymbolRecordKind::S_DEFRANGE_SUBFIELD_REGISTER)
|
||||
{
|
||||
Printf(blockLevel, "S_DEFRANGE_SUBFIELD_REGISTER: Register %u | Parent offset 0x%X | Range Start 0x%X | Range Section Start 0x%X | Range Length %u\n",
|
||||
data.S_DEFRANGE_SUBFIELD_REGISTER.reg,
|
||||
data.S_DEFRANGE_SUBFIELD_REGISTER.offsetParent,
|
||||
data.S_DEFRANGE_SUBFIELD_REGISTER.range.offsetStart,
|
||||
data.S_DEFRANGE_SUBFIELD_REGISTER.range.isectionStart,
|
||||
data.S_DEFRANGE_SUBFIELD_REGISTER.range.length);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_DEFRANGE_FRAMEPOINTER_REL_FULL_SCOPE)
|
||||
{
|
||||
Printf(blockLevel, "S_DEFRANGE_FRAMEPOINTER_REL_FULL_SCOPE: Offset 0x%X\n", data.S_DEFRANGE_FRAMEPOINTER_REL_FULL_SCOPE.offsetFramePointer);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_DEFRANGE_REGISTER_REL)
|
||||
{
|
||||
Printf(blockLevel, "S_DEFRANGE_REGISTER_REL: Base Register %u | Parent offset 0x%X | Base Register Offset 0x%X | Range Start 0x%X | Range Section Start 0x%X | Range Length %u\n",
|
||||
data.S_DEFRANGE_REGISTER_REL.baseRegister,
|
||||
data.S_DEFRANGE_REGISTER_REL.offsetParent,
|
||||
data.S_DEFRANGE_REGISTER_REL.offsetBasePointer,
|
||||
data.S_DEFRANGE_REGISTER_REL.offsetParent,
|
||||
data.S_DEFRANGE_REGISTER_REL.range.offsetStart,
|
||||
data.S_DEFRANGE_REGISTER_REL.range.isectionStart,
|
||||
data.S_DEFRANGE_REGISTER_REL.range.length);
|
||||
}
|
||||
else if(kind == SymbolRecordKind::S_FILESTATIC)
|
||||
{
|
||||
Printf(blockLevel, "S_FILESTATIC: '%s'\n", data.S_FILESTATIC.name);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_INLINESITE)
|
||||
{
|
||||
Printf(blockLevel, "S_INLINESITE: Parent 0x%X\n", data.S_INLINESITE.parent);
|
||||
blockLevel++;
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_INLINESITE_END)
|
||||
{
|
||||
PDB_ASSERT(blockLevel > 0, "Block level for S_INLINESITE_END is 0");
|
||||
blockLevel--;
|
||||
Printf(blockLevel, "S_INLINESITE_END:\n");
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_CALLEES)
|
||||
{
|
||||
Printf(blockLevel, "S_CALLEES: Count %u\n", data.S_CALLEES.count);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_CALLERS)
|
||||
{
|
||||
Printf(blockLevel, "S_CALLERS: Count %u\n", data.S_CALLERS.count);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_INLINEES)
|
||||
{
|
||||
Printf(blockLevel, "S_INLINEES: Count %u\n", data.S_INLINEES.count);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_LDATA32)
|
||||
{
|
||||
if (blockLevel > 0)
|
||||
{
|
||||
// Not sure why some type index 0 (T_NO_TYPE) are included in some PDBs.
|
||||
if (data.S_LDATA32.typeIndex != 0) // PDB::CodeView::TPI::TypeIndexKind::T_NOTYPE)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_LDATA32.typeIndex);
|
||||
Printf(blockLevel, "S_LDATA32: '%s' -> '%s'\n", data.S_LDATA32.name, typeName.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_LTHREAD32)
|
||||
{
|
||||
if (blockLevel > 0)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_LTHREAD32.typeIndex);
|
||||
Printf(blockLevel, "S_LTHREAD32: '%s' -> '%s'\n", data.S_LTHREAD32.name, typeName.c_str());
|
||||
}
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_UDT)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_UDT.typeIndex);
|
||||
|
||||
Printf(blockLevel, "S_UDT: '%s' -> '%s'\n", data.S_UDT.name, typeName.c_str());
|
||||
}
|
||||
else if (kind == PDB::CodeView::DBI::SymbolRecordKind::S_REGISTER)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_REGSYM.typeIndex);
|
||||
|
||||
Printf(blockLevel, "S_REGSYM: '%s' -> '%s' | Register %i\n",
|
||||
data.S_REGSYM.name, typeName.c_str(),
|
||||
data.S_REGSYM.reg);
|
||||
}
|
||||
else if (kind == PDB::CodeView::DBI::SymbolRecordKind::S_BPREL32)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_BPRELSYM32.typeIndex);
|
||||
|
||||
Printf(blockLevel, "S_BPRELSYM32: '%s' -> '%s' | BP register Offset 0x%X\n",
|
||||
data.S_BPRELSYM32.name, typeName.c_str(),
|
||||
data.S_BPRELSYM32.offset);
|
||||
}
|
||||
else if (kind == PDB::CodeView::DBI::SymbolRecordKind::S_REGREL32)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_REGREL32.typeIndex);
|
||||
|
||||
Printf(blockLevel, "S_REGREL32: '%s' -> '%s' | Register %i | Register Offset 0x%X\n",
|
||||
data.S_REGREL32.name, typeName.c_str(),
|
||||
data.S_REGREL32.reg,
|
||||
data.S_REGREL32.offset);
|
||||
}
|
||||
else if(kind == SymbolRecordKind::S_FRAMECOOKIE)
|
||||
{
|
||||
Printf(blockLevel, "S_FRAMECOOKIE: Offset 0x%X | Register %u | Type %u\n",
|
||||
data.S_FRAMECOOKIE.offset,
|
||||
data.S_FRAMECOOKIE.reg,
|
||||
data.S_FRAMECOOKIE.cookietype);
|
||||
}
|
||||
else if(kind == SymbolRecordKind::S_CALLSITEINFO)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_CALLSITEINFO.typeIndex);
|
||||
Printf(blockLevel, "S_CALLSITEINFO: '%s' | Offset 0x%X | Section %u\n", typeName.c_str(), data.S_CALLSITEINFO.offset, data.S_CALLSITEINFO.section);
|
||||
}
|
||||
else if(kind == SymbolRecordKind::S_HEAPALLOCSITE)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_HEAPALLOCSITE.typeIndex);
|
||||
Printf(blockLevel, "S_HEAPALLOCSITE: '%s' | Offset 0x%X | Section %u | Instruction Length %u\n", typeName.c_str(),
|
||||
data.S_HEAPALLOCSITE.offset,
|
||||
data.S_HEAPALLOCSITE.section,
|
||||
data.S_HEAPALLOCSITE.instructionLength);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_FRAMEPROC)
|
||||
{
|
||||
Printf(blockLevel, "S_FRAMEPROC: Size %u | Padding %u | Padding Offset 0x%X | Callee Registers Size %u\n",
|
||||
data.S_FRAMEPROC.cbFrame,
|
||||
data.S_FRAMEPROC.cbPad,
|
||||
data.S_FRAMEPROC.offPad,
|
||||
data.S_FRAMEPROC.cbSaveRegs);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_ANNOTATION)
|
||||
{
|
||||
Printf(blockLevel, "S_ANNOTATION: Offset 0x%X | Count %u\n", data.S_ANNOTATIONSYM.offset, data.S_ANNOTATIONSYM.annotationsCount);
|
||||
// print N null-terminated annotation strings, skipping their null-terminators to get to the next string
|
||||
const char* annotation = data.S_ANNOTATIONSYM.annotations;
|
||||
for (int i = 0; i < data.S_ANNOTATIONSYM.annotationsCount; ++i, annotation += strlen(annotation) + 1)
|
||||
Printf(blockLevel + 1, "S_ANNOTATION.%u: %s\n", i, annotation);
|
||||
PDB_ASSERT(annotation <= (const char*)record + record->header.size + sizeof(record->header.size),
|
||||
"Annotation strings end beyond the record size %X; annotaions count: %u", record->header.size, data.S_ANNOTATIONSYM.annotationsCount);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_THUNK32)
|
||||
{
|
||||
PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
|
||||
|
||||
if (data.S_THUNK32.thunk == PDB::CodeView::DBI::ThunkOrdinal::TrampolineIncremental)
|
||||
{
|
||||
// we have never seen incremental linking thunks stored inside a S_THUNK32 symbol, but better safe than sorry
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_THUNK32.section, data.S_THUNK32.offset);
|
||||
Printf(blockLevel, "Function: 'ILT/Thunk' | RVA 0x%X\n", rva);
|
||||
}
|
||||
else
|
||||
{
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_THUNK32.section, data.S_THUNK32.offset);
|
||||
Printf(blockLevel, "S_THUNK32 Function '%s' | RVA 0x%X\n", data.S_THUNK32.name, rva);
|
||||
blockLevel++;
|
||||
}
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_TRAMPOLINE)
|
||||
{
|
||||
PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
|
||||
// incremental linking thunks are stored in the linker module
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_TRAMPOLINE.thunkSection, data.S_TRAMPOLINE.thunkOffset);
|
||||
Printf(blockLevel, "Function 'ILT/Trampoline' | RVA 0x%X\n", rva);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_LPROC32)
|
||||
{
|
||||
PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_LPROC32.section, data.S_LPROC32.offset);
|
||||
Printf(blockLevel, "S_LPROC32 Function '%s' | RVA 0x%X\n", data.S_LPROC32.name, rva);
|
||||
blockLevel++;
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_GPROC32)
|
||||
{
|
||||
PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_GPROC32.section, data.S_GPROC32.offset);
|
||||
Printf(blockLevel, "S_GPROC32 Function '%s' | RVA 0x%X\n", data.S_GPROC32.name, rva);
|
||||
blockLevel++;
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_LPROC32_ID)
|
||||
{
|
||||
PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_LPROC32_ID.section, data.S_LPROC32_ID.offset);
|
||||
Printf(blockLevel, "S_LPROC32_ID Function '%s' | RVA 0x%X\n", data.S_LPROC32_ID.name, rva);
|
||||
blockLevel++;
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_GPROC32_ID)
|
||||
{
|
||||
PDB_ASSERT(blockLevel == 0, "BlockLevel %u != 0", blockLevel);
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(data.S_GPROC32_ID.section, data.S_GPROC32_ID.offset);
|
||||
Printf(blockLevel, "S_GPROC32_ID Function '%s' | RVA 0x%X\n", data.S_GPROC32_ID.name, rva);
|
||||
blockLevel++;
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_REGREL32_INDIR)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_REGREL32_INDIR.typeIndex);
|
||||
|
||||
Printf(blockLevel, "S_REGREL32_INDIR: '%s' -> '%s' | Register %i | Unknown1 0x%X | Unknown2 0x%X\n",
|
||||
data.S_REGREL32_INDIR.name, typeName.c_str(),
|
||||
data.S_REGREL32_INDIR.unknown1,
|
||||
data.S_REGREL32_INDIR.unknown1);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_REGREL32_ENCTMP)
|
||||
{
|
||||
const std::string typeName = GetVariableTypeName(typeTable, data.S_REGREL32.typeIndex);
|
||||
|
||||
Printf(blockLevel, "S_REGREL32_ENCTMP: '%s' -> '%s' | Register %i | Register Offset 0x%X\n",
|
||||
data.S_REGREL32.name, typeName.c_str(),
|
||||
data.S_REGREL32.reg,
|
||||
data.S_REGREL32.offset);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_UNAMESPACE)
|
||||
{
|
||||
Printf(blockLevel, "S_UNAMESPACE: '%s'\n", data.S_UNAMESPACE.name);
|
||||
}
|
||||
else if (kind == SymbolRecordKind::S_ARMSWITCHTABLE)
|
||||
{
|
||||
Printf(blockLevel, "S_ARMSWITCHTABLE: "
|
||||
"Switch Type: %u | Num Entries: %u | Base Section: %u | Base Offset: 0x%X | "
|
||||
"Branch Section: %u | Branch Offset: 0x%X | Table Section: %u | Table Offset: 0x%X\n",
|
||||
data.S_ARMSWITCHTABLE.switchType,
|
||||
data.S_ARMSWITCHTABLE.numEntries,
|
||||
data.S_ARMSWITCHTABLE.sectionBase,
|
||||
data.S_ARMSWITCHTABLE.offsetBase,
|
||||
data.S_ARMSWITCHTABLE.sectionBranch,
|
||||
data.S_ARMSWITCHTABLE.offsetBranch,
|
||||
data.S_ARMSWITCHTABLE.sectionTable,
|
||||
data.S_ARMSWITCHTABLE.offsetTable);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We only care about records inside functions.
|
||||
if (blockLevel > 0)
|
||||
{
|
||||
PDB_ASSERT(false, "Unhandled record kind 0x%X with block level %u\n", static_cast<uint16_t>(kind), blockLevel);
|
||||
}
|
||||
}
|
||||
|
||||
recordCount++;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
scope.Done(recordCount);
|
||||
}
|
||||
}
|
||||
198
third_party/raw_pdb/src/Examples/ExampleIPI.cpp
vendored
Normal file
198
third_party/raw_pdb/src/Examples/ExampleIPI.cpp
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleTimedScope.h"
|
||||
#include "ExampleTypeTable.h"
|
||||
#include "PDB_RawFile.h"
|
||||
#include "PDB_InfoStream.h"
|
||||
#include "PDB_IPIStream.h"
|
||||
#include "PDB_TPIStream.h"
|
||||
|
||||
static std::string GetTypeNameIPI(const TypeTable& typeTable, uint32_t typeIndex)
|
||||
{
|
||||
// Defined in ExampleTypes.cpp
|
||||
extern std::string GetTypeName(const TypeTable & typeTable, uint32_t typeIndex);
|
||||
|
||||
std::string typeName = GetTypeName(typeTable, typeIndex);
|
||||
|
||||
// Remove any '%s' substring used to insert a variable/field name.
|
||||
const uint64_t markerPos = typeName.find("%s");
|
||||
if (markerPos != typeName.npos)
|
||||
{
|
||||
typeName.erase(markerPos, 2);
|
||||
}
|
||||
|
||||
return typeName;
|
||||
}
|
||||
|
||||
void ExampleIPI(const PDB::RawFile& rawPdbFile, const PDB::InfoStream& infoStream, const PDB::TPIStream& tpiStream, const PDB::IPIStream& ipiStream);
|
||||
|
||||
void ExampleIPI(const PDB::RawFile& rawPdbFile, const PDB::InfoStream& infoStream, const PDB::TPIStream& tpiStream, const PDB::IPIStream& ipiStream)
|
||||
{
|
||||
if (!infoStream.HasIPIStream())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TimedScope total("\nRunning example \"IPI\"");
|
||||
|
||||
TimedScope typeTableScope("Create TypeTable");
|
||||
TypeTable typeTable(tpiStream);
|
||||
typeTableScope.Done();
|
||||
|
||||
// prepare names stream for grabbing file paths from lines
|
||||
TimedScope namesScope("Reading names stream");
|
||||
const PDB::NamesStream namesStream = infoStream.CreateNamesStream(rawPdbFile);
|
||||
namesScope.Done();
|
||||
|
||||
const uint32_t firstTypeIndex = ipiStream.GetFirstTypeIndex();
|
||||
|
||||
PDB::ArrayView<const PDB::CodeView::IPI::Record*> records = ipiStream.GetTypeRecords();
|
||||
|
||||
std::vector<const char*> strings;
|
||||
|
||||
strings.resize(records.GetLength(), nullptr);
|
||||
|
||||
size_t index = 0;
|
||||
|
||||
for (const PDB::CodeView::IPI::Record* record : records)
|
||||
{
|
||||
const PDB::CodeView::IPI::RecordHeader& header = record->header;
|
||||
|
||||
if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_STRING_ID)
|
||||
{
|
||||
strings[index] = record->data.LF_STRING_ID.name;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
uint32_t identifier = firstTypeIndex;
|
||||
|
||||
std::string typeName, parentTypeName;
|
||||
|
||||
printf("\n --- IPI Records ---\n\n");
|
||||
|
||||
for(const PDB::CodeView::IPI::Record* record : records)
|
||||
{
|
||||
const PDB::CodeView::IPI::RecordHeader& header = record->header;
|
||||
|
||||
if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_FUNC_ID)
|
||||
{
|
||||
typeName = GetTypeNameIPI(typeTable, record->data.LF_FUNC_ID.typeIndex);
|
||||
|
||||
printf("Kind: 'LF_FUNC_ID' Size: %i ID: %u\n", header.size, identifier);
|
||||
printf(" Scope ID: %u\n Type: '%s'\n Name: '%s'\n\n",
|
||||
record->data.LF_FUNC_ID.scopeId,
|
||||
typeName.c_str(),
|
||||
record->data.LF_FUNC_ID.name);
|
||||
|
||||
}
|
||||
else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_MFUNC_ID)
|
||||
{
|
||||
typeName = GetTypeNameIPI(typeTable, record->data.LF_MFUNC_ID.typeIndex);
|
||||
parentTypeName = GetTypeNameIPI(typeTable, record->data.LF_MFUNC_ID.parentTypeIndex);
|
||||
|
||||
printf("Kind: 'LF_MFUNC_ID' Size: %i ID: %u\n", header.size, identifier);
|
||||
printf(" Parent Type: '%s'\n Type: '%s'\n Name: '%s'\n\n",
|
||||
parentTypeName.c_str(),
|
||||
typeName.c_str(),
|
||||
record->data.LF_MFUNC_ID.name);
|
||||
|
||||
}
|
||||
else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_BUILDINFO)
|
||||
{
|
||||
printf("Kind: 'LF_BUILDINFO' Size: %u ID: %u\n", header.size, identifier);
|
||||
|
||||
if (record->data.LF_BUILDINFO.count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
printf("Strings: '%s'", strings[record->data.LF_BUILDINFO.typeIndices[0] - firstTypeIndex]);
|
||||
|
||||
for (uint32_t i = 1, size = record->data.LF_BUILDINFO.count; i < size; ++i)
|
||||
{
|
||||
const uint32_t stringIndex = record->data.LF_BUILDINFO.typeIndices[i];
|
||||
|
||||
if (stringIndex == 0)
|
||||
{
|
||||
printf(", ''");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf(", '%s'", strings[stringIndex - firstTypeIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n\n");
|
||||
}
|
||||
else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_SUBSTR_LIST)
|
||||
{
|
||||
printf("Kind: 'LF_SUBSTR_LIST' Size: %u ID: %u\n", header.size, identifier);
|
||||
|
||||
if (record->data.LF_SUBSTR_LIST.count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
printf(" Strings: '%s'", strings[record->data.LF_SUBSTR_LIST.typeIndices[0] - firstTypeIndex]);
|
||||
|
||||
for (uint32_t i = 1, size = record->data.LF_SUBSTR_LIST.count; i < size; ++i)
|
||||
{
|
||||
const uint32_t stringIndex = record->data.LF_SUBSTR_LIST.typeIndices[i];
|
||||
|
||||
if (stringIndex == 0)
|
||||
{
|
||||
printf(", ''");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf(", '%s'", strings[stringIndex - firstTypeIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n\n");
|
||||
}
|
||||
else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_STRING_ID)
|
||||
{
|
||||
printf("Kind: 'LF_STRING_ID' Size: %u ID: %u\n", header.size, identifier);
|
||||
|
||||
printf(" Substring ID: %u\n Name: '%s'\n\n", record->data.LF_STRING_ID.id, record->data.LF_STRING_ID.name);
|
||||
}
|
||||
else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_UDT_SRC_LINE)
|
||||
{
|
||||
typeName = GetTypeNameIPI(typeTable, record->data.LF_UDT_SRC_LINE.typeIndex);
|
||||
|
||||
const uint32_t stringIndex = record->data.LF_UDT_SRC_LINE.stringIndex;
|
||||
|
||||
printf("Kind: 'LF_UDT_SRC_LINE' Size: %u ID: %u\n", header.size, identifier);
|
||||
|
||||
printf(" Type: '%s'\n Source Path: %s\n Line: %u\n\n",
|
||||
typeName.c_str(),
|
||||
strings[stringIndex - firstTypeIndex],
|
||||
record->data.LF_UDT_SRC_LINE.line);
|
||||
}
|
||||
else if (header.kind == PDB::CodeView::IPI::TypeRecordKind::LF_UDT_MOD_SRC_LINE)
|
||||
{
|
||||
typeName = GetTypeNameIPI(typeTable, record->data.LF_UDT_MOD_SRC_LINE.typeIndex);
|
||||
|
||||
const char* string = namesStream.GetFilename(record->data.LF_UDT_MOD_SRC_LINE.stringIndex);
|
||||
|
||||
printf("Kind: 'LF_UDT_SRC_LINE' Size: %u ID: %u\n", header.size, identifier);
|
||||
|
||||
printf(" Type: '%s'\n Source Path: %s\n Line: %u\n Module Index: %u\n\n",
|
||||
typeName.c_str(),
|
||||
string,
|
||||
record->data.LF_UDT_MOD_SRC_LINE.line,
|
||||
record->data.LF_UDT_MOD_SRC_LINE.moduleIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("Kind: 0x%X Size: %u ID: %u\n\n", static_cast<uint32_t>(header.kind), header.size, identifier);
|
||||
}
|
||||
|
||||
identifier++;
|
||||
}
|
||||
}
|
||||
268
third_party/raw_pdb/src/Examples/ExampleLines.cpp
vendored
Normal file
268
third_party/raw_pdb/src/Examples/ExampleLines.cpp
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleTimedScope.h"
|
||||
#include "Foundation/PDB_PointerUtil.h"
|
||||
#include "PDB_RawFile.h"
|
||||
#include "PDB_DBIStream.h"
|
||||
#include "PDB_InfoStream.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct Section
|
||||
{
|
||||
uint16_t index;
|
||||
uint32_t offset;
|
||||
size_t lineIndex;
|
||||
};
|
||||
|
||||
struct Filename
|
||||
{
|
||||
uint32_t fileChecksumOffset;
|
||||
uint32_t namesFilenameOffset;
|
||||
PDB::CodeView::DBI::ChecksumKind checksumKind;
|
||||
uint8_t checksumSize;
|
||||
uint8_t checksum[32];
|
||||
};
|
||||
|
||||
struct Line
|
||||
{
|
||||
uint32_t lineNumber;
|
||||
uint32_t codeSize;
|
||||
size_t filenameIndex;
|
||||
};
|
||||
}
|
||||
|
||||
void ExampleLines(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::InfoStream& infoStream);
|
||||
void ExampleLines(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::InfoStream& infoStream)
|
||||
{
|
||||
if (!infoStream.HasNamesStream())
|
||||
{
|
||||
printf("PDB has no '/names' stream for looking up filenames for lines, skipping \"Lines\" example.");
|
||||
return;
|
||||
}
|
||||
|
||||
TimedScope total("\nRunning example \"Lines\"");
|
||||
|
||||
// prepare the image section stream first. it is needed for converting section + offset into an RVA
|
||||
TimedScope sectionScope("Reading image section stream");
|
||||
const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawPdbFile);
|
||||
sectionScope.Done();
|
||||
|
||||
// prepare the module info stream for grabbing function symbols from modules
|
||||
TimedScope moduleScope("Reading module info stream");
|
||||
const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
|
||||
moduleScope.Done();
|
||||
|
||||
// prepare names stream for grabbing file paths from lines
|
||||
TimedScope namesScope("Reading names stream");
|
||||
const PDB::NamesStream namesStream = infoStream.CreateNamesStream(rawPdbFile);
|
||||
namesScope.Done();
|
||||
|
||||
// keeping sections and lines separate, as sorting the smaller Section struct is 2x faster in release builds
|
||||
// than having all the fields in one big Line struct and sorting those.
|
||||
std::vector<Section> sections;
|
||||
std::vector<Filename> filenames;
|
||||
std::vector<Line> lines;
|
||||
|
||||
{
|
||||
TimedScope scope("Storing lines from modules");
|
||||
|
||||
const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = moduleInfoStream.GetModules();
|
||||
|
||||
for (const PDB::ModuleInfoStream::Module& module : modules)
|
||||
{
|
||||
if (!module.HasLineStream())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const PDB::ModuleLineStream moduleLineStream = module.CreateLineStream(rawPdbFile);
|
||||
|
||||
const size_t moduleFilenamesStartIndex = filenames.size();
|
||||
const PDB::CodeView::DBI::FileChecksumHeader* moduleFileChecksumHeader = nullptr;
|
||||
|
||||
moduleLineStream.ForEachSection([&moduleLineStream, &namesStream, &moduleFileChecksumHeader, §ions, &filenames, &lines](const PDB::CodeView::DBI::LineSection* lineSection)
|
||||
{
|
||||
if (lineSection->header.kind == PDB::CodeView::DBI::DebugSubsectionKind::S_LINES)
|
||||
{
|
||||
moduleLineStream.ForEachLinesBlock(lineSection,
|
||||
[&lineSection, §ions, &filenames, &lines](const PDB::CodeView::DBI::LinesFileBlockHeader* linesBlockHeader, const PDB::CodeView::DBI::Line* blocklines, const PDB::CodeView::DBI::Column* blockColumns)
|
||||
{
|
||||
if (linesBlockHeader->numLines == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const PDB::CodeView::DBI::Line& firstLine = blocklines[0];
|
||||
|
||||
const uint16_t sectionIndex = lineSection->linesHeader.sectionIndex;
|
||||
const uint32_t sectionOffset = lineSection->linesHeader.sectionOffset;
|
||||
const uint32_t fileChecksumOffset = linesBlockHeader->fileChecksumOffset;
|
||||
|
||||
const size_t filenameIndex = filenames.size();
|
||||
|
||||
// there will be duplicate filenames for any real world pdb.
|
||||
// ideally the filenames would be stored in a map with the filename or checksum as the key.
|
||||
// but that would complicate the logic in this example and therefore just use a vector to make it easier to understand.
|
||||
filenames.push_back({ fileChecksumOffset, 0, PDB::CodeView::DBI::ChecksumKind::None, 0, {0} });
|
||||
|
||||
sections.push_back({ sectionIndex, sectionOffset, lines.size() });
|
||||
|
||||
// initially set code size of first line to 0, will be updated in loop below.
|
||||
lines.push_back({ firstLine.linenumStart, 0, filenameIndex });
|
||||
|
||||
for(uint32_t i = 1, size = linesBlockHeader->numLines; i < size; ++i)
|
||||
{
|
||||
const PDB::CodeView::DBI::Line& line = blocklines[i];
|
||||
|
||||
// calculate code size of previous line by using the current line offset.
|
||||
lines.back().codeSize = line.offset - blocklines[i-1].offset;
|
||||
|
||||
sections.push_back({ sectionIndex, sectionOffset + line.offset, lines.size() });
|
||||
lines.push_back({ line.linenumStart, 0, filenameIndex });
|
||||
}
|
||||
|
||||
// calc code size of last line
|
||||
lines.back().codeSize = lineSection->linesHeader.codeSize - blocklines[linesBlockHeader->numLines-1].offset;
|
||||
|
||||
// columns are optional
|
||||
if (blockColumns == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0, size = linesBlockHeader->numLines; i < size; ++i)
|
||||
{
|
||||
const PDB::CodeView::DBI::Column& column = blockColumns[i];
|
||||
(void)column;
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (lineSection->header.kind == PDB::CodeView::DBI::DebugSubsectionKind::S_FILECHECKSUMS)
|
||||
{
|
||||
// how to read checksums and their filenames from the Names Stream
|
||||
moduleLineStream.ForEachFileChecksum(lineSection, [&namesStream](const PDB::CodeView::DBI::FileChecksumHeader* fileChecksumHeader)
|
||||
{
|
||||
const char* filename = namesStream.GetFilename(fileChecksumHeader->filenameOffset);
|
||||
(void)filename;
|
||||
});
|
||||
|
||||
// store the checksum header for the module, as there might be more lines after the checksums.
|
||||
// so lines will get their checksum header values assigned after processing all line sections in the module.
|
||||
PDB_ASSERT(moduleFileChecksumHeader == nullptr, "Module File Checksum Header already set");
|
||||
moduleFileChecksumHeader = &lineSection->checksumHeader;
|
||||
}
|
||||
else if (lineSection->header.kind == PDB::CodeView::DBI::DebugSubsectionKind::S_INLINEELINES)
|
||||
{
|
||||
if (lineSection->inlineeHeader.kind == PDB::CodeView::DBI::InlineeSourceLineKind::Signature)
|
||||
{
|
||||
moduleLineStream.ForEachInlineeSourceLine(lineSection, [](const PDB::CodeView::DBI::InlineeSourceLine* inlineeSourceLine)
|
||||
{
|
||||
(void)inlineeSourceLine;
|
||||
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
moduleLineStream.ForEachInlineeSourceLineEx(lineSection, [](const PDB::CodeView::DBI::InlineeSourceLineEx* inlineeSourceLineEx)
|
||||
{
|
||||
for (uint32_t i = 0; i < inlineeSourceLineEx->extraLines; ++i)
|
||||
{
|
||||
const uint32_t checksumOffset = inlineeSourceLineEx->extrafileChecksumOffsets[i];
|
||||
(void)checksumOffset;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PDB_ASSERT(false, "Line Section kind 0x%X not handled", static_cast<uint32_t>(lineSection->header.kind));
|
||||
}
|
||||
});
|
||||
|
||||
// assign checksum values for each filename added in this module
|
||||
for (size_t i = moduleFilenamesStartIndex, size = filenames.size(); i < size; ++i)
|
||||
{
|
||||
Filename& filename = filenames[i];
|
||||
|
||||
// look up the filename's checksum header in the module's checksums section
|
||||
const PDB::CodeView::DBI::FileChecksumHeader* checksumHeader = PDB::Pointer::Offset<const PDB::CodeView::DBI::FileChecksumHeader*>(moduleFileChecksumHeader, filename.fileChecksumOffset);
|
||||
|
||||
PDB_ASSERT(checksumHeader->checksumKind >= PDB::CodeView::DBI::ChecksumKind::None &&
|
||||
checksumHeader->checksumKind <= PDB::CodeView::DBI::ChecksumKind::SHA256,
|
||||
"Invalid checksum kind %u", static_cast<uint16_t>(checksumHeader->checksumKind));
|
||||
|
||||
// store checksum values in filname struct
|
||||
filename.namesFilenameOffset = checksumHeader->filenameOffset;
|
||||
filename.checksumKind = checksumHeader->checksumKind;
|
||||
filename.checksumSize = checksumHeader->checksumSize;
|
||||
std::memcpy(filename.checksum, checksumHeader->checksum, checksumHeader->checksumSize);
|
||||
}
|
||||
}
|
||||
|
||||
scope.Done(modules.GetLength());
|
||||
|
||||
TimedScope sortScope("std::sort sections");
|
||||
|
||||
// sort sections, so we can iterate over lines by address order.
|
||||
std::sort(sections.begin(), sections.end(), [](const Section& lhs, const Section& rhs)
|
||||
{
|
||||
if (lhs.index == rhs.index)
|
||||
{
|
||||
return lhs.offset < rhs.offset;
|
||||
}
|
||||
|
||||
return lhs.index < rhs.index;
|
||||
});
|
||||
|
||||
sortScope.Done(sections.size());
|
||||
|
||||
// Disabled by default, as it will print a lot of lines for large PDBs :-)
|
||||
#if 0
|
||||
// DIA2Dump style lines output
|
||||
static const char hexChars[17] = "0123456789ABCDEF";
|
||||
char checksumString[128];
|
||||
|
||||
printf("*** LINES RAW PDB\n");
|
||||
|
||||
const char* prevFilename = nullptr;
|
||||
|
||||
for (const Section& section : sections)
|
||||
{
|
||||
const Line& line = lines[section.lineIndex];
|
||||
const Filename& lineFilename = filenames[line.filenameIndex];
|
||||
|
||||
const char* filename = namesStream.GetFilename(lineFilename.namesFilenameOffset);
|
||||
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(section.index, section.offset);
|
||||
|
||||
// only print filename for a line if it is different from the previous one.
|
||||
if (filename != prevFilename)
|
||||
{
|
||||
for (size_t i = 0, j = 0; i < lineFilename.checksumSize; i++, j+=2)
|
||||
{
|
||||
checksumString[j] = hexChars[lineFilename.checksum[i] >> 4];
|
||||
checksumString[j+1] = hexChars[lineFilename.checksum[i] & 0xF];
|
||||
}
|
||||
|
||||
checksumString[lineFilename.checksumSize * 2] = '\0';
|
||||
|
||||
printf(" line %u at [0x%08X][0x%04X:0x%08X], len = 0x%X %s (0x%02X: %s)\n",
|
||||
line.lineNumber, rva, section.index, section.offset, line.codeSize,
|
||||
filename, static_cast<uint32_t>(lineFilename.checksumKind), checksumString);
|
||||
|
||||
prevFilename = filename;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf(" line %u at [0x%08X][0x%04X:0x%08X], len = 0x%X\n",
|
||||
line.lineNumber, rva, section.index, section.offset, line.codeSize);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
200
third_party/raw_pdb/src/Examples/ExampleMain.cpp
vendored
Normal file
200
third_party/raw_pdb/src/Examples/ExampleMain.cpp
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleMemoryMappedFile.h"
|
||||
#include "PDB.h"
|
||||
#include "PDB_RawFile.h"
|
||||
#include "PDB_InfoStream.h"
|
||||
#include "PDB_DBIStream.h"
|
||||
#include "PDB_TPIStream.h"
|
||||
#include "PDB_IPIStream.h"
|
||||
#include "PDB_NamesStream.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
PDB_NO_DISCARD static bool IsError(PDB::ErrorCode errorCode)
|
||||
{
|
||||
switch (errorCode)
|
||||
{
|
||||
case PDB::ErrorCode::Success:
|
||||
return false;
|
||||
|
||||
case PDB::ErrorCode::InvalidSuperBlock:
|
||||
printf("Invalid Superblock\n");
|
||||
return true;
|
||||
|
||||
case PDB::ErrorCode::InvalidFreeBlockMap:
|
||||
printf("Invalid free block map\n");
|
||||
return true;
|
||||
|
||||
case PDB::ErrorCode::InvalidStream:
|
||||
printf("Invalid stream\n");
|
||||
return true;
|
||||
|
||||
case PDB::ErrorCode::InvalidSignature:
|
||||
printf("Invalid stream signature\n");
|
||||
return true;
|
||||
|
||||
case PDB::ErrorCode::InvalidStreamIndex:
|
||||
printf("Invalid stream index\n");
|
||||
return true;
|
||||
|
||||
case PDB::ErrorCode::InvalidDataSize:
|
||||
printf("Invalid data size\n");
|
||||
return true;
|
||||
|
||||
case PDB::ErrorCode::UnknownVersion:
|
||||
printf("Unknown version\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
// only ErrorCode::Success means there wasn't an error, so all other paths have to assume there was an error
|
||||
return true;
|
||||
}
|
||||
|
||||
PDB_NO_DISCARD static bool HasValidDBIStreams(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream)
|
||||
{
|
||||
// check whether the DBI stream offers all sub-streams we need
|
||||
if (IsError(dbiStream.HasValidSymbolRecordStream(rawPdbFile)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsError(dbiStream.HasValidPublicSymbolStream(rawPdbFile)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsError(dbiStream.HasValidGlobalSymbolStream(rawPdbFile)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsError(dbiStream.HasValidSectionContributionStream(rawPdbFile)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsError(dbiStream.HasValidImageSectionStream(rawPdbFile)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// declare all examples
|
||||
extern void ExamplePDBSize(const PDB::RawFile&, const PDB::DBIStream&);
|
||||
extern void ExampleTPISize(const PDB::TPIStream& tpiStream, const char* outPath);
|
||||
extern void ExampleContributions(const PDB::RawFile&, const PDB::DBIStream&);
|
||||
extern void ExampleSymbols(const PDB::RawFile&, const PDB::DBIStream&);
|
||||
extern void ExampleFunctionSymbols(const PDB::RawFile&, const PDB::DBIStream&);
|
||||
extern void ExampleFunctionVariables(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::TPIStream&);
|
||||
extern void ExampleLines(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream, const PDB::InfoStream& infoStream);
|
||||
extern void ExampleTypes(const PDB::TPIStream&);
|
||||
extern void ExampleIPI(const PDB::RawFile& rawPdbFile, const PDB::InfoStream& infoStream, const PDB::TPIStream& tpiStream, const PDB::IPIStream& ipiStream);
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc != 2)
|
||||
{
|
||||
printf("Usage: Examples <PDB path>\nError: Incorrect usage\n");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Opening PDB file %s\n", argv[1]);
|
||||
|
||||
// try to open the PDB file and check whether all the data we need is available
|
||||
MemoryMappedFile::Handle pdbFile = MemoryMappedFile::Open(argv[1]);
|
||||
if (!pdbFile.baseAddress)
|
||||
{
|
||||
printf("Cannot memory-map file %s\n", argv[1]);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (IsError(PDB::ValidateFile(pdbFile.baseAddress, pdbFile.len)))
|
||||
{
|
||||
MemoryMappedFile::Close(pdbFile);
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
const PDB::RawFile rawPdbFile = PDB::CreateRawFile(pdbFile.baseAddress);
|
||||
if (IsError(PDB::HasValidDBIStream(rawPdbFile)))
|
||||
{
|
||||
MemoryMappedFile::Close(pdbFile);
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
const PDB::InfoStream infoStream(rawPdbFile);
|
||||
if (infoStream.UsesDebugFastLink())
|
||||
{
|
||||
printf("PDB was linked using unsupported option /DEBUG:FASTLINK\n");
|
||||
|
||||
MemoryMappedFile::Close(pdbFile);
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
const auto h = infoStream.GetHeader();
|
||||
printf("Version %u, signature %u, age %u, GUID %08x-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x\n",
|
||||
static_cast<uint32_t>(h->version), h->signature, h->age,
|
||||
h->guid.Data1, h->guid.Data2, h->guid.Data3,
|
||||
h->guid.Data4[0], h->guid.Data4[1], h->guid.Data4[2], h->guid.Data4[3], h->guid.Data4[4], h->guid.Data4[5], h->guid.Data4[6], h->guid.Data4[7]);
|
||||
|
||||
const PDB::DBIStream dbiStream = PDB::CreateDBIStream(rawPdbFile);
|
||||
if (!HasValidDBIStreams(rawPdbFile, dbiStream))
|
||||
{
|
||||
MemoryMappedFile::Close(pdbFile);
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (IsError(PDB::HasValidTPIStream(rawPdbFile)))
|
||||
{
|
||||
MemoryMappedFile::Close(pdbFile);
|
||||
|
||||
return 5;
|
||||
}
|
||||
const PDB::TPIStream tpiStream = PDB::CreateTPIStream(rawPdbFile);
|
||||
|
||||
PDB::IPIStream ipiStream;
|
||||
|
||||
// It's perfectly possible that an old PDB does not have an IPI stream.
|
||||
if(infoStream.HasIPIStream())
|
||||
{
|
||||
PDB::ErrorCode error = PDB::HasValidIPIStream(rawPdbFile);
|
||||
|
||||
if (error != PDB::ErrorCode::InvalidStream && IsError(error))
|
||||
{
|
||||
MemoryMappedFile::Close(pdbFile);
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
ipiStream = PDB::CreateIPIStream(rawPdbFile);
|
||||
}
|
||||
|
||||
|
||||
// run all examples
|
||||
ExamplePDBSize(rawPdbFile, dbiStream);
|
||||
ExampleContributions(rawPdbFile, dbiStream);
|
||||
ExampleSymbols(rawPdbFile, dbiStream);
|
||||
ExampleFunctionSymbols(rawPdbFile, dbiStream);
|
||||
ExampleFunctionVariables(rawPdbFile, dbiStream, tpiStream);
|
||||
ExampleLines(rawPdbFile, dbiStream, infoStream);
|
||||
ExampleTypes(tpiStream);
|
||||
ExampleIPI(rawPdbFile, infoStream, tpiStream, ipiStream);
|
||||
// uncomment to dump type sizes to a CSV
|
||||
// ExampleTPISize(tpiStream, "output.csv");
|
||||
|
||||
MemoryMappedFile::Close(pdbFile);
|
||||
|
||||
return 0;
|
||||
}
|
||||
100
third_party/raw_pdb/src/Examples/ExampleMemoryMappedFile.cpp
vendored
Normal file
100
third_party/raw_pdb/src/Examples/ExampleMemoryMappedFile.cpp
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleMemoryMappedFile.h"
|
||||
|
||||
|
||||
MemoryMappedFile::Handle MemoryMappedFile::Open(const char* path)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
void* file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
|
||||
|
||||
if (file == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return Handle { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, nullptr, 0 };
|
||||
}
|
||||
|
||||
void* fileMapping = CreateFileMappingW(file, nullptr, PAGE_READONLY, 0, 0, nullptr);
|
||||
|
||||
if (fileMapping == nullptr)
|
||||
{
|
||||
CloseHandle(file);
|
||||
|
||||
return Handle { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, nullptr, 0 };
|
||||
}
|
||||
|
||||
void* baseAddress = MapViewOfFile(fileMapping, FILE_MAP_READ, 0, 0, 0);
|
||||
|
||||
if (baseAddress == nullptr)
|
||||
{
|
||||
CloseHandle(fileMapping);
|
||||
CloseHandle(file);
|
||||
|
||||
return Handle { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, nullptr, 0 };
|
||||
}
|
||||
|
||||
BY_HANDLE_FILE_INFORMATION fileInformation;
|
||||
const bool getInformationResult = GetFileInformationByHandle(file, &fileInformation);
|
||||
if (!getInformationResult)
|
||||
{
|
||||
UnmapViewOfFile(baseAddress);
|
||||
CloseHandle(fileMapping);
|
||||
CloseHandle(file);
|
||||
|
||||
return Handle { INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, nullptr, 0 };
|
||||
}
|
||||
|
||||
const size_t fileSizeHighBytes = static_cast<size_t>(fileInformation.nFileSizeHigh) << 32;
|
||||
const size_t fileSizeLowBytes = fileInformation.nFileSizeLow;
|
||||
const size_t fileSize = fileSizeHighBytes | fileSizeLowBytes;
|
||||
return Handle { file, fileMapping, baseAddress, fileSize };
|
||||
#else
|
||||
struct stat fileSb;
|
||||
|
||||
int file = open(path, O_RDONLY);
|
||||
|
||||
if (file == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return Handle { INVALID_HANDLE_VALUE, nullptr, 0 };
|
||||
}
|
||||
|
||||
if (fstat(file, &fileSb) == -1)
|
||||
{
|
||||
close(file);
|
||||
|
||||
return Handle { INVALID_HANDLE_VALUE, nullptr, 0 };
|
||||
}
|
||||
|
||||
void* baseAddress = mmap(nullptr, fileSb.st_size, PROT_READ, MAP_PRIVATE, file, 0);
|
||||
|
||||
if (baseAddress == MAP_FAILED)
|
||||
{
|
||||
close(file);
|
||||
|
||||
return Handle { INVALID_HANDLE_VALUE, nullptr, 0 };
|
||||
}
|
||||
|
||||
return Handle { file, baseAddress, static_cast<size_t>(fileSb.st_size) };
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void MemoryMappedFile::Close(Handle& handle)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
UnmapViewOfFile(handle.baseAddress);
|
||||
CloseHandle(handle.fileMapping);
|
||||
CloseHandle(handle.file);
|
||||
|
||||
handle.file = nullptr;
|
||||
handle.fileMapping = nullptr;
|
||||
#else
|
||||
munmap(handle.baseAddress, handle.len);
|
||||
close(handle.file);
|
||||
|
||||
handle.file = 0;
|
||||
#endif
|
||||
|
||||
handle.baseAddress = nullptr;
|
||||
}
|
||||
29
third_party/raw_pdb/src/Examples/ExampleMemoryMappedFile.h
vendored
Normal file
29
third_party/raw_pdb/src/Examples/ExampleMemoryMappedFile.h
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define INVALID_HANDLE_VALUE ((long)-1)
|
||||
#endif
|
||||
|
||||
namespace MemoryMappedFile
|
||||
{
|
||||
struct Handle
|
||||
{
|
||||
#ifdef _WIN32
|
||||
void* file;
|
||||
void* fileMapping;
|
||||
#else
|
||||
int file;
|
||||
#endif
|
||||
void* baseAddress;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
Handle Open(const char* path);
|
||||
void Close(Handle& handle);
|
||||
}
|
||||
124
third_party/raw_pdb/src/Examples/ExamplePDBSize.cpp
vendored
Normal file
124
third_party/raw_pdb/src/Examples/ExamplePDBSize.cpp
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleTimedScope.h"
|
||||
#include "PDB_RawFile.h"
|
||||
#include "PDB_DBIStream.h"
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
struct Stream
|
||||
{
|
||||
std::string name;
|
||||
uint32_t size;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
void ExamplePDBSize(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream);
|
||||
void ExamplePDBSize(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream)
|
||||
{
|
||||
TimedScope total("\nRunning example \"PDBSize\"");
|
||||
|
||||
std::vector<Stream> streams;
|
||||
|
||||
// print show general statistics
|
||||
printf("General\n");
|
||||
printf("-------\n");
|
||||
{
|
||||
const PDB::SuperBlock* superBlock = rawPdbFile.GetSuperBlock();
|
||||
printf("PDB page size (block size): %u\n", superBlock->blockSize);
|
||||
printf("PDB block count: %u\n", superBlock->blockCount);
|
||||
|
||||
const size_t rawSize = static_cast<size_t>(superBlock->blockSize) * static_cast<size_t>(superBlock->blockCount);
|
||||
printf("PDB raw size: %zu MiB (%zu GiB)\n", rawSize >> 20u, rawSize >> 30u);
|
||||
}
|
||||
|
||||
// print the sizes of all known streams
|
||||
printf("\n");
|
||||
printf("Sizes of known streams\n");
|
||||
printf("----------------------\n");
|
||||
{
|
||||
const uint32_t streamCount = rawPdbFile.GetStreamCount();
|
||||
const uint32_t tpiStreamSize = (streamCount > 2u) ? rawPdbFile.GetStreamSize(2u) : 0u;
|
||||
const uint32_t dbiStreamSize = (streamCount > 3u) ? rawPdbFile.GetStreamSize(3u) : 0u;
|
||||
const uint32_t ipiStreamSize = (streamCount > 4u) ? rawPdbFile.GetStreamSize(4u) : 0u;
|
||||
|
||||
printf("TPI stream size: %u KiB (%u MiB)\n", tpiStreamSize >> 10u, tpiStreamSize >> 20u);
|
||||
printf("DBI stream size: %u KiB (%u MiB)\n", dbiStreamSize >> 10u, dbiStreamSize >> 20u);
|
||||
printf("IPI stream size: %u KiB (%u MiB)\n", ipiStreamSize >> 10u, ipiStreamSize >> 20u);
|
||||
|
||||
streams.push_back(Stream { "TPI", tpiStreamSize });
|
||||
streams.push_back(Stream { "DBI", dbiStreamSize });
|
||||
streams.push_back(Stream { "IPI", ipiStreamSize });
|
||||
|
||||
const uint32_t globalSymbolStreamSize = rawPdbFile.GetStreamSize(dbiStream.GetHeader().globalStreamIndex);
|
||||
const uint32_t publicSymbolStreamSize = rawPdbFile.GetStreamSize(dbiStream.GetHeader().publicStreamIndex);
|
||||
const uint32_t symbolRecordStreamSize = rawPdbFile.GetStreamSize(dbiStream.GetHeader().symbolRecordStreamIndex);
|
||||
|
||||
printf("Global symbol stream size: %u KiB (%u MiB)\n", globalSymbolStreamSize >> 10u, globalSymbolStreamSize >> 20u);
|
||||
printf("Public symbol stream size: %u KiB (%u MiB)\n", publicSymbolStreamSize >> 10u, publicSymbolStreamSize >> 20u);
|
||||
printf("Symbol record stream size: %u KiB (%u MiB)\n", symbolRecordStreamSize >> 10u, symbolRecordStreamSize >> 20u);
|
||||
|
||||
streams.emplace_back(Stream { "Global", globalSymbolStreamSize });
|
||||
streams.emplace_back(Stream { "Public", publicSymbolStreamSize });
|
||||
streams.emplace_back(Stream { "Symbol", symbolRecordStreamSize });
|
||||
}
|
||||
|
||||
// print the sizes of all module streams
|
||||
printf("\n");
|
||||
printf("Sizes of module streams\n");
|
||||
printf("-----------------------\n");
|
||||
{
|
||||
const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
|
||||
const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = moduleInfoStream.GetModules();
|
||||
|
||||
for (const PDB::ModuleInfoStream::Module& module : modules)
|
||||
{
|
||||
const PDB::DBI::ModuleInfo* moduleInfo = module.GetInfo();
|
||||
const char* name = module.GetName().Decay();
|
||||
const char* objectName = module.GetObjectName().Decay();
|
||||
|
||||
const uint16_t streamIndex = module.HasSymbolStream() ? moduleInfo->moduleSymbolStreamIndex : 0u;
|
||||
const uint32_t moduleStreamSize = (streamIndex != 0u) ? rawPdbFile.GetStreamSize(streamIndex) : 0u;
|
||||
|
||||
printf("Module %s (%s) stream size: %u KiB (%u MiB)\n", name, objectName, moduleStreamSize >> 10u, moduleStreamSize >> 20u);
|
||||
|
||||
streams.push_back(Stream { name, moduleStreamSize });
|
||||
}
|
||||
}
|
||||
|
||||
// sort the streams by their size
|
||||
std::sort(streams.begin(), streams.end(), [](const Stream& lhs, const Stream& rhs)
|
||||
{
|
||||
return lhs.size > rhs.size;
|
||||
});
|
||||
|
||||
// log the 20 largest stream
|
||||
{
|
||||
printf("\n");
|
||||
printf("Sizes of 20 largest streams:\n");
|
||||
|
||||
const size_t countToShow = std::min<size_t>(20ul, streams.size());
|
||||
for (size_t i = 0u; i < countToShow; ++i)
|
||||
{
|
||||
const Stream& stream = streams[i];
|
||||
printf("%zu: %u KiB (%u MiB) from stream %s\n", i + 1u, stream.size >> 10u, stream.size >> 20u, stream.name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// print the raw stream sizes
|
||||
printf("\n");
|
||||
printf("Raw sizes of all streams\n");
|
||||
printf("------------------------\n");
|
||||
{
|
||||
const uint32_t streamCount = rawPdbFile.GetStreamCount();
|
||||
for (uint32_t i = 0u; i < streamCount; ++i)
|
||||
{
|
||||
const uint32_t streamSize = rawPdbFile.GetStreamSize(i);
|
||||
printf("Stream %u size: %u KiB (%u MiB)\n", i, streamSize >> 10u, streamSize >> 20u);
|
||||
}
|
||||
}
|
||||
}
|
||||
238
third_party/raw_pdb/src/Examples/ExampleSymbols.cpp
vendored
Normal file
238
third_party/raw_pdb/src/Examples/ExampleSymbols.cpp
vendored
Normal file
@@ -0,0 +1,238 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleTimedScope.h"
|
||||
#include "PDB_RawFile.h"
|
||||
#include "PDB_DBIStream.h"
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
// we don't have to store std::string in the symbols, since all the data is memory-mapped anyway.
|
||||
// we do it in this example to ensure that we don't "cheat" when reading the PDB file. memory-mapped data will only
|
||||
// be faulted into the process once it's touched, so actually copying the string data makes us touch the needed data,
|
||||
// giving us a real performance measurement.
|
||||
struct Symbol
|
||||
{
|
||||
std::string name;
|
||||
uint32_t rva;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
void ExampleSymbols(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream);
|
||||
void ExampleSymbols(const PDB::RawFile& rawPdbFile, const PDB::DBIStream& dbiStream)
|
||||
{
|
||||
TimedScope total("\nRunning example \"Symbols\"");
|
||||
|
||||
// in order to keep the example easy to understand, we load the PDB data serially.
|
||||
// note that this can be improved a lot by reading streams concurrently.
|
||||
|
||||
// prepare the image section stream first. it is needed for converting section + offset into an RVA
|
||||
TimedScope sectionScope("Reading image section stream");
|
||||
const PDB::ImageSectionStream imageSectionStream = dbiStream.CreateImageSectionStream(rawPdbFile);
|
||||
sectionScope.Done();
|
||||
|
||||
|
||||
// prepare the module info stream for matching contributions against files
|
||||
TimedScope moduleScope("Reading module info stream");
|
||||
const PDB::ModuleInfoStream moduleInfoStream = dbiStream.CreateModuleInfoStream(rawPdbFile);
|
||||
moduleScope.Done();
|
||||
|
||||
|
||||
// prepare symbol record stream needed by both public and global streams
|
||||
TimedScope symbolStreamScope("Reading symbol record stream");
|
||||
const PDB::CoalescedMSFStream symbolRecordStream = dbiStream.CreateSymbolRecordStream(rawPdbFile);
|
||||
symbolStreamScope.Done();
|
||||
|
||||
std::vector<Symbol> symbols;
|
||||
|
||||
// read public symbols
|
||||
TimedScope publicScope("Reading public symbol stream");
|
||||
const PDB::PublicSymbolStream publicSymbolStream = dbiStream.CreatePublicSymbolStream(rawPdbFile);
|
||||
publicScope.Done();
|
||||
{
|
||||
TimedScope scope("Storing public symbols");
|
||||
|
||||
const PDB::ArrayView<PDB::HashRecord> hashRecords = publicSymbolStream.GetRecords();
|
||||
const size_t count = hashRecords.GetLength();
|
||||
|
||||
symbols.reserve(count);
|
||||
|
||||
for (const PDB::HashRecord& hashRecord : hashRecords)
|
||||
{
|
||||
const PDB::CodeView::DBI::Record* record = publicSymbolStream.GetRecord(symbolRecordStream, hashRecord);
|
||||
if (record->header.kind != PDB::CodeView::DBI::SymbolRecordKind::S_PUB32)
|
||||
{
|
||||
// normally, a PDB only contains S_PUB32 symbols in the public symbol stream, but we have seen PDBs that also store S_CONSTANT as public symbols.
|
||||
// ignore these.
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_PUB32.section, record->data.S_PUB32.offset);
|
||||
if (rva == 0u)
|
||||
{
|
||||
// certain symbols (e.g. control-flow guard symbols) don't have a valid RVA, ignore those
|
||||
continue;
|
||||
}
|
||||
|
||||
symbols.push_back(Symbol { record->data.S_PUB32.name, rva });
|
||||
}
|
||||
|
||||
scope.Done(count);
|
||||
}
|
||||
|
||||
|
||||
// read global symbols
|
||||
TimedScope globalScope("Reading global symbol stream");
|
||||
const PDB::GlobalSymbolStream globalSymbolStream = dbiStream.CreateGlobalSymbolStream(rawPdbFile);
|
||||
globalScope.Done();
|
||||
{
|
||||
TimedScope scope("Storing global symbols");
|
||||
|
||||
const PDB::ArrayView<PDB::HashRecord> hashRecords = globalSymbolStream.GetRecords();
|
||||
const size_t count = hashRecords.GetLength();
|
||||
|
||||
symbols.reserve(symbols.size() + count);
|
||||
|
||||
for (const PDB::HashRecord& hashRecord : hashRecords)
|
||||
{
|
||||
const PDB::CodeView::DBI::Record* record = globalSymbolStream.GetRecord(symbolRecordStream, hashRecord);
|
||||
|
||||
const char* name = nullptr;
|
||||
uint32_t rva = 0u;
|
||||
if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32)
|
||||
{
|
||||
name = record->data.S_GDATA32.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GDATA32.section, record->data.S_GDATA32.offset);
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GTHREAD32)
|
||||
{
|
||||
name = record->data.S_GTHREAD32.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GTHREAD32.section, record->data.S_GTHREAD32.offset);
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32)
|
||||
{
|
||||
name = record->data.S_LDATA32.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LDATA32.section, record->data.S_LDATA32.offset);
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LTHREAD32)
|
||||
{
|
||||
name = record->data.S_LTHREAD32.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LTHREAD32.section, record->data.S_LTHREAD32.offset);
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_UDT)
|
||||
{
|
||||
name = record->data.S_UDT.name;
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_UDT_ST)
|
||||
{
|
||||
name = record->data.S_UDT_ST.name;
|
||||
}
|
||||
|
||||
if (rva == 0u)
|
||||
{
|
||||
// certain symbols (e.g. control-flow guard symbols) don't have a valid RVA, ignore those
|
||||
continue;
|
||||
}
|
||||
|
||||
symbols.push_back(Symbol { name, rva });
|
||||
}
|
||||
|
||||
scope.Done(count);
|
||||
}
|
||||
|
||||
|
||||
// read module symbols
|
||||
{
|
||||
TimedScope scope("Storing symbols from modules");
|
||||
|
||||
const PDB::ArrayView<PDB::ModuleInfoStream::Module> modules = moduleInfoStream.GetModules();
|
||||
|
||||
for (const PDB::ModuleInfoStream::Module& module : modules)
|
||||
{
|
||||
if (!module.HasSymbolStream())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const PDB::ModuleSymbolStream moduleSymbolStream = module.CreateSymbolStream(rawPdbFile);
|
||||
moduleSymbolStream.ForEachSymbol([&symbols, &imageSectionStream](const PDB::CodeView::DBI::Record* record)
|
||||
{
|
||||
const char* name = nullptr;
|
||||
uint32_t rva = 0u;
|
||||
if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_THUNK32)
|
||||
{
|
||||
if (record->data.S_THUNK32.thunk == PDB::CodeView::DBI::ThunkOrdinal::TrampolineIncremental)
|
||||
{
|
||||
// we have never seen incremental linking thunks stored inside a S_THUNK32 symbol, but better be safe than sorry
|
||||
name = "ILT";
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_THUNK32.section, record->data.S_THUNK32.offset);
|
||||
}
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_TRAMPOLINE)
|
||||
{
|
||||
// incremental linking thunks are stored in the linker module
|
||||
name = "ILT";
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_TRAMPOLINE.thunkSection, record->data.S_TRAMPOLINE.thunkOffset);
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_BLOCK32)
|
||||
{
|
||||
// blocks never store a name and are only stored for indicating whether other symbols are children of this block
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LABEL32)
|
||||
{
|
||||
// labels don't have a name
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32)
|
||||
{
|
||||
name = record->data.S_LPROC32.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LPROC32.section, record->data.S_LPROC32.offset);
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32)
|
||||
{
|
||||
name = record->data.S_GPROC32.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GPROC32.section, record->data.S_GPROC32.offset);
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32_ID)
|
||||
{
|
||||
name = record->data.S_LPROC32_ID.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LPROC32_ID.section, record->data.S_LPROC32_ID.offset);
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32_ID)
|
||||
{
|
||||
name = record->data.S_GPROC32_ID.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_GPROC32_ID.section, record->data.S_GPROC32_ID.offset);
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_REGREL32)
|
||||
{
|
||||
name = record->data.S_REGREL32.name;
|
||||
// You can only get the address while running the program by checking the register value and adding the offset
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32)
|
||||
{
|
||||
name = record->data.S_LDATA32.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LDATA32.section, record->data.S_LDATA32.offset);
|
||||
}
|
||||
else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LTHREAD32)
|
||||
{
|
||||
name = record->data.S_LTHREAD32.name;
|
||||
rva = imageSectionStream.ConvertSectionOffsetToRVA(record->data.S_LTHREAD32.section, record->data.S_LTHREAD32.offset);
|
||||
}
|
||||
|
||||
if (rva == 0u)
|
||||
{
|
||||
// certain symbols (e.g. control-flow guard symbols) don't have a valid RVA, ignore those
|
||||
return;
|
||||
}
|
||||
|
||||
symbols.push_back(Symbol { name, rva });
|
||||
});
|
||||
}
|
||||
|
||||
scope.Done(modules.GetLength());
|
||||
}
|
||||
|
||||
total.Done(symbols.size());
|
||||
}
|
||||
54
third_party/raw_pdb/src/Examples/ExampleTimedScope.cpp
vendored
Normal file
54
third_party/raw_pdb/src/Examples/ExampleTimedScope.cpp
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleTimedScope.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
static unsigned int g_indent = 0u;
|
||||
|
||||
static void PrintIndent(void)
|
||||
{
|
||||
printf("%.*s", g_indent * 2u, "| | | | | | | | ");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TimedScope::TimedScope(const char* message)
|
||||
: m_begin(std::chrono::high_resolution_clock::now())
|
||||
{
|
||||
PrintIndent();
|
||||
++g_indent;
|
||||
|
||||
printf("%s\n", message);
|
||||
}
|
||||
|
||||
|
||||
void TimedScope::Done(void) const
|
||||
{
|
||||
--g_indent;
|
||||
PrintIndent();
|
||||
|
||||
const double milliSeconds = ReadMilliseconds();
|
||||
printf("---> done in %.3fms\n", milliSeconds);
|
||||
}
|
||||
|
||||
|
||||
void TimedScope::Done(size_t count) const
|
||||
{
|
||||
--g_indent;
|
||||
PrintIndent();
|
||||
|
||||
const double milliSeconds = ReadMilliseconds();
|
||||
printf("---> done in %.3fms (%zu elements)\n", milliSeconds, count);
|
||||
}
|
||||
|
||||
|
||||
double TimedScope::ReadMilliseconds(void) const
|
||||
{
|
||||
const std::chrono::high_resolution_clock::time_point now = std::chrono::high_resolution_clock::now();
|
||||
const std::chrono::duration<double> seconds = now - m_begin;
|
||||
|
||||
return seconds.count() * 1000.0;
|
||||
}
|
||||
22
third_party/raw_pdb/src/Examples/ExampleTimedScope.h
vendored
Normal file
22
third_party/raw_pdb/src/Examples/ExampleTimedScope.h
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Foundation/PDB_Macros.h"
|
||||
#include <chrono>
|
||||
|
||||
|
||||
class TimedScope
|
||||
{
|
||||
public:
|
||||
explicit TimedScope(const char* message);
|
||||
|
||||
void Done(void) const;
|
||||
void Done(size_t count) const;
|
||||
|
||||
private:
|
||||
double ReadMilliseconds(void) const;
|
||||
|
||||
const std::chrono::high_resolution_clock::time_point m_begin;
|
||||
|
||||
PDB_DISABLE_COPY_MOVE(TimedScope);
|
||||
};
|
||||
41
third_party/raw_pdb/src/Examples/ExampleTypeTable.cpp
vendored
Normal file
41
third_party/raw_pdb/src/Examples/ExampleTypeTable.cpp
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
#include "ExampleTypeTable.h"
|
||||
#include "Foundation/PDB_Memory.h"
|
||||
|
||||
TypeTable::TypeTable(const PDB::TPIStream& tpiStream) PDB_NO_EXCEPT
|
||||
: typeIndexBegin(tpiStream.GetFirstTypeIndex()), typeIndexEnd(tpiStream.GetLastTypeIndex()),
|
||||
m_recordCount(tpiStream.GetTypeRecordCount())
|
||||
{
|
||||
// Create coalesced stream from TPI stream, so the records can be referenced directly using pointers.
|
||||
const PDB::DirectMSFStream& directStream = tpiStream.GetDirectMSFStream();
|
||||
m_stream = PDB::CoalescedMSFStream(directStream, directStream.GetSize(), 0);
|
||||
|
||||
// types in the TPI stream are accessed by their index from other streams.
|
||||
// however, the index is not stored with types in the TPI stream directly, but has to be built while walking the stream.
|
||||
// similarly, because types are variable-length records, there are no direct offsets to access individual types.
|
||||
// we therefore walk the TPI stream once, and store pointers to the records for trivial O(1) array lookup by index later.
|
||||
m_records = PDB_NEW_ARRAY(const PDB::CodeView::TPI::Record*, m_recordCount);
|
||||
|
||||
// parse the CodeView records
|
||||
uint32_t typeIndex = 0u;
|
||||
|
||||
tpiStream.ForEachTypeRecordHeaderAndOffset([this, &typeIndex](const PDB::CodeView::TPI::RecordHeader& header, size_t offset)
|
||||
{
|
||||
// The header includes the record kind and size, which can be stored along with offset
|
||||
// to allow for lazy loading of the types on-demand directly from the TPIStream::GetDirectMSFStream()
|
||||
// using DirectMSFStream::ReadAtOffset(...). Thus not needing a CoalescedMSFStream to look up the types.
|
||||
(void)header;
|
||||
|
||||
const PDB::CodeView::TPI::Record* record = m_stream.GetDataAtOffset<const PDB::CodeView::TPI::Record>(offset);
|
||||
m_records[typeIndex] = record;
|
||||
++typeIndex;
|
||||
});
|
||||
}
|
||||
|
||||
TypeTable::~TypeTable() PDB_NO_EXCEPT
|
||||
{
|
||||
PDB_DELETE_ARRAY(m_records);
|
||||
}
|
||||
49
third_party/raw_pdb/src/Examples/ExampleTypeTable.h
vendored
Normal file
49
third_party/raw_pdb/src/Examples/ExampleTypeTable.h
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "PDB_TPIStream.h"
|
||||
#include "PDB_CoalescedMSFStream.h"
|
||||
|
||||
class TypeTable
|
||||
{
|
||||
public:
|
||||
explicit TypeTable(const PDB::TPIStream& tpiStream) PDB_NO_EXCEPT;
|
||||
~TypeTable() PDB_NO_EXCEPT;
|
||||
|
||||
// Returns the index of the first type, which is not necessarily zero.
|
||||
PDB_NO_DISCARD inline uint32_t GetFirstTypeIndex(void) const PDB_NO_EXCEPT
|
||||
{
|
||||
return typeIndexBegin;
|
||||
}
|
||||
|
||||
// Returns the index of the last type.
|
||||
PDB_NO_DISCARD inline uint32_t GetLastTypeIndex(void) const PDB_NO_EXCEPT
|
||||
{
|
||||
return typeIndexEnd;
|
||||
}
|
||||
|
||||
PDB_NO_DISCARD inline const PDB::CodeView::TPI::Record* GetTypeRecord(uint32_t typeIndex) const PDB_NO_EXCEPT
|
||||
{
|
||||
if (typeIndex < typeIndexBegin || typeIndex > typeIndexEnd)
|
||||
return nullptr;
|
||||
|
||||
return m_records[typeIndex - typeIndexBegin];
|
||||
}
|
||||
|
||||
// Returns a view of all type records.
|
||||
// Records identified by a type index can be accessed via "allRecords[typeIndex - firstTypeIndex]".
|
||||
PDB_NO_DISCARD inline PDB::ArrayView<const PDB::CodeView::TPI::Record*> GetTypeRecords(void) const PDB_NO_EXCEPT
|
||||
{
|
||||
return PDB::ArrayView<const PDB::CodeView::TPI::Record*>(m_records, m_recordCount);
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t typeIndexBegin;
|
||||
uint32_t typeIndexEnd;
|
||||
|
||||
size_t m_recordCount;
|
||||
const PDB::CodeView::TPI::Record **m_records;
|
||||
|
||||
PDB::CoalescedMSFStream m_stream;
|
||||
|
||||
PDB_DISABLE_COPY(TypeTable);
|
||||
};
|
||||
1418
third_party/raw_pdb/src/Examples/ExampleTypes.cpp
vendored
Normal file
1418
third_party/raw_pdb/src/Examples/ExampleTypes.cpp
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4
third_party/raw_pdb/src/Examples/Examples_PCH.cpp
vendored
Normal file
4
third_party/raw_pdb/src/Examples/Examples_PCH.cpp
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#include "Examples_PCH.h"
|
||||
53
third_party/raw_pdb/src/Examples/Examples_PCH.h
vendored
Normal file
53
third_party/raw_pdb/src/Examples/Examples_PCH.h
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Foundation/PDB_Warnings.h"
|
||||
|
||||
// The following clang warnings must be disabled for the examples to build with 0 warnings
|
||||
#if PDB_COMPILER_CLANG
|
||||
# pragma clang diagnostic ignored "-Wformat-nonliteral" // format string is not a string literal
|
||||
# pragma clang diagnostic ignored "-Wswitch-default" // switch' missing 'default' label
|
||||
# pragma clang diagnostic ignored "-Wcast-align" // increases required alignment from X to Y
|
||||
# pragma clang diagnostic ignored "-Wold-style-cast" // use of old-style cast
|
||||
#endif
|
||||
|
||||
#if PDB_COMPILER_MSVC
|
||||
# pragma warning(push, 0)
|
||||
#elif PDB_COMPILER_CLANG
|
||||
# pragma clang diagnostic push
|
||||
#endif
|
||||
|
||||
#if PDB_COMPILER_MSVC
|
||||
// we compile without exceptions
|
||||
# define _ALLOW_RTCc_IN_STL
|
||||
|
||||
// triggered by Windows.h
|
||||
# pragma warning (disable : 4668)
|
||||
|
||||
// triggered by xlocale in VS 2017
|
||||
# pragma warning (disable : 4625) // copy constructor was implicitly defined as deleted
|
||||
# pragma warning (disable : 4626) // assignment operator was implicitly defined as deleted
|
||||
# pragma warning (disable : 5026) // move constructor was implicitly defined as deleted
|
||||
# pragma warning (disable : 5027) // move assignment operator was implicitly defined as deleted
|
||||
# pragma warning (disable : 4774) // format string expected in argument 1 is not a string literal
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
# define NOMINMAX
|
||||
# include <Windows.h>
|
||||
# undef cdecl
|
||||
#endif
|
||||
# include <vector>
|
||||
# include <unordered_set>
|
||||
# include <chrono>
|
||||
# include <string>
|
||||
# include <algorithm>
|
||||
# include <cstdarg>
|
||||
|
||||
#if PDB_COMPILER_MSVC
|
||||
# pragma warning(pop)
|
||||
#elif PDB_COMPILER_CLANG
|
||||
# pragma clang diagnostic pop
|
||||
#endif
|
||||
68
third_party/raw_pdb/src/Foundation/PDB_ArrayView.h
vendored
Normal file
68
third_party/raw_pdb/src/Foundation/PDB_ArrayView.h
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "PDB_Macros.h"
|
||||
#include "PDB_Assert.h"
|
||||
|
||||
|
||||
namespace PDB
|
||||
{
|
||||
// A read-only view into arrays of any type and length.
|
||||
template <typename T>
|
||||
class PDB_NO_DISCARD ArrayView
|
||||
{
|
||||
public:
|
||||
// Constructs an array view from a C array with explicit length.
|
||||
inline constexpr explicit ArrayView(const T* const array, size_t length) PDB_NO_EXCEPT
|
||||
: m_data(array)
|
||||
, m_length(length)
|
||||
{
|
||||
}
|
||||
|
||||
PDB_DEFAULT_COPY_CONSTRUCTOR(ArrayView);
|
||||
PDB_DEFAULT_MOVE_CONSTRUCTOR(ArrayView);
|
||||
|
||||
// Provides read-only access to the underlying array.
|
||||
PDB_NO_DISCARD inline constexpr const T* Decay(void) const PDB_NO_EXCEPT
|
||||
{
|
||||
return m_data;
|
||||
}
|
||||
|
||||
// Returns the length of the view.
|
||||
PDB_NO_DISCARD inline constexpr size_t GetLength(void) const PDB_NO_EXCEPT
|
||||
{
|
||||
return m_length;
|
||||
}
|
||||
|
||||
// Returns the i-th element.
|
||||
PDB_NO_DISCARD inline const T& operator[](size_t i) const PDB_NO_EXCEPT
|
||||
{
|
||||
PDB_ASSERT(i < GetLength(), "Index %zu out of bounds [0, %zu).", i, GetLength());
|
||||
return m_data[i];
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// Range-based for-loop support
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
PDB_NO_DISCARD inline const T* begin(void) const PDB_NO_EXCEPT
|
||||
{
|
||||
return m_data;
|
||||
}
|
||||
|
||||
PDB_NO_DISCARD inline const T* end(void) const PDB_NO_EXCEPT
|
||||
{
|
||||
return m_data + m_length;
|
||||
}
|
||||
|
||||
private:
|
||||
const T* const m_data;
|
||||
const size_t m_length;
|
||||
|
||||
PDB_DISABLE_MOVE_ASSIGNMENT(ArrayView);
|
||||
PDB_DISABLE_COPY_ASSIGNMENT(ArrayView);
|
||||
};
|
||||
}
|
||||
31
third_party/raw_pdb/src/Foundation/PDB_Assert.h
vendored
Normal file
31
third_party/raw_pdb/src/Foundation/PDB_Assert.h
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "PDB_Macros.h"
|
||||
#include "PDB_Log.h"
|
||||
|
||||
|
||||
PDB_PUSH_WARNING_CLANG
|
||||
PDB_DISABLE_WARNING_CLANG("-Wgnu-zero-variadic-macro-arguments")
|
||||
PDB_DISABLE_WARNING_CLANG("-Wreserved-identifier")
|
||||
|
||||
#if PDB_COMPILER_MSVC
|
||||
extern "C" void __cdecl __debugbreak(void);
|
||||
# pragma intrinsic(__debugbreak)
|
||||
#elif defined(__has_builtin) && __has_builtin(__builtin_debugtrap)
|
||||
# define __debugbreak() __builtin_debugtrap()
|
||||
#else
|
||||
# include <signal.h>
|
||||
# define __debugbreak() raise(SIGTRAP)
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef _DEBUG
|
||||
# define PDB_ASSERT(_condition, _msg, ...) (_condition) ? (void)true : (PDB_LOG_ERROR(_msg, ##__VA_ARGS__), __debugbreak())
|
||||
#else
|
||||
# define PDB_ASSERT(_condition, _msg, ...) PDB_NOOP(_condition, _msg, ##__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
PDB_POP_WARNING_CLANG
|
||||
23
third_party/raw_pdb/src/Foundation/PDB_BitOperators.h
vendored
Normal file
23
third_party/raw_pdb/src/Foundation/PDB_BitOperators.h
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "PDB_Macros.h"
|
||||
|
||||
|
||||
#define PDB_DEFINE_BIT_OPERATORS(_type) \
|
||||
PDB_NO_DISCARD inline constexpr _type operator|(_type lhs, _type rhs) PDB_NO_EXCEPT \
|
||||
{ \
|
||||
return static_cast<_type>(PDB_AS_UNDERLYING(lhs) | PDB_AS_UNDERLYING(rhs)); \
|
||||
} \
|
||||
\
|
||||
PDB_NO_DISCARD inline constexpr _type operator&(_type lhs, _type rhs) PDB_NO_EXCEPT \
|
||||
{ \
|
||||
return static_cast<_type>(PDB_AS_UNDERLYING(lhs) & PDB_AS_UNDERLYING(rhs)); \
|
||||
} \
|
||||
\
|
||||
PDB_NO_DISCARD inline constexpr _type operator~(_type value) PDB_NO_EXCEPT \
|
||||
{ \
|
||||
return static_cast<_type>(~PDB_AS_UNDERLYING(value)); \
|
||||
}
|
||||
73
third_party/raw_pdb/src/Foundation/PDB_BitUtil.h
vendored
Normal file
73
third_party/raw_pdb/src/Foundation/PDB_BitUtil.h
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2011-2022, Molecular Matters GmbH <office@molecular-matters.com>
|
||||
// See LICENSE.txt for licensing details (2-clause BSD License: https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "PDB_Assert.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
PDB_PUSH_WARNING_CLANG
|
||||
PDB_DISABLE_WARNING_CLANG("-Wreserved-identifier")
|
||||
|
||||
extern "C" unsigned char _BitScanForward(unsigned long* _Index, unsigned long _Mask);
|
||||
|
||||
PDB_POP_WARNING_CLANG
|
||||
|
||||
# if PDB_COMPILER_MSVC
|
||||
# pragma intrinsic(_BitScanForward)
|
||||
# endif
|
||||
#endif
|
||||
|
||||
|
||||
namespace PDB
|
||||
{
|
||||
namespace BitUtil
|
||||
{
|
||||
// Returns whether the given unsigned value is a power of two.
|
||||
template <typename T>
|
||||
PDB_NO_DISCARD inline constexpr bool IsPowerOfTwo(T value) PDB_NO_EXCEPT
|
||||
{
|
||||
PDB_ASSERT(value != 0u, "Invalid value.");
|
||||
|
||||
return (value & (value - 1u)) == 0u;
|
||||
}
|
||||
|
||||
|
||||
// Rounds the given unsigned value up to the next multiple.
|
||||
template <typename T>
|
||||
PDB_NO_DISCARD inline constexpr T RoundUpToMultiple(T numToRound, T multipleOf) PDB_NO_EXCEPT
|
||||
{
|
||||
PDB_ASSERT(IsPowerOfTwo(multipleOf), "Multiple must be a power-of-two.");
|
||||
|
||||
return (numToRound + (multipleOf - 1u)) & ~(multipleOf - 1u);
|
||||
}
|
||||
|
||||
|
||||
// Finds the position of the first set bit in the given value starting from the LSB, e.g. FindFirstSetBit(0b00000010) == 1.
|
||||
// This operation is also known as CTZ (Count Trailing Zeros).
|
||||
template <typename T>
|
||||
PDB_NO_DISCARD inline uint32_t FindFirstSetBit(T value) PDB_NO_EXCEPT;
|
||||
|
||||
template <>
|
||||
PDB_NO_DISCARD inline uint32_t FindFirstSetBit(uint32_t value) PDB_NO_EXCEPT
|
||||
{
|
||||
PDB_ASSERT(value != 0u, "Invalid value.");
|
||||
|
||||
#ifdef _WIN32
|
||||
unsigned long result = 0ul;
|
||||
|
||||
_BitScanForward(&result, value);
|
||||
#else
|
||||
unsigned int result = 0u;
|
||||
|
||||
result = static_cast<unsigned int>(__builtin_ffs(static_cast<int>(value)));
|
||||
if (result)
|
||||
{
|
||||
--result;
|
||||
}
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user