mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
55 Commits
v2027.02.1
...
snapshot-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acc3ebf5db | ||
|
|
26217f5de8 | ||
|
|
fa0d9a377b | ||
|
|
b1d3e52204 | ||
|
|
1cccd320b0 | ||
|
|
5b6e0473cb | ||
|
|
57d55456a8 | ||
|
|
bb466516ba | ||
|
|
444ba34fa3 | ||
|
|
91633169a0 | ||
|
|
f041761b62 | ||
|
|
1c3b4af045 | ||
|
|
5ae9ca0979 | ||
|
|
e064646c02 | ||
|
|
c6c56ffaee | ||
|
|
aba8e5cac9 | ||
|
|
3a5d03fae0 | ||
|
|
df79da54e3 | ||
|
|
e3ff4dfe71 | ||
|
|
735e4ea9f7 | ||
|
|
d937d2f42e | ||
|
|
3685530287 | ||
|
|
9e90f66ca0 | ||
|
|
f53fa84a15 | ||
|
|
13e28e8791 | ||
|
|
079b3121ce | ||
|
|
5e40349768 | ||
|
|
8dd6110ec6 | ||
|
|
eb27fc7988 | ||
|
|
85994d68b9 | ||
|
|
55dc5d5875 | ||
|
|
3a92336132 | ||
|
|
f9b33f2ba7 | ||
|
|
f2dab07870 | ||
|
|
9d22a5ed69 | ||
|
|
193ab81ecf | ||
|
|
aa0840b332 | ||
|
|
f3631f17ff | ||
|
|
42e9bde7ba | ||
|
|
07fedf0ae8 | ||
|
|
2e02a01495 | ||
|
|
71bc51cbab | ||
|
|
60a97ab81b | ||
|
|
bb00e75019 | ||
|
|
c038c59e34 | ||
|
|
862f76b984 | ||
|
|
818285a76e | ||
|
|
ef5e2ebdb9 | ||
|
|
75fedd2222 | ||
|
|
389745e501 | ||
|
|
1473a58742 | ||
|
|
4192a4dad3 | ||
|
|
4c6bb9564f | ||
|
|
0ef9841f90 | ||
|
|
0a8244dad4 |
191
.github/workflows/build.yml
vendored
Normal file
191
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Qt6
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
arch: 'win64_msvc2022_64'
|
||||
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: |
|
||||
mkdir -p release
|
||||
cp build/Reclass.exe release/
|
||||
cp build/ReclassMcpBridge.exe release/
|
||||
cp build/*.dll release/ 2>/dev/null || true
|
||||
cp -r build/platforms release/ 2>/dev/null || true
|
||||
cp -r build/styles release/ 2>/dev/null || true
|
||||
cp -r build/imageformats release/ 2>/dev/null || true
|
||||
cp -r build/iconengines release/ 2>/dev/null || true
|
||||
mkdir -p release/Plugins
|
||||
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
|
||||
cp -r build/themes release/ 2>/dev/null || true
|
||||
cp -r build/examples release/ 2>/dev/null || true
|
||||
cp build/screenshot.png release/ 2>/dev/null || true
|
||||
cd release && 7z a ../Reclass-win64-qt6.zip *
|
||||
|
||||
- name: Upload release asset
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: snapshot-${{ steps.date.outputs.tag }}
|
||||
name: Snapshot ${{ steps.date.outputs.tag }}
|
||||
body: |
|
||||
Automated snapshot from main branch.
|
||||
Commit: ${{ github.sha }}
|
||||
prerelease: false
|
||||
files: Reclass-win64-qt6.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
linux:
|
||||
needs: windows
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Qt6
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: '6.8.1'
|
||||
cache: true
|
||||
aqtversion: '==3.1.21'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ninja-build libgl1-mesa-dev libfuse2 libxcb-cursor0
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- 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
|
||||
|
||||
- name: Create AppImage
|
||||
run: |
|
||||
# Download linuxdeploy and Qt plugin
|
||||
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
|
||||
chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage
|
||||
|
||||
# Build AppDir structure
|
||||
mkdir -p AppDir/usr/bin AppDir/usr/share/icons/hicolor/256x256/apps
|
||||
cp build/Reclass AppDir/usr/bin/
|
||||
cp build/ReclassMcpBridge AppDir/usr/bin/
|
||||
cp -r build/themes AppDir/usr/bin/ 2>/dev/null || true
|
||||
cp -r build/examples AppDir/usr/bin/ 2>/dev/null || true
|
||||
mkdir -p AppDir/usr/bin/Plugins
|
||||
cp build/Plugins/*.so AppDir/usr/bin/Plugins/ 2>/dev/null || true
|
||||
cp src/icons/class.png AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png
|
||||
|
||||
# Create AppImage with Qt libs bundled
|
||||
# install-qt-action adds Qt bin to PATH; find qmake there
|
||||
QMAKE_BIN=$(which qmake 2>/dev/null || which qmake6 2>/dev/null || find "$RUNNER_WORKSPACE" -name qmake -path "*/bin/*" | head -1)
|
||||
echo "Found qmake at: $QMAKE_BIN"
|
||||
export QMAKE="$QMAKE_BIN"
|
||||
QT_ROOT=$(dirname "$(dirname "$QMAKE_BIN")")
|
||||
export LD_LIBRARY_PATH="$QT_ROOT/lib:$LD_LIBRARY_PATH"
|
||||
export EXTRA_QT_PLUGINS="svg;iconengines"
|
||||
./linuxdeploy-x86_64.AppImage --appdir AppDir \
|
||||
--desktop-file deploy/Reclass.desktop \
|
||||
--icon-file AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png \
|
||||
--plugin qt \
|
||||
--output appimage
|
||||
# Rename to final name
|
||||
ls Reclass-*.AppImage
|
||||
mv Reclass-*.AppImage Reclass-linux64-qt6.AppImage
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: Reclass-linux64-qt6
|
||||
path: Reclass-linux64-qt6.AppImage
|
||||
|
||||
- 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'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: snapshot-${{ steps.date.outputs.tag }}
|
||||
name: Snapshot ${{ steps.date.outputs.tag }}
|
||||
body: |
|
||||
Automated snapshot from main branch.
|
||||
Commit: ${{ github.sha }}
|
||||
prerelease: false
|
||||
files: Reclass-linux64-qt6.AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
150
CMakeLists.txt
150
CMakeLists.txt
@@ -1,8 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(Reclass VERSION 0.1 LANGUAGES CXX)
|
||||
project(Reclass VERSION 0.1 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
@@ -59,15 +60,27 @@ 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/mainwindow.h
|
||||
src/optionsdialog.h
|
||||
src/optionsdialog.cpp
|
||||
src/titlebar.h
|
||||
src/titlebar.cpp
|
||||
src/mcp/mcp_bridge.h
|
||||
src/mcp/mcp_bridge.cpp
|
||||
src/disasm.h
|
||||
src/disasm.cpp
|
||||
third_party/fadec/decode.c
|
||||
third_party/fadec/format.c
|
||||
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
||||
)
|
||||
|
||||
target_include_directories(Reclass PRIVATE src)
|
||||
target_include_directories(Reclass PRIVATE src third_party/fadec)
|
||||
|
||||
target_link_libraries(Reclass PRIVATE
|
||||
${QT}::Widgets
|
||||
@@ -93,14 +106,16 @@ foreach(_tf ${_theme_files})
|
||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
|
||||
# Copy example .rcx files to build directory
|
||||
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
||||
foreach(_ef ${_example_files})
|
||||
get_filename_component(_name ${_ef} NAME)
|
||||
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
|
||||
include(deploy)
|
||||
|
||||
add_custom_target(screenshot ALL
|
||||
COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
|
||||
DEPENDS Reclass deploy
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
COMMENT "Capturing UI screenshot with class open..."
|
||||
)
|
||||
|
||||
set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
|
||||
file(WRITE ${_combine_script} "
|
||||
@@ -117,7 +132,7 @@ foreach(_f
|
||||
\"${CMAKE_SOURCE_DIR}/src/generator.cpp\"
|
||||
\"${CMAKE_SOURCE_DIR}/src/main.cpp\")
|
||||
file(READ \${_f} _content)
|
||||
file(APPEND \${_out} \${_content})
|
||||
file(APPEND \${_out} \"\${_content}\")
|
||||
file(APPEND \${_out} \"\\n\")
|
||||
endforeach()
|
||||
message(STATUS \"Combined sources -> \${_out}\")
|
||||
@@ -134,6 +149,11 @@ if(BUILD_TESTING)
|
||||
find_package(${QT} REQUIRED COMPONENTS Test)
|
||||
enable_testing()
|
||||
|
||||
# Disasm/Fadec sources needed by any test that links editor.cpp
|
||||
set(DISASM_SRCS src/disasm.cpp third_party/fadec/decode.c third_party/fadec/format.c)
|
||||
|
||||
# ── Headless tests (Qt::Core only — safe for CI without a display) ──
|
||||
|
||||
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp)
|
||||
target_include_directories(test_core PRIVATE src)
|
||||
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
|
||||
@@ -149,14 +169,6 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_compose COMMAND test_compose)
|
||||
|
||||
add_executable(test_editor tests/test_editor.cpp src/editor.cpp src/compose.cpp src/format.cpp src/providerregistry.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_editor PRIVATE src)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
|
||||
add_executable(test_provider tests/test_provider.cpp)
|
||||
target_include_directories(test_provider PRIVATE src)
|
||||
target_link_libraries(test_provider PRIVATE ${QT}::Core ${QT}::Test)
|
||||
@@ -167,12 +179,47 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_command_row PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_command_row COMMAND test_command_row)
|
||||
|
||||
add_executable(test_generator tests/test_generator.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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
|
||||
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)
|
||||
|
||||
# ── 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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_controller PRIVATE src)
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_controller PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
@@ -185,8 +232,8 @@ if(BUILD_TESTING)
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_validation PRIVATE src)
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_validation PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_validation PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
@@ -195,18 +242,12 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME test_validation COMMAND test_validation)
|
||||
|
||||
add_executable(test_generator tests/test_generator.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp)
|
||||
target_include_directories(test_generator PRIVATE src)
|
||||
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_generator COMMAND test_generator)
|
||||
|
||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_context_menu PRIVATE src)
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
@@ -215,6 +256,16 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
|
||||
add_executable(test_editor tests/test_editor.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp
|
||||
src/providerregistry.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_editor PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
|
||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp)
|
||||
target_include_directories(test_rendered_view PRIVATE src)
|
||||
@@ -227,8 +278,8 @@ if(BUILD_TESTING)
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_new_features PRIVATE src)
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_new_features PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_new_features PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
@@ -241,8 +292,8 @@ if(BUILD_TESTING)
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_type_selector PRIVATE src)
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_selector PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
@@ -251,21 +302,21 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME test_type_selector COMMAND test_type_selector)
|
||||
|
||||
add_executable(test_theme tests/test_theme.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_theme PRIVATE src)
|
||||
target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_theme COMMAND test_theme)
|
||||
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
|
||||
add_executable(test_options_dialog tests/test_options_dialog.cpp
|
||||
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_options_dialog PRIVATE src)
|
||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(test_windbg_provider PRIVATE dbgeng ole32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
endif()
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
|
||||
# Standalone test: proves whether CoInitializeSecurity is needed for DebugConnect
|
||||
# Requires a running WinDbg debug server on port 5055
|
||||
@@ -287,6 +338,11 @@ if(BUILD_TESTING)
|
||||
COMMENT "Deploying Qt runtime DLLs for tests..."
|
||||
)
|
||||
endif()
|
||||
|
||||
endif() # BUILD_UI_TESTS
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
if(WIN32)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemoryWindows)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## State
|
||||
|
||||
- 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`):
|
||||
@@ -15,10 +13,6 @@ This tool helps you inspect raw bytes and interpret them as types (structs, arra
|
||||
}
|
||||
}
|
||||
```
|
||||
- Plugin system is partially implemented. Some UI bugs exist.
|
||||
- Vector/Matrix improvements have been made but are not entirely complete.
|
||||
- Every edit goes through a full undo/redo system.
|
||||
|
||||
## Build
|
||||
|
||||
1. Prerequisites
|
||||
|
||||
8
deploy/Reclass.desktop
Normal file
8
deploy/Reclass.desktop
Normal file
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Reclass
|
||||
Comment=Memory structure reverse engineering tool
|
||||
Exec=Reclass
|
||||
Icon=reclass
|
||||
Categories=Development;Debugger;
|
||||
Terminal=false
|
||||
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(ProcessMemoryWindowsPlugin LANGUAGES CXX)
|
||||
project(ProcessMemoryPlugin LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -12,36 +12,36 @@ set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# Plugin sources
|
||||
set(PLUGIN_SOURCES
|
||||
ProcessMemoryWindowsPlugin.h
|
||||
ProcessMemoryWindowsPlugin.cpp
|
||||
ProcessMemoryPlugin.h
|
||||
ProcessMemoryPlugin.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui
|
||||
)
|
||||
|
||||
# Create shared library (DLL)
|
||||
add_library(ProcessMemoryWindowsPlugin SHARED ${PLUGIN_SOURCES})
|
||||
add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
# Link Qt
|
||||
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
|
||||
target_link_libraries(ProcessMemoryPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
|
||||
|
||||
# Platform-specific linking
|
||||
if(WIN32)
|
||||
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE psapi shell32)
|
||||
target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32)
|
||||
endif()
|
||||
|
||||
# On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_compile_options(ProcessMemoryWindowsPlugin PRIVATE -fvisibility=hidden)
|
||||
target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden)
|
||||
endif()
|
||||
|
||||
# Include directories
|
||||
target_include_directories(ProcessMemoryWindowsPlugin PRIVATE
|
||||
target_include_directories(ProcessMemoryPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
)
|
||||
|
||||
# Output to Plugins folder
|
||||
set_target_properties(ProcessMemoryWindowsPlugin PROPERTIES
|
||||
set_target_properties(ProcessMemoryPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "ProcessMemoryWindowsPlugin.h"
|
||||
#include "ProcessMemoryPlugin.h"
|
||||
|
||||
#include "../../src/processpicker.h"
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
#endif
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// ProcessMemoryWindowsProvider implementation
|
||||
// ProcessMemoryProvider implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName)
|
||||
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
|
||||
: m_handle(nullptr)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
@@ -60,28 +60,28 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
|
||||
cacheModules();
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (!m_handle || len <= 0) return false;
|
||||
|
||||
SIZE_T bytesRead = 0;
|
||||
ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead);
|
||||
ReadProcessMemory(m_handle, (LPCVOID)(addr), buf, (SIZE_T)len, &bytesRead);
|
||||
if ((int)bytesRead < len)
|
||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||
return bytesRead > 0;
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len)
|
||||
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (!m_handle || !m_writable || len <= 0) return false;
|
||||
|
||||
SIZE_T bytesWritten = 0;
|
||||
if (WriteProcessMemory(m_handle, (LPVOID)(m_base + addr), buf, (SIZE_T)len, &bytesWritten))
|
||||
if (WriteProcessMemory(m_handle, (LPVOID)(addr), buf, (SIZE_T)len, &bytesWritten))
|
||||
return bytesWritten == (SIZE_T)len;
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules)
|
||||
{
|
||||
@@ -96,7 +96,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
return {};
|
||||
}
|
||||
|
||||
void ProcessMemoryWindowsProvider::cacheModules()
|
||||
void ProcessMemoryProvider::cacheModules()
|
||||
{
|
||||
HMODULE mods[1024];
|
||||
DWORD needed = 0;
|
||||
@@ -126,7 +126,7 @@ void ProcessMemoryWindowsProvider::cacheModules()
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName)
|
||||
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
|
||||
: m_fd(-1)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
@@ -152,19 +152,17 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
|
||||
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (m_fd < 0 || len <= 0) return false;
|
||||
|
||||
uint64_t absAddr = m_base + addr;
|
||||
|
||||
// Try process_vm_readv first (faster, no fd seek contention)
|
||||
struct iovec local;
|
||||
local.iov_base = buf;
|
||||
local.iov_len = static_cast<size_t>(len);
|
||||
|
||||
struct iovec remote;
|
||||
remote.iov_base = reinterpret_cast<void*>(absAddr);
|
||||
remote.iov_base = reinterpret_cast<void*>(addr);
|
||||
remote.iov_len = static_cast<size_t>(len);
|
||||
|
||||
ssize_t nread = process_vm_readv(m_pid, &local, 1, &remote, 1, 0);
|
||||
@@ -172,23 +170,21 @@ bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
|
||||
return true;
|
||||
|
||||
// Fallback: pread on /proc/<pid>/mem
|
||||
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
|
||||
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
|
||||
return nread == static_cast<ssize_t>(len);
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len)
|
||||
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (m_fd < 0 || !m_writable || len <= 0) return false;
|
||||
|
||||
uint64_t absAddr = m_base + addr;
|
||||
|
||||
// Try process_vm_writev first
|
||||
struct iovec local;
|
||||
local.iov_base = const_cast<void*>(buf);
|
||||
local.iov_len = static_cast<size_t>(len);
|
||||
|
||||
struct iovec remote;
|
||||
remote.iov_base = reinterpret_cast<void*>(absAddr);
|
||||
remote.iov_base = reinterpret_cast<void*>(addr);
|
||||
remote.iov_len = static_cast<size_t>(len);
|
||||
|
||||
ssize_t nwritten = process_vm_writev(m_pid, &local, 1, &remote, 1, 0);
|
||||
@@ -196,11 +192,11 @@ bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len
|
||||
return true;
|
||||
|
||||
// Fallback: pwrite on /proc/<pid>/mem
|
||||
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
|
||||
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
|
||||
return nwritten == static_cast<ssize_t>(len);
|
||||
}
|
||||
|
||||
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules)
|
||||
{
|
||||
@@ -215,7 +211,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
|
||||
return {};
|
||||
}
|
||||
|
||||
void ProcessMemoryWindowsProvider::cacheModules()
|
||||
void ProcessMemoryProvider::cacheModules()
|
||||
{
|
||||
// Parse /proc/<pid>/maps to discover loaded modules
|
||||
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
|
||||
@@ -288,7 +284,7 @@ void ProcessMemoryWindowsProvider::cacheModules()
|
||||
|
||||
#endif // platform
|
||||
|
||||
ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
|
||||
ProcessMemoryProvider::~ProcessMemoryProvider()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_handle)
|
||||
@@ -299,7 +295,7 @@ ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
|
||||
#endif
|
||||
}
|
||||
|
||||
int ProcessMemoryWindowsProvider::size() const
|
||||
int ProcessMemoryProvider::size() const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return m_handle ? 0x10000 : 0;
|
||||
@@ -309,22 +305,22 @@ int ProcessMemoryWindowsProvider::size() const
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// ProcessMemoryWindowsPlugin implementation
|
||||
// ProcessMemoryPlugin implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
QIcon ProcessMemoryWindowsPlugin::Icon() const
|
||||
QIcon ProcessMemoryPlugin::Icon() const
|
||||
{
|
||||
return qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsPlugin::canHandle(const QString& target) const
|
||||
bool ProcessMemoryPlugin::canHandle(const QString& target) const
|
||||
{
|
||||
// Target format: "pid:name" or just "pid"
|
||||
QRegularExpression re("^\\d+");
|
||||
return re.match(target).hasMatch();
|
||||
}
|
||||
|
||||
std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
std::unique_ptr<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
{
|
||||
// Parse target: "pid:name" or just "pid"
|
||||
QStringList parts = target.split(':');
|
||||
@@ -339,7 +335,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
|
||||
|
||||
QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid);
|
||||
|
||||
auto provider = std::make_unique<ProcessMemoryWindowsProvider>(pid, name);
|
||||
auto provider = std::make_unique<ProcessMemoryProvider>(pid, name);
|
||||
if (!provider->isValid())
|
||||
{
|
||||
if (errorMsg)
|
||||
@@ -352,7 +348,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
|
||||
return provider;
|
||||
}
|
||||
|
||||
uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target) const
|
||||
uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Parse PID from target
|
||||
@@ -409,7 +405,7 @@ uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
// Use custom process enumeration from plugin
|
||||
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
|
||||
@@ -440,7 +436,7 @@ bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
|
||||
QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
{
|
||||
QVector<PluginProcessInfo> processes;
|
||||
|
||||
@@ -543,5 +539,5 @@ QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new ProcessMemoryWindowsPlugin();
|
||||
return new ProcessMemoryPlugin();
|
||||
}
|
||||
@@ -5,14 +5,14 @@
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Process memory provider (Windows)
|
||||
* Reads/writes memory from a live process using Windows platform APIs
|
||||
* Process memory provider
|
||||
* Reads/writes memory from a live process using platform APIs
|
||||
*/
|
||||
class ProcessMemoryWindowsProvider : public rcx::Provider
|
||||
class ProcessMemoryProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName);
|
||||
~ProcessMemoryWindowsProvider() override;
|
||||
ProcessMemoryProvider(uint32_t pid, const QString& processName);
|
||||
~ProcessMemoryProvider() override;
|
||||
|
||||
// Required overrides
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
@@ -27,11 +27,16 @@ public:
|
||||
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
void setBase(uint64_t b) override { m_base = b; }
|
||||
bool isReadable(uint64_t, int len) const override {
|
||||
#ifdef _WIN32
|
||||
return m_handle && len >= 0;
|
||||
#elif defined(__linux__)
|
||||
return m_fd >= 0 && len >= 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Process-specific helpers
|
||||
uint32_t pid() const { return m_pid; }
|
||||
uint64_t baseAddress() const { return m_base; }
|
||||
void refreshModules() { m_modules.clear(); cacheModules(); }
|
||||
|
||||
private:
|
||||
@@ -57,15 +62,15 @@ private:
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin that provides ProcessMemoryWindowsProvider
|
||||
* Plugin that provides ProcessMemoryProvider
|
||||
*/
|
||||
class ProcessMemoryWindowsPlugin : public IProviderPlugin
|
||||
class ProcessMemoryPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
std::string Name() const override { return "Process Memory Windows"; }
|
||||
std::string Name() const override { return "Process Memory"; }
|
||||
std::string Version() const override { return "1.0.0"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
std::string Description() const override { return "Read and write memory from local running processes (Windows)"; }
|
||||
std::string Description() const override { return "Read and write memory from local running processes"; }
|
||||
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
||||
QIcon Icon() const override;
|
||||
|
||||
93
plugins/RcNetPluginCompatLayer/CMakeLists.txt
Normal file
93
plugins/RcNetPluginCompatLayer/CMakeLists.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(RcNetCompatPlugin LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# Plugin sources
|
||||
set(PLUGIN_SOURCES
|
||||
RcNetCompatPlugin.h
|
||||
RcNetCompatPlugin.cpp
|
||||
RcNetCompatProvider.h
|
||||
RcNetCompatProvider.cpp
|
||||
ReClassNET_Plugin.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui
|
||||
)
|
||||
|
||||
# -- Optional .NET bridge -------------------------------------------------
|
||||
# When the .NET SDK is available, build the C# bridge assembly and enable
|
||||
# CLR hosting support in the C++ plugin.
|
||||
|
||||
find_program(DOTNET_EXE dotnet)
|
||||
if(DOTNET_EXE)
|
||||
# Check that 'dotnet build' actually works for net472
|
||||
execute_process(
|
||||
COMMAND ${DOTNET_EXE} --list-sdks
|
||||
OUTPUT_VARIABLE _dotnet_sdks
|
||||
ERROR_QUIET
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
if(_dotnet_sdks)
|
||||
set(HAS_CLR_BRIDGE ON)
|
||||
message(STATUS "RcNetCompat: .NET SDK found -- building managed bridge")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(HAS_CLR_BRIDGE)
|
||||
list(APPEND PLUGIN_SOURCES
|
||||
ClrHost.h
|
||||
ClrHost.cpp
|
||||
)
|
||||
|
||||
# Build the C# bridge assembly
|
||||
set(_bridge_src "${CMAKE_CURRENT_SOURCE_DIR}/bridge")
|
||||
set(_bridge_out "${CMAKE_BINARY_DIR}/Plugins/RcNetBridge.dll")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_bridge_out}"
|
||||
COMMAND ${DOTNET_EXE} build
|
||||
"${_bridge_src}/RcNetBridge.csproj"
|
||||
-c Release
|
||||
-o "${CMAKE_BINARY_DIR}/Plugins"
|
||||
--nologo -v quiet
|
||||
DEPENDS
|
||||
"${_bridge_src}/RcNetBridge.cs"
|
||||
"${_bridge_src}/RcNetBridge.csproj"
|
||||
COMMENT "Building RcNetBridge.dll (.NET bridge)..."
|
||||
)
|
||||
add_custom_target(RcNetBridge ALL DEPENDS "${_bridge_out}")
|
||||
else()
|
||||
message(STATUS "RcNetCompat: .NET SDK not found -- managed plugin support disabled")
|
||||
endif()
|
||||
|
||||
# Create shared library (DLL)
|
||||
add_library(RcNetCompatPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
if(HAS_CLR_BRIDGE)
|
||||
target_compile_definitions(RcNetCompatPlugin PRIVATE HAS_CLR_BRIDGE=1)
|
||||
add_dependencies(RcNetCompatPlugin RcNetBridge)
|
||||
# CLR hosting uses COM (ole32)
|
||||
target_link_libraries(RcNetCompatPlugin PRIVATE ole32)
|
||||
endif()
|
||||
|
||||
# Link Qt
|
||||
target_link_libraries(RcNetCompatPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
|
||||
|
||||
# Include directories
|
||||
target_include_directories(RcNetCompatPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
)
|
||||
|
||||
# Output to Plugins folder
|
||||
set_target_properties(RcNetCompatPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
162
plugins/RcNetPluginCompatLayer/ClrHost.cpp
Normal file
162
plugins/RcNetPluginCompatLayer/ClrHost.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include "ClrHost.h"
|
||||
|
||||
#include <cwchar>
|
||||
|
||||
// -- GUIDs ----------------------------------------------------------------
|
||||
|
||||
using FnCLRCreateInstance = HRESULT(STDAPICALLTYPE*)(REFCLSID, REFIID, LPVOID*);
|
||||
|
||||
// {9280188D-0E8E-4867-B30C-7FA83884E8DE}
|
||||
static const GUID sCLSID_CLRMetaHost =
|
||||
{0x9280188d, 0x0e8e, 0x4867, {0xb3, 0x0c, 0x7f, 0xa8, 0x38, 0x84, 0xe8, 0xde}};
|
||||
|
||||
// {D332DB9E-B9B3-4125-8207-A14884F53216}
|
||||
static const GUID sIID_ICLRMetaHost =
|
||||
{0xD332DB9E, 0xB9B3, 0x4125, {0x82, 0x07, 0xA1, 0x48, 0x84, 0xF5, 0x32, 0x16}};
|
||||
|
||||
// {BD39D1D2-BA2F-486A-89B0-B4B0CB466891}
|
||||
static const GUID sIID_ICLRRuntimeInfo =
|
||||
{0xBD39D1D2, 0xBA2F, 0x486a, {0x89, 0xB0, 0xB4, 0xB0, 0xCB, 0x46, 0x68, 0x91}};
|
||||
|
||||
// {90F1A06E-7712-4762-86B5-7A5EBA6BDB02}
|
||||
static const GUID sCLSID_CLRRuntimeHost =
|
||||
{0x90F1A06E, 0x7712, 0x4762, {0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}};
|
||||
|
||||
// {90F1A06C-7712-4762-86B5-7A5EBA6BDB02}
|
||||
static const GUID sIID_ICLRRuntimeHost =
|
||||
{0x90F1A06C, 0x7712, 0x4762, {0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}};
|
||||
|
||||
// -- ClrHost implementation -----------------------------------------------
|
||||
|
||||
ClrHost::ClrHost()
|
||||
{
|
||||
startClr();
|
||||
}
|
||||
|
||||
ClrHost::~ClrHost()
|
||||
{
|
||||
if (m_runtimeHost) m_runtimeHost->Release();
|
||||
if (m_runtimeInfo) m_runtimeInfo->Release();
|
||||
if (m_metaHost) m_metaHost->Release();
|
||||
if (m_mscoree) FreeLibrary(m_mscoree);
|
||||
}
|
||||
|
||||
bool ClrHost::startClr()
|
||||
{
|
||||
m_mscoree = LoadLibraryW(L"mscoree.dll");
|
||||
if (!m_mscoree)
|
||||
return false;
|
||||
|
||||
auto fnCreate = reinterpret_cast<FnCLRCreateInstance>(
|
||||
GetProcAddress(m_mscoree, "CLRCreateInstance"));
|
||||
if (!fnCreate)
|
||||
return false;
|
||||
|
||||
HRESULT hr = fnCreate(sCLSID_CLRMetaHost, sIID_ICLRMetaHost,
|
||||
reinterpret_cast<LPVOID*>(&m_metaHost));
|
||||
if (FAILED(hr) || !m_metaHost)
|
||||
return false;
|
||||
|
||||
hr = m_metaHost->GetRuntime(L"v4.0.30319", sIID_ICLRRuntimeInfo,
|
||||
reinterpret_cast<LPVOID*>(&m_runtimeInfo));
|
||||
if (FAILED(hr) || !m_runtimeInfo)
|
||||
return false;
|
||||
|
||||
hr = m_runtimeInfo->GetInterface(sCLSID_CLRRuntimeHost, sIID_ICLRRuntimeHost,
|
||||
(LPVOID*)&m_runtimeHost);
|
||||
if (FAILED(hr) || !m_runtimeHost)
|
||||
return false;
|
||||
|
||||
hr = m_runtimeHost->Start();
|
||||
if (FAILED(hr))
|
||||
return false;
|
||||
|
||||
m_clrStarted = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClrHost::loadManagedPlugin(const QString& bridgeDllPath,
|
||||
const QString& pluginPath,
|
||||
RcNetFunctions* outFunctions,
|
||||
QString* errorMsg)
|
||||
{
|
||||
if (!m_runtimeHost || !m_clrStarted) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
".NET Framework 4.x is not available on this machine.\n"
|
||||
"Install the .NET Framework 4.7.2+ runtime to load managed plugins.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Zero the function table -- the bridge will fill it
|
||||
memset(outFunctions, 0, sizeof(RcNetFunctions));
|
||||
|
||||
// Build the argument string: "<hex_address_of_function_table>|<plugin_path>"
|
||||
// Use %ls (not %s) for wide strings -- MinGW follows POSIX conventions.
|
||||
wchar_t arg[2048];
|
||||
swprintf(arg, sizeof(arg) / sizeof(wchar_t),
|
||||
L"%llx|%ls",
|
||||
reinterpret_cast<unsigned long long>(outFunctions),
|
||||
reinterpret_cast<const wchar_t*>(pluginPath.utf16()));
|
||||
|
||||
DWORD retVal = 0;
|
||||
HRESULT hr = m_runtimeHost->ExecuteInDefaultAppDomain(
|
||||
reinterpret_cast<LPCWSTR>(bridgeDllPath.utf16()),
|
||||
L"RcNetBridge.Bridge",
|
||||
L"Initialize",
|
||||
arg,
|
||||
&retVal
|
||||
);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"Failed to execute .NET bridge (HRESULT 0x%1).\n"
|
||||
"Bridge: %2\n"
|
||||
"Plugin: %3")
|
||||
.arg(static_cast<uint>(hr), 8, 16, QChar('0'))
|
||||
.arg(bridgeDllPath)
|
||||
.arg(pluginPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (retVal != 0) {
|
||||
if (errorMsg) {
|
||||
switch (retVal) {
|
||||
case 1:
|
||||
*errorMsg = QStringLiteral("Bridge: invalid argument format.");
|
||||
break;
|
||||
case 2:
|
||||
*errorMsg = QStringLiteral(
|
||||
"No ICoreProcessFunctions implementation found in the .NET plugin.\n"
|
||||
"The DLL may not be a ReClass.NET plugin.");
|
||||
break;
|
||||
case 3:
|
||||
*errorMsg = QStringLiteral(
|
||||
"Failed to load the .NET plugin assembly.\n"
|
||||
"Check that all its dependencies are available.");
|
||||
break;
|
||||
default:
|
||||
*errorMsg = QStringLiteral("Bridge returned error code %1.").arg(retVal);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the bridge wrote at least the minimum required function pointers
|
||||
if (!outFunctions->ReadRemoteMemory ||
|
||||
!outFunctions->OpenRemoteProcess ||
|
||||
!outFunctions->EnumerateProcesses ||
|
||||
!outFunctions->CloseRemoteProcess) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"The .NET bridge loaded but did not provide the required functions "
|
||||
"(ReadRemoteMemory, OpenRemoteProcess, CloseRemoteProcess, EnumerateProcesses).");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
99
plugins/RcNetPluginCompatLayer/ClrHost.h
Normal file
99
plugins/RcNetPluginCompatLayer/ClrHost.h
Normal file
@@ -0,0 +1,99 @@
|
||||
#pragma once
|
||||
// In-process CLR hosting for loading .NET ReClass.NET plugins.
|
||||
// Dynamically loads mscoree.dll and uses ICLRMetaHost -> ICLRRuntimeInfo ->
|
||||
// ICLRRuntimeHost::ExecuteInDefaultAppDomain to call into the C# bridge.
|
||||
|
||||
#include "ReClassNET_Plugin.hpp"
|
||||
#include <QString>
|
||||
#include <windows.h>
|
||||
#include <objbase.h>
|
||||
|
||||
// -- Minimal COM interface definitions for CLR hosting --------------------
|
||||
// Defined here to avoid depending on Windows SDK metahost.h / mscoree.h
|
||||
// which may not be present in all MinGW distributions.
|
||||
// Only methods we actually call have real signatures; the rest are stubs
|
||||
// that preserve correct vtable offsets.
|
||||
|
||||
#undef INTERFACE
|
||||
#define INTERFACE ICLRMetaHost
|
||||
DECLARE_INTERFACE_(ICLRMetaHost, IUnknown)
|
||||
{
|
||||
// IUnknown
|
||||
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
|
||||
STDMETHOD_(ULONG, AddRef)() PURE;
|
||||
STDMETHOD_(ULONG, Release)() PURE;
|
||||
// ICLRMetaHost
|
||||
STDMETHOD(GetRuntime)(LPCWSTR pwzVersion, REFIID riid, LPVOID* ppRuntime) PURE;
|
||||
STDMETHOD(GetVersionFromFile)(LPCWSTR, LPWSTR, DWORD*) PURE;
|
||||
STDMETHOD(EnumerateInstalledRuntimes)(void**) PURE;
|
||||
STDMETHOD(EnumerateLoadedRuntimes)(HANDLE, void**) PURE;
|
||||
STDMETHOD(RequestRuntimeLoadedNotification)(void*) PURE;
|
||||
STDMETHOD(QueryLegacyV2RuntimeBinding)(REFIID, LPVOID*) PURE;
|
||||
STDMETHOD_(void, ExitProcess)(INT32) PURE;
|
||||
};
|
||||
#undef INTERFACE
|
||||
|
||||
#define INTERFACE ICLRRuntimeInfo
|
||||
DECLARE_INTERFACE_(ICLRRuntimeInfo, IUnknown)
|
||||
{
|
||||
// IUnknown
|
||||
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
|
||||
STDMETHOD_(ULONG, AddRef)() PURE;
|
||||
STDMETHOD_(ULONG, Release)() PURE;
|
||||
// ICLRRuntimeInfo
|
||||
STDMETHOD(GetVersionString)(LPWSTR, DWORD*) PURE;
|
||||
STDMETHOD(GetRuntimeDirectory)(LPWSTR, DWORD*) PURE;
|
||||
STDMETHOD(IsLoaded)(HANDLE, BOOL*) PURE;
|
||||
STDMETHOD(LoadErrorString)(UINT, LPWSTR, DWORD*, LONG) PURE;
|
||||
STDMETHOD(LoadLibrary)(LPCWSTR, HMODULE*) PURE;
|
||||
STDMETHOD(GetProcAddress)(LPCSTR, LPVOID*) PURE;
|
||||
STDMETHOD(GetInterface)(REFCLSID rclsid, REFIID riid, LPVOID* ppUnk) PURE;
|
||||
};
|
||||
#undef INTERFACE
|
||||
|
||||
#define INTERFACE ICLRRuntimeHost
|
||||
DECLARE_INTERFACE_(ICLRRuntimeHost, IUnknown)
|
||||
{
|
||||
// IUnknown
|
||||
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
|
||||
STDMETHOD_(ULONG, AddRef)() PURE;
|
||||
STDMETHOD_(ULONG, Release)() PURE;
|
||||
// ICLRRuntimeHost
|
||||
STDMETHOD(Start)() PURE;
|
||||
STDMETHOD(Stop)() PURE;
|
||||
STDMETHOD(SetHostControl)(void*) PURE;
|
||||
STDMETHOD(GetCLRControl)(void**) PURE;
|
||||
STDMETHOD(UnloadAppDomain)(DWORD, BOOL) PURE;
|
||||
STDMETHOD(ExecuteInAppDomain)(DWORD, void*, void*) PURE;
|
||||
STDMETHOD(GetCurrentAppDomainId)(DWORD*) PURE;
|
||||
STDMETHOD(ExecuteApplication)(LPCWSTR, DWORD, LPCWSTR*, DWORD, LPCWSTR*, int*) PURE;
|
||||
STDMETHOD(ExecuteInDefaultAppDomain)(LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, DWORD*) PURE;
|
||||
};
|
||||
#undef INTERFACE
|
||||
|
||||
// -- CLR Host wrapper -----------------------------------------------------
|
||||
|
||||
class ClrHost
|
||||
{
|
||||
public:
|
||||
ClrHost();
|
||||
~ClrHost();
|
||||
|
||||
// True if the .NET Framework CLR (v4.0) is available on this machine.
|
||||
bool isAvailable() const { return m_runtimeHost != nullptr && m_clrStarted; }
|
||||
|
||||
// Load a managed ReClass.NET plugin via the C# bridge.
|
||||
bool loadManagedPlugin(const QString& bridgeDllPath,
|
||||
const QString& pluginPath,
|
||||
RcNetFunctions* outFunctions,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
private:
|
||||
bool startClr();
|
||||
|
||||
HMODULE m_mscoree = nullptr;
|
||||
ICLRMetaHost* m_metaHost = nullptr;
|
||||
ICLRRuntimeInfo* m_runtimeInfo = nullptr;
|
||||
ICLRRuntimeHost* m_runtimeHost = nullptr;
|
||||
bool m_clrStarted = false;
|
||||
};
|
||||
333
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp
Normal file
333
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp
Normal file
@@ -0,0 +1,333 @@
|
||||
#include "RcNetCompatPlugin.h"
|
||||
#include "RcNetCompatProvider.h"
|
||||
#include "../../src/processpicker.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QStyle>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
// -- Helpers --------------------------------------------------------------
|
||||
|
||||
QIcon RcNetCompatPlugin::Icon() const
|
||||
{
|
||||
return qApp->style()->standardIcon(QStyle::SP_TrashIcon);
|
||||
}
|
||||
|
||||
// --.NET assembly detection ----------------------------------------------
|
||||
|
||||
static bool isDotNetAssembly(const QString& path)
|
||||
{
|
||||
// A .NET assembly has a non-zero CLR header directory entry in the PE
|
||||
// optional header. We check this by loading the PE without running
|
||||
// DllMain and inspecting the IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR.
|
||||
HMODULE hMod = GetModuleHandleW(reinterpret_cast<LPCWSTR>(path.utf16()));
|
||||
if (!hMod)
|
||||
hMod = LoadLibraryExW(reinterpret_cast<LPCWSTR>(path.utf16()),
|
||||
nullptr, DONT_RESOLVE_DLL_REFERENCES);
|
||||
if (!hMod) return false;
|
||||
|
||||
auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(hMod);
|
||||
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false;
|
||||
|
||||
auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(
|
||||
reinterpret_cast<const char*>(hMod) + dos->e_lfanew);
|
||||
if (nt->Signature != IMAGE_NT_SIGNATURE) return false;
|
||||
|
||||
constexpr DWORD kClrIndex = IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR; // 14
|
||||
DWORD rva = 0, dirSize = 0;
|
||||
|
||||
if (nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
|
||||
auto* opt = reinterpret_cast<const IMAGE_OPTIONAL_HEADER64*>(&nt->OptionalHeader);
|
||||
if (opt->NumberOfRvaAndSizes > kClrIndex) {
|
||||
rva = opt->DataDirectory[kClrIndex].VirtualAddress;
|
||||
dirSize = opt->DataDirectory[kClrIndex].Size;
|
||||
}
|
||||
} else {
|
||||
auto* opt = reinterpret_cast<const IMAGE_OPTIONAL_HEADER32*>(&nt->OptionalHeader);
|
||||
if (opt->NumberOfRvaAndSizes > kClrIndex) {
|
||||
rva = opt->DataDirectory[kClrIndex].VirtualAddress;
|
||||
dirSize = opt->DataDirectory[kClrIndex].Size;
|
||||
}
|
||||
}
|
||||
|
||||
return rva != 0 && dirSize != 0;
|
||||
}
|
||||
|
||||
// --Unified loader (dispatches native vs managed) ------------------------
|
||||
|
||||
bool RcNetCompatPlugin::loadPlugin(const QString& path, QString* errorMsg)
|
||||
{
|
||||
if (m_dllPath == path && (m_lib || m_isManaged))
|
||||
return true; // Already loaded
|
||||
|
||||
if (isDotNetAssembly(path)) {
|
||||
#ifdef HAS_CLR_BRIDGE
|
||||
return loadManagedDll(path, errorMsg);
|
||||
#else
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"This is a .NET assembly.\n\n"
|
||||
"This build does not include .NET bridge support.\n"
|
||||
"Rebuild with the .NET SDK installed to enable managed plugin loading.");
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
return loadNativeDll(path, errorMsg);
|
||||
}
|
||||
|
||||
// --Native DLL loading ---------------------------------------------------
|
||||
|
||||
bool RcNetCompatPlugin::loadNativeDll(const QString& path, QString* errorMsg)
|
||||
{
|
||||
unloadNativeDll();
|
||||
|
||||
m_lib = std::make_unique<QLibrary>(path);
|
||||
if (!m_lib->load()) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Failed to load DLL: %1").arg(m_lib->errorString());
|
||||
m_lib.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve all function pointers
|
||||
m_fns.EnumerateProcesses =
|
||||
reinterpret_cast<FnEnumerateProcesses>(m_lib->resolve("EnumerateProcesses"));
|
||||
m_fns.OpenRemoteProcess =
|
||||
reinterpret_cast<FnOpenRemoteProcess>(m_lib->resolve("OpenRemoteProcess"));
|
||||
m_fns.IsProcessValid =
|
||||
reinterpret_cast<FnIsProcessValid>(m_lib->resolve("IsProcessValid"));
|
||||
m_fns.CloseRemoteProcess =
|
||||
reinterpret_cast<FnCloseRemoteProcess>(m_lib->resolve("CloseRemoteProcess"));
|
||||
m_fns.ReadRemoteMemory =
|
||||
reinterpret_cast<FnReadRemoteMemory>(m_lib->resolve("ReadRemoteMemory"));
|
||||
m_fns.WriteRemoteMemory =
|
||||
reinterpret_cast<FnWriteRemoteMemory>(m_lib->resolve("WriteRemoteMemory"));
|
||||
m_fns.EnumerateRemoteSectionsAndModules =
|
||||
reinterpret_cast<FnEnumerateRemoteSectionsAndModules>(
|
||||
m_lib->resolve("EnumerateRemoteSectionsAndModules"));
|
||||
m_fns.ControlRemoteProcess =
|
||||
reinterpret_cast<FnControlRemoteProcess>(m_lib->resolve("ControlRemoteProcess"));
|
||||
|
||||
// At minimum we need read + open + close
|
||||
if (!m_fns.ReadRemoteMemory || !m_fns.OpenRemoteProcess || !m_fns.CloseRemoteProcess || !m_fns.EnumerateProcesses) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"DLL is missing required exports (ReadRemoteMemory, OpenRemoteProcess, "
|
||||
"CloseRemoteProcess, EnumerateProcesses). Is this a ReClass.NET native plugin?");
|
||||
m_lib->unload();
|
||||
m_lib.reset();
|
||||
m_fns = {};
|
||||
return false;
|
||||
}
|
||||
|
||||
m_dllPath = path;
|
||||
m_isManaged = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RcNetCompatPlugin::unloadNativeDll()
|
||||
{
|
||||
if (m_lib) {
|
||||
m_lib->unload();
|
||||
m_lib.reset();
|
||||
}
|
||||
m_fns = {};
|
||||
m_dllPath.clear();
|
||||
m_isManaged = false;
|
||||
}
|
||||
|
||||
// --Managed (.NET) DLL loading via CLR bridge ----------------------------
|
||||
|
||||
#ifdef HAS_CLR_BRIDGE
|
||||
|
||||
bool RcNetCompatPlugin::loadManagedDll(const QString& path, QString* errorMsg)
|
||||
{
|
||||
unloadNativeDll();
|
||||
|
||||
// Lazily create the CLR host (one per plugin lifetime)
|
||||
if (!m_clrHost)
|
||||
m_clrHost = std::make_unique<ClrHost>();
|
||||
|
||||
if (!m_clrHost->isAvailable()) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
".NET Framework 4.x is not available on this machine.\n"
|
||||
"Install the .NET Framework 4.7.2+ runtime to load managed plugins.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Locate RcNetBridge.dll next to our own plugin DLL
|
||||
// Use native separators -- the CLR expects Windows-style backslash paths.
|
||||
QString bridgePath = QDir::toNativeSeparators(
|
||||
QCoreApplication::applicationDirPath()
|
||||
+ QStringLiteral("/Plugins/RcNetBridge.dll"));
|
||||
|
||||
if (!QFileInfo::exists(bridgePath)) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"RcNetBridge.dll not found in the Plugins folder.\n"
|
||||
"Expected at: %1").arg(bridgePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_fns = {};
|
||||
QString nativePath = QDir::toNativeSeparators(path);
|
||||
if (!m_clrHost->loadManagedPlugin(bridgePath, nativePath, &m_fns, errorMsg))
|
||||
return false;
|
||||
|
||||
m_dllPath = path;
|
||||
m_isManaged = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // HAS_CLR_BRIDGE
|
||||
|
||||
// --IProviderPlugin ------------------------------------------------------
|
||||
|
||||
bool RcNetCompatPlugin::canHandle(const QString& target) const
|
||||
{
|
||||
// Target format: "dllpath|pid:name"
|
||||
return target.contains('|');
|
||||
}
|
||||
|
||||
std::unique_ptr<rcx::Provider> RcNetCompatPlugin::createProvider(
|
||||
const QString& target, QString* errorMsg)
|
||||
{
|
||||
// Parse "dllpath|pid:name"
|
||||
int sep = target.indexOf('|');
|
||||
if (sep < 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid target format");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString dllPath = target.left(sep);
|
||||
QString pidPart = target.mid(sep + 1);
|
||||
|
||||
// Load (or reuse) the plugin DLL
|
||||
if (!loadPlugin(dllPath, errorMsg))
|
||||
return nullptr;
|
||||
|
||||
// Parse pid:name
|
||||
QStringList parts = pidPart.split(':');
|
||||
bool ok = false;
|
||||
uint32_t pid = parts[0].toUInt(&ok);
|
||||
if (!ok || pid == 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID: %1").arg(parts[0]);
|
||||
return nullptr;
|
||||
}
|
||||
QString procName = parts.size() > 1 ? parts[1] : QStringLiteral("PID %1").arg(pid);
|
||||
|
||||
auto provider = std::make_unique<RcNetCompatProvider>(m_fns, pid, procName);
|
||||
if (!provider->isValid()) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral(
|
||||
"Failed to open process %1 (PID: %2) via ReClass.NET plugin.\n"
|
||||
"Ensure the process is running and the plugin supports it.")
|
||||
.arg(procName).arg(pid);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
uint64_t RcNetCompatPlugin::getInitialBaseAddress(const QString& target) const
|
||||
{
|
||||
Q_UNUSED(target);
|
||||
// The provider sets its own base from module enumeration.
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool RcNetCompatPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
// Step 1: Pick a ReClass.NET plugin DLL (native or .NET)
|
||||
QString dllPath = QFileDialog::getOpenFileName(
|
||||
parent,
|
||||
QStringLiteral("Select ReClass.NET Plugin"),
|
||||
QString(),
|
||||
QStringLiteral("DLL Files (*.dll)"));
|
||||
|
||||
if (dllPath.isEmpty())
|
||||
return false;
|
||||
|
||||
// Step 2: Load and validate the DLL
|
||||
QString loadErr;
|
||||
if (!loadPlugin(dllPath, &loadErr)) {
|
||||
QMessageBox::warning(parent,
|
||||
QStringLiteral("ReClass.NET Compat Layer"),
|
||||
loadErr);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Enumerate processes and show picker
|
||||
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
|
||||
|
||||
QList<ProcessInfo> processes;
|
||||
for (const auto& p : pluginProcesses) {
|
||||
ProcessInfo info;
|
||||
info.pid = p.pid;
|
||||
info.name = p.name;
|
||||
info.path = p.path;
|
||||
info.icon = p.icon;
|
||||
processes.append(info);
|
||||
}
|
||||
|
||||
ProcessPicker picker(processes, parent);
|
||||
if (picker.exec() != QDialog::Accepted)
|
||||
return false;
|
||||
|
||||
uint32_t pid = picker.selectedProcessId();
|
||||
QString name = picker.selectedProcessName();
|
||||
|
||||
// Step 4: Format target as "dllpath|pid:name"
|
||||
*target = QStringLiteral("%1|%2:%3").arg(dllPath).arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --Process enumeration --------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
struct ProcessCollector {
|
||||
QVector<PluginProcessInfo>* dest = nullptr;
|
||||
};
|
||||
thread_local ProcessCollector g_processCollector;
|
||||
|
||||
void RC_CALLCONV processCallback(EnumerateProcessData* data)
|
||||
{
|
||||
if (!data || !g_processCollector.dest) return;
|
||||
|
||||
PluginProcessInfo info;
|
||||
info.pid = static_cast<uint32_t>(data->Id);
|
||||
info.name = QString::fromUtf16(data->Name);
|
||||
info.path = QString::fromUtf16(data->Path);
|
||||
g_processCollector.dest->append(info);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
QVector<PluginProcessInfo> RcNetCompatPlugin::enumerateProcesses()
|
||||
{
|
||||
QVector<PluginProcessInfo> result;
|
||||
|
||||
if (!m_fns.EnumerateProcesses)
|
||||
return result;
|
||||
|
||||
g_processCollector.dest = &result;
|
||||
m_fns.EnumerateProcesses(processCallback);
|
||||
g_processCollector.dest = nullptr;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// --Plugin factory -------------------------------------------------------
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new RcNetCompatPlugin();
|
||||
}
|
||||
61
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h
Normal file
61
plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
#include "../../src/iplugin.h"
|
||||
#include "ReClassNET_Plugin.hpp"
|
||||
|
||||
#include <QLibrary>
|
||||
#include <memory>
|
||||
|
||||
#ifdef HAS_CLR_BRIDGE
|
||||
#include "ClrHost.h"
|
||||
#endif
|
||||
|
||||
/**
|
||||
* ReclassX plugin that loads ReClass.NET plugin DLLs
|
||||
* and exposes them as ReclassX providers.
|
||||
*
|
||||
* Supports both native DLLs (C exports) and, when built with
|
||||
* HAS_CLR_BRIDGE, managed .NET assemblies via in-process CLR hosting.
|
||||
*
|
||||
* Target string format: "dllpath|pid:processname"
|
||||
*/
|
||||
class RcNetCompatPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
// Plugin metadata
|
||||
std::string Name() const override { return "ReClass.NET Compat Layer"; }
|
||||
std::string Version() const override { return "1.0.0"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
std::string Description() const override {
|
||||
return "Loads ReClass.NET native and .NET plugin DLLs as Reclass data sources";
|
||||
}
|
||||
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
||||
QIcon Icon() const override;
|
||||
|
||||
// IProviderPlugin interface
|
||||
bool canHandle(const QString& target) const override;
|
||||
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
|
||||
uint64_t getInitialBaseAddress(const QString& target) const override;
|
||||
bool selectTarget(QWidget* parent, QString* target) override;
|
||||
|
||||
// Override process enumeration -- we enumerate via the loaded DLL
|
||||
bool providesProcessList() const override { return true; }
|
||||
QVector<PluginProcessInfo> enumerateProcesses() override;
|
||||
|
||||
private:
|
||||
bool loadPlugin(const QString& path, QString* errorMsg = nullptr);
|
||||
bool loadNativeDll(const QString& path, QString* errorMsg = nullptr);
|
||||
void unloadNativeDll();
|
||||
|
||||
#ifdef HAS_CLR_BRIDGE
|
||||
bool loadManagedDll(const QString& path, QString* errorMsg = nullptr);
|
||||
std::unique_ptr<ClrHost> m_clrHost;
|
||||
#endif
|
||||
|
||||
std::unique_ptr<QLibrary> m_lib;
|
||||
RcNetFunctions m_fns;
|
||||
QString m_dllPath;
|
||||
bool m_isManaged = false;
|
||||
};
|
||||
|
||||
// Plugin export
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||
123
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp
Normal file
123
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp
Normal file
@@ -0,0 +1,123 @@
|
||||
#include "RcNetCompatProvider.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <cstring>
|
||||
|
||||
// -- Construction / destruction -------------------------------------------
|
||||
|
||||
RcNetCompatProvider::RcNetCompatProvider(const RcNetFunctions& fns,
|
||||
uint32_t pid,
|
||||
const QString& processName)
|
||||
: m_fns(fns)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
{
|
||||
if (m_fns.OpenRemoteProcess)
|
||||
m_handle = m_fns.OpenRemoteProcess(static_cast<RC_Size>(pid),
|
||||
ProcessAccess::Full);
|
||||
|
||||
if (m_handle)
|
||||
cacheModules();
|
||||
}
|
||||
|
||||
RcNetCompatProvider::~RcNetCompatProvider()
|
||||
{
|
||||
if (m_handle && m_fns.CloseRemoteProcess)
|
||||
m_fns.CloseRemoteProcess(m_handle);
|
||||
}
|
||||
|
||||
// -- Required overrides ---------------------------------------------------
|
||||
|
||||
bool RcNetCompatProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (!m_handle || !m_fns.ReadRemoteMemory || len <= 0)
|
||||
return false;
|
||||
|
||||
return m_fns.ReadRemoteMemory(m_handle,
|
||||
reinterpret_cast<RC_Pointer>(addr),
|
||||
static_cast<RC_Pointer>(buf),
|
||||
0, len);
|
||||
}
|
||||
|
||||
int RcNetCompatProvider::size() const
|
||||
{
|
||||
if (!m_handle) return 0;
|
||||
if (m_fns.IsProcessValid && !m_fns.IsProcessValid(m_handle)) return 0;
|
||||
return 0x10000;
|
||||
}
|
||||
|
||||
// -- Optional overrides ---------------------------------------------------
|
||||
|
||||
bool RcNetCompatProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (!m_handle || !m_fns.WriteRemoteMemory || len <= 0)
|
||||
return false;
|
||||
|
||||
return m_fns.WriteRemoteMemory(m_handle,
|
||||
reinterpret_cast<RC_Pointer>(addr),
|
||||
const_cast<RC_Pointer>(static_cast<const void*>(buf)),
|
||||
0, len);
|
||||
}
|
||||
|
||||
QString RcNetCompatProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules)
|
||||
{
|
||||
if (addr >= mod.base && addr < mod.base + mod.size)
|
||||
{
|
||||
uint64_t offset = addr - mod.base;
|
||||
return QStringLiteral("%1+0x%2")
|
||||
.arg(mod.name)
|
||||
.arg(offset, 0, 16, QChar('0'));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// -- Module enumeration ---------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// Thread-local collector for the module enumeration callback.
|
||||
// ReClass.NET callbacks are synchronous, so this is safe.
|
||||
struct ModuleCollector {
|
||||
QVector<RcNetCompatProvider::ModuleInfo>* dest = nullptr;
|
||||
};
|
||||
thread_local ModuleCollector g_moduleCollector;
|
||||
|
||||
void RC_CALLCONV moduleCallback(EnumerateRemoteModuleData* data)
|
||||
{
|
||||
if (!data || !g_moduleCollector.dest) return;
|
||||
|
||||
QString path = QString::fromUtf16(data->Path);
|
||||
QFileInfo fi(path);
|
||||
|
||||
RcNetCompatProvider::ModuleInfo info;
|
||||
info.name = fi.fileName();
|
||||
info.base = reinterpret_cast<uint64_t>(data->BaseAddress);
|
||||
info.size = static_cast<uint64_t>(data->Size);
|
||||
g_moduleCollector.dest->append(info);
|
||||
}
|
||||
|
||||
// We still need a section callback even though we don't use it.
|
||||
void RC_CALLCONV sectionCallback(EnumerateRemoteSectionData*)
|
||||
{
|
||||
// Intentionally empty -- we only need module data.
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void RcNetCompatProvider::cacheModules()
|
||||
{
|
||||
if (!m_fns.EnumerateRemoteSectionsAndModules || !m_handle)
|
||||
return;
|
||||
|
||||
m_modules.clear();
|
||||
g_moduleCollector.dest = &m_modules;
|
||||
m_fns.EnumerateRemoteSectionsAndModules(m_handle, sectionCallback, moduleCallback);
|
||||
g_moduleCollector.dest = nullptr;
|
||||
|
||||
// Set base to first module if we got any
|
||||
if (!m_modules.isEmpty() && m_base == 0)
|
||||
m_base = m_modules.first().base;
|
||||
}
|
||||
47
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h
Normal file
47
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
#include "../../src/providers/provider.h"
|
||||
#include "ReClassNET_Plugin.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
/**
|
||||
* Provider that bridges ReClass.NET native plugin DLL calls
|
||||
* to the ReclassX Provider interface.
|
||||
*/
|
||||
class RcNetCompatProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
RcNetCompatProvider(const RcNetFunctions& fns, uint32_t pid,
|
||||
const QString& processName);
|
||||
~RcNetCompatProvider() override;
|
||||
|
||||
// Required overrides
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
int size() const override;
|
||||
|
||||
// Optional overrides
|
||||
bool write(uint64_t addr, const void* buf, int len) override;
|
||||
bool isWritable() const override { return m_fns.WriteRemoteMemory != nullptr; }
|
||||
QString name() const override { return m_processName; }
|
||||
QString kind() const override { return QStringLiteral("RcNet"); }
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
|
||||
struct ModuleInfo {
|
||||
QString name;
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
};
|
||||
|
||||
private:
|
||||
void cacheModules();
|
||||
|
||||
RcNetFunctions m_fns;
|
||||
RC_Pointer m_handle = nullptr;
|
||||
uint32_t m_pid;
|
||||
QString m_processName;
|
||||
uint64_t m_base = 0;
|
||||
QVector<ModuleInfo> m_modules;
|
||||
};
|
||||
140
plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp
Normal file
140
plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
// Subset of ReClass.NET native plugin types needed for the compatibility layer.
|
||||
// Based on the ReClass.NET NativeCore plugin interface.
|
||||
// Only types required by the 8 supported exports are included (no debug types).
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define RC_CALLCONV __stdcall
|
||||
#else
|
||||
#define RC_CALLCONV
|
||||
#endif
|
||||
|
||||
// -- Basic types ----------------------------------------------------------
|
||||
|
||||
using RC_Pointer = void*;
|
||||
using RC_Size = uint64_t;
|
||||
using RC_UnicodeChar = char16_t;
|
||||
|
||||
// -- Enums ----------------------------------------------------------------
|
||||
|
||||
enum class ProcessAccess
|
||||
{
|
||||
Read = 0,
|
||||
Write = 1,
|
||||
Full = 2
|
||||
};
|
||||
|
||||
enum class SectionProtection
|
||||
{
|
||||
NoAccess = 0,
|
||||
Read = 1,
|
||||
Write = 2,
|
||||
Execute = 4,
|
||||
Guard = 8
|
||||
};
|
||||
|
||||
enum class SectionType
|
||||
{
|
||||
Unknown = 0,
|
||||
Private = 1,
|
||||
Mapped = 2,
|
||||
Image = 3
|
||||
};
|
||||
|
||||
enum class SectionCategory
|
||||
{
|
||||
Unknown = 0,
|
||||
CODE = 1,
|
||||
DATA = 2,
|
||||
HEAP = 3
|
||||
};
|
||||
|
||||
enum class ControlRemoteProcessAction
|
||||
{
|
||||
Suspend = 0,
|
||||
Resume = 1,
|
||||
Terminate = 2
|
||||
};
|
||||
|
||||
// -- Callback data structures ---------------------------------------------
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
struct EnumerateProcessData
|
||||
{
|
||||
RC_Size Id;
|
||||
RC_UnicodeChar Name[260];
|
||||
RC_UnicodeChar Path[260];
|
||||
};
|
||||
|
||||
struct EnumerateRemoteSectionData
|
||||
{
|
||||
RC_Pointer BaseAddress;
|
||||
RC_Size Size;
|
||||
SectionType Type;
|
||||
SectionCategory Category;
|
||||
SectionProtection Protection;
|
||||
RC_UnicodeChar Name[16];
|
||||
RC_UnicodeChar ModulePath[260];
|
||||
};
|
||||
|
||||
struct EnumerateRemoteModuleData
|
||||
{
|
||||
RC_Pointer BaseAddress;
|
||||
RC_Size Size;
|
||||
RC_UnicodeChar Path[260];
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
// -- Callback typedefs ----------------------------------------------------
|
||||
|
||||
using EnumerateProcessCallback = void(RC_CALLCONV*)(EnumerateProcessData* data);
|
||||
using EnumerateRemoteSectionsCallback = void(RC_CALLCONV*)(EnumerateRemoteSectionData* data);
|
||||
using EnumerateRemoteModulesCallback = void(RC_CALLCONV*)(EnumerateRemoteModuleData* data);
|
||||
|
||||
// -- Function pointer typedefs for resolved exports -----------------------
|
||||
|
||||
using FnEnumerateProcesses = void(RC_CALLCONV*)(EnumerateProcessCallback callback);
|
||||
|
||||
using FnOpenRemoteProcess = RC_Pointer(RC_CALLCONV*)(RC_Size id, ProcessAccess desiredAccess);
|
||||
|
||||
using FnIsProcessValid = bool(RC_CALLCONV*)(RC_Pointer handle);
|
||||
|
||||
using FnCloseRemoteProcess = void(RC_CALLCONV*)(RC_Pointer handle);
|
||||
|
||||
using FnReadRemoteMemory = bool(RC_CALLCONV*)(RC_Pointer handle,
|
||||
RC_Pointer address,
|
||||
RC_Pointer buffer,
|
||||
int offset,
|
||||
int size);
|
||||
|
||||
using FnWriteRemoteMemory = bool(RC_CALLCONV*)(RC_Pointer handle,
|
||||
RC_Pointer address,
|
||||
RC_Pointer buffer,
|
||||
int offset,
|
||||
int size);
|
||||
|
||||
using FnEnumerateRemoteSectionsAndModules =
|
||||
void(RC_CALLCONV*)(RC_Pointer handle,
|
||||
EnumerateRemoteSectionsCallback sectionCallback,
|
||||
EnumerateRemoteModulesCallback moduleCallback);
|
||||
|
||||
using FnControlRemoteProcess = void(RC_CALLCONV*)(RC_Pointer handle,
|
||||
ControlRemoteProcessAction action);
|
||||
|
||||
// -- Resolved function table ----------------------------------------------
|
||||
|
||||
struct RcNetFunctions
|
||||
{
|
||||
FnEnumerateProcesses EnumerateProcesses = nullptr;
|
||||
FnOpenRemoteProcess OpenRemoteProcess = nullptr;
|
||||
FnIsProcessValid IsProcessValid = nullptr;
|
||||
FnCloseRemoteProcess CloseRemoteProcess = nullptr;
|
||||
FnReadRemoteMemory ReadRemoteMemory = nullptr;
|
||||
FnWriteRemoteMemory WriteRemoteMemory = nullptr;
|
||||
FnEnumerateRemoteSectionsAndModules EnumerateRemoteSectionsAndModules = nullptr;
|
||||
FnControlRemoteProcess ControlRemoteProcess = nullptr;
|
||||
};
|
||||
677
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs
Normal file
677
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs
Normal file
@@ -0,0 +1,677 @@
|
||||
// RcNetBridge -- in-process C# bridge for loading .NET ReClass.NET plugins.
|
||||
//
|
||||
// Called from C++ via ICLRRuntimeHost::ExecuteInDefaultAppDomain().
|
||||
// The single entry point is Bridge.Initialize(string arg) where arg is:
|
||||
// "<hex_address_of_RcNetFunctions>|<plugin_dll_path>"
|
||||
//
|
||||
// The bridge:
|
||||
// 1. Registers an AssemblyResolve handler that provides THIS assembly
|
||||
// when a plugin asks for "ReClassNET", so the stub types below satisfy
|
||||
// the plugin's type references.
|
||||
// 2. Loads the plugin assembly and finds an ICoreProcessFunctions
|
||||
// implementation.
|
||||
// 3. Creates [UnmanagedFunctionPointer] delegates wrapping each method.
|
||||
// 4. Writes the native-callable function pointers into the RcNetFunctions
|
||||
// struct at the address provided by C++.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// ===========================================================================
|
||||
// ReClass.NET stub types
|
||||
// These mirror the subset of types from the ReClass.NET assembly that
|
||||
// memory-reading plugins reference. When the CLR resolves "ReClassNET"
|
||||
// via our AssemblyResolve handler, it gets THIS assembly, and these types
|
||||
// satisfy the plugin's type references.
|
||||
//
|
||||
// Types are placed in the exact namespaces used by the real ReClass.NET
|
||||
// assembly so that plugins compiled against it resolve correctly.
|
||||
// ===========================================================================
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Memory -- section enums (referenced by EnumerateRemoteSectionData)
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Memory
|
||||
{
|
||||
public enum SectionProtection
|
||||
{
|
||||
NoAccess = 0,
|
||||
Read = 1,
|
||||
Write = 2,
|
||||
Execute = 4,
|
||||
Guard = 8
|
||||
}
|
||||
|
||||
public enum SectionType
|
||||
{
|
||||
Unknown = 0,
|
||||
Private = 1,
|
||||
Mapped = 2,
|
||||
Image = 3
|
||||
}
|
||||
|
||||
public enum SectionCategory
|
||||
{
|
||||
Unknown = 0,
|
||||
CODE = 1,
|
||||
DATA = 2,
|
||||
HEAP = 3
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Debugger -- debugger types (used by ICoreProcessFunctions)
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Debugger
|
||||
{
|
||||
public enum DebugContinueStatus
|
||||
{
|
||||
Handled = 0,
|
||||
NotHandled = 1
|
||||
}
|
||||
|
||||
public enum HardwareBreakpointRegister
|
||||
{
|
||||
InvalidRegister = 0,
|
||||
Dr0 = 1,
|
||||
Dr1 = 2,
|
||||
Dr2 = 3,
|
||||
Dr3 = 4
|
||||
}
|
||||
|
||||
public enum HardwareBreakpointTrigger
|
||||
{
|
||||
Execute = 0,
|
||||
Access = 1,
|
||||
Write = 2
|
||||
}
|
||||
|
||||
public enum HardwareBreakpointSize
|
||||
{
|
||||
Size1 = 1,
|
||||
Size2 = 2,
|
||||
Size4 = 4,
|
||||
Size8 = 8
|
||||
}
|
||||
|
||||
public struct ExceptionDebugInfo
|
||||
{
|
||||
public IntPtr ExceptionCode;
|
||||
public IntPtr ExceptionFlags;
|
||||
public IntPtr ExceptionAddress;
|
||||
public HardwareBreakpointRegister CausedBy;
|
||||
public RegisterInfo Registers;
|
||||
|
||||
public struct RegisterInfo
|
||||
{
|
||||
public IntPtr Rax, Rbx, Rcx, Rdx;
|
||||
public IntPtr Rdi, Rsi, Rsp, Rbp, Rip;
|
||||
public IntPtr R8, R9, R10, R11, R12, R13, R14, R15;
|
||||
}
|
||||
}
|
||||
|
||||
public struct DebugEvent
|
||||
{
|
||||
public DebugContinueStatus ContinueStatus;
|
||||
public IntPtr ProcessId;
|
||||
public IntPtr ThreadId;
|
||||
public ExceptionDebugInfo ExceptionInfo;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Core -- interface, enums, delegates, and data structs
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Core
|
||||
{
|
||||
public enum ProcessAccess
|
||||
{
|
||||
Read = 0,
|
||||
Write = 1,
|
||||
Full = 2
|
||||
}
|
||||
|
||||
public enum ControlRemoteProcessAction
|
||||
{
|
||||
Suspend = 0,
|
||||
Resume = 1,
|
||||
Terminate = 2
|
||||
}
|
||||
|
||||
public struct EnumerateProcessData
|
||||
{
|
||||
public IntPtr Id;
|
||||
public string Name;
|
||||
public string Path;
|
||||
}
|
||||
|
||||
public struct EnumerateRemoteSectionData
|
||||
{
|
||||
public IntPtr BaseAddress;
|
||||
public IntPtr Size;
|
||||
public ReClassNET.Memory.SectionType Type;
|
||||
public ReClassNET.Memory.SectionCategory Category;
|
||||
public ReClassNET.Memory.SectionProtection Protection;
|
||||
public string Name;
|
||||
public string ModulePath;
|
||||
}
|
||||
|
||||
public struct EnumerateRemoteModuleData
|
||||
{
|
||||
public IntPtr BaseAddress;
|
||||
public IntPtr Size;
|
||||
public string Path;
|
||||
}
|
||||
|
||||
public delegate void EnumerateProcessCallback(ref EnumerateProcessData data);
|
||||
public delegate void EnumerateRemoteSectionCallback(ref EnumerateRemoteSectionData data);
|
||||
public delegate void EnumerateRemoteModuleCallback(ref EnumerateRemoteModuleData data);
|
||||
|
||||
public interface ICoreProcessFunctions
|
||||
{
|
||||
void EnumerateProcesses(EnumerateProcessCallback callbackProcess);
|
||||
IntPtr OpenRemoteProcess(IntPtr pid, ProcessAccess desiredAccess);
|
||||
bool IsProcessValid(IntPtr process);
|
||||
void CloseRemoteProcess(IntPtr process);
|
||||
bool ReadRemoteMemory(IntPtr process, IntPtr address, ref byte[] buffer, int offset, int size);
|
||||
bool WriteRemoteMemory(IntPtr process, IntPtr address, ref byte[] buffer, int offset, int size);
|
||||
void EnumerateRemoteSectionsAndModules(
|
||||
IntPtr process,
|
||||
EnumerateRemoteSectionCallback callbackSection,
|
||||
EnumerateRemoteModuleCallback callbackModule);
|
||||
void ControlRemoteProcess(IntPtr process, ControlRemoteProcessAction action);
|
||||
|
||||
// Debugger methods -- stubs required for interface compatibility
|
||||
bool AttachDebuggerToProcess(IntPtr id);
|
||||
void DetachDebuggerFromProcess(IntPtr id);
|
||||
bool AwaitDebugEvent(ref ReClassNET.Debugger.DebugEvent evt, int timeoutInMilliseconds);
|
||||
void HandleDebugEvent(ref ReClassNET.Debugger.DebugEvent evt);
|
||||
bool SetHardwareBreakpoint(IntPtr id, IntPtr address,
|
||||
ReClassNET.Debugger.HardwareBreakpointRegister register,
|
||||
ReClassNET.Debugger.HardwareBreakpointTrigger trigger,
|
||||
ReClassNET.Debugger.HardwareBreakpointSize size,
|
||||
bool set);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Memory -- RemoteProcess stub
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Memory
|
||||
{
|
||||
public class RemoteProcess { }
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Logger -- ILogger stub
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Logger
|
||||
{
|
||||
public interface ILogger { }
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Stub types for IPluginHost properties
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Forms
|
||||
{
|
||||
public class MainForm { }
|
||||
}
|
||||
|
||||
namespace ReClassNET
|
||||
{
|
||||
public class Settings { }
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// ReClassNET.Plugins
|
||||
// --------------------------------------------------------------------------
|
||||
namespace ReClassNET.Plugins
|
||||
{
|
||||
public abstract class Plugin : IDisposable
|
||||
{
|
||||
public virtual bool Initialize(IPluginHost host) { return true; }
|
||||
public virtual void Terminate() { }
|
||||
public virtual void Dispose() { }
|
||||
}
|
||||
|
||||
public interface IPluginHost
|
||||
{
|
||||
ReClassNET.Forms.MainForm MainWindow { get; }
|
||||
System.Resources.ResourceManager Resources { get; }
|
||||
ReClassNET.Memory.RemoteProcess Process { get; }
|
||||
ReClassNET.Logger.ILogger Logger { get; }
|
||||
ReClassNET.Settings Settings { get; }
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Bridge
|
||||
// ===========================================================================
|
||||
|
||||
namespace RcNetBridge
|
||||
{
|
||||
internal class StubPluginHost : ReClassNET.Plugins.IPluginHost
|
||||
{
|
||||
public ReClassNET.Forms.MainForm MainWindow => null;
|
||||
public System.Resources.ResourceManager Resources => null;
|
||||
public ReClassNET.Memory.RemoteProcess Process => null;
|
||||
public ReClassNET.Logger.ILogger Logger => null;
|
||||
public ReClassNET.Settings Settings => null;
|
||||
}
|
||||
|
||||
public class Bridge
|
||||
{
|
||||
// -- Persistent state (static so it survives after Initialize returns) --
|
||||
|
||||
private static ReClassNET.Core.ICoreProcessFunctions s_functions;
|
||||
private static readonly List<Delegate> s_pinned = new List<Delegate>();
|
||||
|
||||
// -- Entry point called from C++ --------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Called by ICLRRuntimeHost::ExecuteInDefaultAppDomain.
|
||||
/// arg = "<hex_address_of_RcNetFunctions>|<plugin_dll_path>"
|
||||
/// Returns 0 on success, non-zero error code on failure.
|
||||
/// </summary>
|
||||
public static int Initialize(string arg)
|
||||
{
|
||||
try
|
||||
{
|
||||
int sep = arg.IndexOf('|');
|
||||
if (sep < 0) return 1; // bad arg
|
||||
|
||||
long ptrValue = long.Parse(arg.Substring(0, sep), NumberStyles.HexNumber);
|
||||
IntPtr funcTablePtr = new IntPtr(ptrValue);
|
||||
string pluginPath = arg.Substring(sep + 1);
|
||||
|
||||
// Set up assembly resolution
|
||||
string pluginDir = Path.GetDirectoryName(pluginPath) ?? ".";
|
||||
string parentDir = Path.GetDirectoryName(pluginDir);
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) =>
|
||||
{
|
||||
string asmName = new AssemblyName(resolveArgs.Name).Name;
|
||||
|
||||
// Provide our own assembly as the "ReClass.NET" stub
|
||||
if (string.Equals(asmName, "ReClass.NET", StringComparison.OrdinalIgnoreCase))
|
||||
return typeof(Bridge).Assembly;
|
||||
|
||||
// Search plugin directory and parent for other dependencies
|
||||
string dllName = asmName + ".dll";
|
||||
foreach (string dir in new[] { pluginDir, parentDir })
|
||||
{
|
||||
if (dir == null) continue;
|
||||
string path = Path.Combine(dir, dllName);
|
||||
if (File.Exists(path))
|
||||
return Assembly.LoadFrom(path);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Load plugin and find ICoreProcessFunctions
|
||||
if (!LoadPlugin(pluginPath))
|
||||
return 2; // no implementation found
|
||||
|
||||
// Write function pointers
|
||||
WriteFunctionPointers(funcTablePtr);
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex) when (ex is ReflectionTypeLoadException || ex is FileNotFoundException)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
// -- Plugin loading ---------------------------------------------------
|
||||
|
||||
private static bool LoadPlugin(string pluginPath)
|
||||
{
|
||||
Assembly asm = Assembly.LoadFrom(pluginPath);
|
||||
|
||||
// Find a concrete type that implements ICoreProcessFunctions.
|
||||
// ReClass.NET plugins typically extend Plugin and directly
|
||||
// implement ICoreProcessFunctions on the same class.
|
||||
foreach (Type type in asm.GetExportedTypes())
|
||||
{
|
||||
if (type.IsAbstract || type.IsInterface) continue;
|
||||
|
||||
Type iface = type.GetInterfaces().FirstOrDefault(i =>
|
||||
i.FullName == "ReClassNET.Core.ICoreProcessFunctions");
|
||||
if (iface == null) continue;
|
||||
|
||||
object instance = Activator.CreateInstance(type);
|
||||
|
||||
// Try calling Initialize() but don't fail if it throws --
|
||||
// plugins use it for UI integration with the host app,
|
||||
// which we can't fully provide. The process functions
|
||||
// (ReadRemoteMemory, etc.) work without it.
|
||||
try
|
||||
{
|
||||
MethodInfo init = type.GetMethod("Initialize",
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null, new[] { typeof(ReClassNET.Plugins.IPluginHost) }, null);
|
||||
if (init != null)
|
||||
init.Invoke(instance, new object[] { new StubPluginHost() });
|
||||
}
|
||||
catch { }
|
||||
|
||||
s_functions = (ReClassNET.Core.ICoreProcessFunctions)instance;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// -- Native-callable delegate types -----------------------------------
|
||||
// These match the C++ RcNetFunctions struct field order exactly.
|
||||
// On x64 Windows all calling conventions collapse to the Microsoft
|
||||
// x64 ABI, so StdCall is used for documentation / x86 correctness.
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void DelEnumProcesses(IntPtr callback);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate IntPtr DelOpenRemoteProcess(ulong id, int access);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
delegate bool DelIsProcessValid(IntPtr handle);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void DelCloseRemoteProcess(IntPtr handle);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
delegate bool DelReadRemoteMemory(IntPtr handle, IntPtr address,
|
||||
IntPtr buffer, int offset, int size);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
delegate bool DelWriteRemoteMemory(IntPtr handle, IntPtr address,
|
||||
IntPtr buffer, int offset, int size);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void DelEnumSectionsAndModules(IntPtr handle,
|
||||
IntPtr sectionCallback, IntPtr moduleCallback);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void DelControlRemoteProcess(IntPtr handle, int action);
|
||||
|
||||
// Callback delegate types -- these point into C++ and are called by us.
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void NativeProcessCallback(IntPtr data);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void NativeSectionCallback(IntPtr data);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
delegate void NativeModuleCallback(IntPtr data);
|
||||
|
||||
// -- Write function pointers to the C++ struct ------------------------
|
||||
|
||||
private static void WriteFunctionPointers(IntPtr funcTable)
|
||||
{
|
||||
// RcNetFunctions layout: 8 consecutive function pointers.
|
||||
int i = 0;
|
||||
WriteSlot(funcTable, i++, Pin<DelEnumProcesses>(EnumProcessesImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelOpenRemoteProcess>(OpenProcessImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelIsProcessValid>(IsProcessValidImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelCloseRemoteProcess>(CloseProcessImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelReadRemoteMemory>(ReadMemoryImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelWriteRemoteMemory>(WriteMemoryImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelEnumSectionsAndModules>(EnumSectionsModulesImpl));
|
||||
WriteSlot(funcTable, i++, Pin<DelControlRemoteProcess>(ControlProcessImpl));
|
||||
}
|
||||
|
||||
private static IntPtr Pin<T>(T del) where T : class
|
||||
{
|
||||
Delegate d = del as Delegate;
|
||||
s_pinned.Add(d); // prevent GC
|
||||
return Marshal.GetFunctionPointerForDelegate(d);
|
||||
}
|
||||
|
||||
private static void WriteSlot(IntPtr table, int index, IntPtr value)
|
||||
{
|
||||
Marshal.WriteIntPtr(table, index * IntPtr.Size, value);
|
||||
}
|
||||
|
||||
// -- Implementation methods -------------------------------------------
|
||||
|
||||
// -- EnumerateProcesses --
|
||||
// C++ passes a native callback; we call the plugin, convert each
|
||||
// managed EnumerateProcessData to the packed native layout, and
|
||||
// forward to the native callback.
|
||||
|
||||
private static void EnumProcessesImpl(IntPtr nativeCallbackPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null || nativeCallbackPtr == IntPtr.Zero) return;
|
||||
|
||||
NativeProcessCallback nativeCb =
|
||||
Marshal.GetDelegateForFunctionPointer<NativeProcessCallback>(nativeCallbackPtr);
|
||||
|
||||
// Native layout (pack=1): uint64 Id + char16[260] Name + char16[260] Path
|
||||
const int kStructSize = 8 + 520 + 520; // 1048 bytes
|
||||
|
||||
s_functions.EnumerateProcesses(
|
||||
(ref ReClassNET.Core.EnumerateProcessData data) =>
|
||||
{
|
||||
IntPtr mem = Marshal.AllocHGlobal(kStructSize);
|
||||
try
|
||||
{
|
||||
// Zero-fill
|
||||
byte[] zeros = new byte[kStructSize];
|
||||
Marshal.Copy(zeros, 0, mem, kStructSize);
|
||||
|
||||
// Id (8 bytes at offset 0)
|
||||
Marshal.WriteInt64(mem, 0, data.Id.ToInt64());
|
||||
|
||||
// Name (char16[260] at offset 8)
|
||||
if (data.Name != null)
|
||||
{
|
||||
char[] chars = data.Name.ToCharArray();
|
||||
int count = Math.Min(chars.Length, 259);
|
||||
Marshal.Copy(chars, 0, new IntPtr(mem.ToInt64() + 8), count);
|
||||
}
|
||||
|
||||
// Path (char16[260] at offset 528)
|
||||
if (data.Path != null)
|
||||
{
|
||||
char[] chars = data.Path.ToCharArray();
|
||||
int count = Math.Min(chars.Length, 259);
|
||||
Marshal.Copy(chars, 0, new IntPtr(mem.ToInt64() + 528), count);
|
||||
}
|
||||
|
||||
nativeCb(mem);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(mem);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch { /* swallow -- don't crash the host process */ }
|
||||
}
|
||||
|
||||
// -- OpenRemoteProcess --
|
||||
private static IntPtr OpenProcessImpl(ulong id, int access)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null) return IntPtr.Zero;
|
||||
return s_functions.OpenRemoteProcess(
|
||||
new IntPtr((long)id),
|
||||
(ReClassNET.Core.ProcessAccess)access);
|
||||
}
|
||||
catch { return IntPtr.Zero; }
|
||||
}
|
||||
|
||||
// -- IsProcessValid --
|
||||
private static bool IsProcessValidImpl(IntPtr handle)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null) return false;
|
||||
return s_functions.IsProcessValid(handle);
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// -- CloseRemoteProcess --
|
||||
private static void CloseProcessImpl(IntPtr handle)
|
||||
{
|
||||
try { s_functions?.CloseRemoteProcess(handle); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
// -- ReadRemoteMemory --
|
||||
// C++ provides a native buffer pointer. We read into a managed array
|
||||
// via the plugin's interface, then copy to the native buffer.
|
||||
private static bool ReadMemoryImpl(IntPtr handle, IntPtr address,
|
||||
IntPtr buffer, int offset, int size)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null || size <= 0) return false;
|
||||
|
||||
byte[] managed = new byte[size];
|
||||
bool ok = s_functions.ReadRemoteMemory(
|
||||
handle, address, ref managed, 0, size);
|
||||
|
||||
if (ok)
|
||||
Marshal.Copy(managed, 0, new IntPtr(buffer.ToInt64() + offset), size);
|
||||
|
||||
return ok;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// -- WriteRemoteMemory --
|
||||
private static bool WriteMemoryImpl(IntPtr handle, IntPtr address,
|
||||
IntPtr buffer, int offset, int size)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null || size <= 0) return false;
|
||||
|
||||
byte[] managed = new byte[size];
|
||||
Marshal.Copy(new IntPtr(buffer.ToInt64() + offset), managed, 0, size);
|
||||
|
||||
return s_functions.WriteRemoteMemory(
|
||||
handle, address, ref managed, 0, size);
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
// -- EnumerateRemoteSectionsAndModules --
|
||||
private static void EnumSectionsModulesImpl(IntPtr handle,
|
||||
IntPtr sectionCallbackPtr, IntPtr moduleCallbackPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (s_functions == null) return;
|
||||
|
||||
// Section callback -- forward to native
|
||||
// Native layout (pack=1): RC_Pointer Base(8) + RC_Size Size(8) +
|
||||
// SectionType(4) + SectionCategory(4) + SectionProtection(4) +
|
||||
// char16 Name[16](32) + char16 ModulePath[260](520) = 580 bytes
|
||||
NativeSectionCallback nativeSectionCb = (sectionCallbackPtr != IntPtr.Zero)
|
||||
? Marshal.GetDelegateForFunctionPointer<NativeSectionCallback>(sectionCallbackPtr)
|
||||
: null;
|
||||
|
||||
// Module callback -- forward to native
|
||||
// Native layout (pack=1): RC_Pointer Base(8) + RC_Size Size(8) +
|
||||
// char16 Path[260](520) = 536 bytes
|
||||
NativeModuleCallback nativeModuleCb = (moduleCallbackPtr != IntPtr.Zero)
|
||||
? Marshal.GetDelegateForFunctionPointer<NativeModuleCallback>(moduleCallbackPtr)
|
||||
: null;
|
||||
|
||||
s_functions.EnumerateRemoteSectionsAndModules(handle,
|
||||
// Section callback
|
||||
(ref ReClassNET.Core.EnumerateRemoteSectionData sdata) =>
|
||||
{
|
||||
if (nativeSectionCb == null) return;
|
||||
|
||||
const int kSize = 8 + 8 + 4 + 4 + 4 + 32 + 520; // 580
|
||||
IntPtr mem = Marshal.AllocHGlobal(kSize);
|
||||
try
|
||||
{
|
||||
byte[] z = new byte[kSize];
|
||||
Marshal.Copy(z, 0, mem, kSize);
|
||||
|
||||
Marshal.WriteInt64(mem, 0, sdata.BaseAddress.ToInt64());
|
||||
Marshal.WriteInt64(mem, 8, sdata.Size.ToInt64());
|
||||
Marshal.WriteInt32(mem, 16, (int)sdata.Type);
|
||||
Marshal.WriteInt32(mem, 20, (int)sdata.Category);
|
||||
Marshal.WriteInt32(mem, 24, (int)sdata.Protection);
|
||||
|
||||
if (sdata.Name != null)
|
||||
{
|
||||
char[] c = sdata.Name.ToCharArray();
|
||||
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 28),
|
||||
Math.Min(c.Length, 15));
|
||||
}
|
||||
if (sdata.ModulePath != null)
|
||||
{
|
||||
char[] c = sdata.ModulePath.ToCharArray();
|
||||
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 60),
|
||||
Math.Min(c.Length, 259));
|
||||
}
|
||||
|
||||
nativeSectionCb(mem);
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(mem); }
|
||||
},
|
||||
// Module callback
|
||||
(ref ReClassNET.Core.EnumerateRemoteModuleData mdata) =>
|
||||
{
|
||||
if (nativeModuleCb == null) return;
|
||||
|
||||
const int kSize = 8 + 8 + 520; // 536
|
||||
IntPtr mem = Marshal.AllocHGlobal(kSize);
|
||||
try
|
||||
{
|
||||
byte[] z = new byte[kSize];
|
||||
Marshal.Copy(z, 0, mem, kSize);
|
||||
|
||||
Marshal.WriteInt64(mem, 0, mdata.BaseAddress.ToInt64());
|
||||
Marshal.WriteInt64(mem, 8, mdata.Size.ToInt64());
|
||||
|
||||
if (mdata.Path != null)
|
||||
{
|
||||
char[] c = mdata.Path.ToCharArray();
|
||||
Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 16),
|
||||
Math.Min(c.Length, 259));
|
||||
}
|
||||
|
||||
nativeModuleCb(mem);
|
||||
}
|
||||
finally { Marshal.FreeHGlobal(mem); }
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// -- ControlRemoteProcess --
|
||||
private static void ControlProcessImpl(IntPtr handle, int action)
|
||||
{
|
||||
try
|
||||
{
|
||||
s_functions?.ControlRemoteProcess(handle,
|
||||
(ReClassNET.Core.ControlRemoteProcessAction)action);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
12
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj
Normal file
12
plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<AssemblyName>RcNetBridge</AssemblyName>
|
||||
<RootNamespace>RcNetBridge</RootNamespace>
|
||||
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -304,7 +304,7 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
bool result = false;
|
||||
dispatchToOwner([&]() {
|
||||
ULONG bytesRead = 0;
|
||||
HRESULT hr = m_dataSpaces->ReadVirtual(m_base + addr, buf, (ULONG)len, &bytesRead);
|
||||
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
|
||||
if (FAILED(hr) || (int)bytesRead < len)
|
||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||
result = bytesRead > 0;
|
||||
@@ -324,7 +324,7 @@ bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
bool result = false;
|
||||
dispatchToOwner([&]() {
|
||||
ULONG bytesWritten = 0;
|
||||
HRESULT hr = m_dataSpaces->WriteVirtual(m_base + addr, const_cast<void*>(buf),
|
||||
HRESULT hr = m_dataSpaces->WriteVirtual(addr, const_cast<void*>(buf),
|
||||
(ULONG)len, &bytesWritten);
|
||||
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
|
||||
});
|
||||
@@ -364,7 +364,7 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
|
||||
char nameBuf[512] = {};
|
||||
ULONG nameSize = 0;
|
||||
ULONG64 displacement = 0;
|
||||
HRESULT hr = m_symbols->GetNameByOffset(m_base + addr, nameBuf, sizeof(nameBuf),
|
||||
HRESULT hr = m_symbols->GetNameByOffset(addr, nameBuf, sizeof(nameBuf),
|
||||
&nameSize, &displacement);
|
||||
if (SUCCEEDED(hr) && nameSize > 0) {
|
||||
result = QString::fromUtf8(nameBuf);
|
||||
|
||||
@@ -62,7 +62,6 @@ public:
|
||||
|
||||
bool isLive() const override { return m_isLive; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
void setBase(uint64_t b) override { m_base = b; }
|
||||
|
||||
private:
|
||||
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
|
||||
|
||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB |
182
src/compose.cpp
182
src/compose.cpp
@@ -14,13 +14,15 @@ constexpr uint64_t kGoldenRatio = 0x9E3779B97F4A7C15ULL;
|
||||
struct ComposeState {
|
||||
QString text;
|
||||
QVector<LineMeta> meta;
|
||||
QSet<uint64_t> visiting; // cycle detection for struct recursion
|
||||
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
|
||||
QSet<uint64_t> visiting; // cycle detection for struct recursion
|
||||
QSet<qulonglong> ptrVisiting; // cycle guard for pointer expansions
|
||||
QSet<uint64_t> virtualPtrRefs; // refIds currently being virtually expanded via pointer deref
|
||||
int currentLine = 0;
|
||||
int typeW = kColType; // global type column width (fallback)
|
||||
int nameW = kColName; // global name column width (fallback)
|
||||
int offsetHexDigits = 8; // hex digit tier for offset margin
|
||||
bool baseEmitted = false; // only first root struct shows base address
|
||||
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
||||
|
||||
// Precomputed for O(1) lookups
|
||||
QHash<uint64_t, QVector<int>> childMap;
|
||||
@@ -64,7 +66,6 @@ uint32_t computeMarkers(const Node& node, const Provider& /*prov*/,
|
||||
uint64_t /*addr*/, bool isCont, int /*depth*/) {
|
||||
uint32_t mask = 0;
|
||||
if (isCont) mask |= (1u << M_CONT);
|
||||
if (node.kind == NodeKind::Padding) mask |= (1u << M_PAD);
|
||||
// No ambient validation markers — errors only shown during inline editing.
|
||||
return mask;
|
||||
}
|
||||
@@ -77,12 +78,6 @@ static QString resolvePointerTarget(const NodeTree& tree, uint64_t refId) {
|
||||
return ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName;
|
||||
}
|
||||
|
||||
static inline uint64_t ptrToProviderAddr(const NodeTree& tree, uint64_t ptr) {
|
||||
if (tree.baseAddress == 0) return ptr;
|
||||
if (ptr >= tree.baseAddress) return ptr - tree.baseAddress;
|
||||
return UINT64_MAX; // Invalid: ptr below base address
|
||||
}
|
||||
|
||||
static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) {
|
||||
int64_t total = 0;
|
||||
QSet<uint64_t> visited;
|
||||
@@ -118,14 +113,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
int typeW = state.effectiveTypeW(scopeId);
|
||||
int nameW = state.effectiveNameW(scopeId);
|
||||
|
||||
// Line count: padding wraps at 8 bytes per line
|
||||
int numLines;
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
int totalBytes = qMax(1, node.arrayLen);
|
||||
numLines = (totalBytes + 7) / 8;
|
||||
} else {
|
||||
numLines = linesForKind(node.kind);
|
||||
}
|
||||
int numLines = linesForKind(node.kind);
|
||||
|
||||
// Resolve pointer target name for display
|
||||
QString ptrTypeOverride;
|
||||
@@ -146,8 +134,9 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
lm.isContinuation = isCont;
|
||||
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.effectiveTypeW = typeW;
|
||||
@@ -156,12 +145,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
|
||||
// Set byte count for hex preview lines (used for per-byte change highlighting)
|
||||
if (isHexPreview(node.kind)) {
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
int totalSz = qMax(1, node.arrayLen);
|
||||
lm.lineByteCount = qMin(8, totalSz - sub * 8);
|
||||
} else {
|
||||
lm.lineByteCount = sizeForKind(node.kind);
|
||||
}
|
||||
lm.lineByteCount = sizeForKind(node.kind);
|
||||
}
|
||||
|
||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||
@@ -197,8 +181,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
@@ -215,8 +200,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::ArrayElementSeparator;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
@@ -244,8 +230,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.isRootHeader = false;
|
||||
lm.foldHead = true;
|
||||
@@ -307,8 +294,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.nodeKind = node.elementKind;
|
||||
lm.isArrayElement = true;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + elemAddr;
|
||||
lm.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;
|
||||
@@ -359,9 +347,10 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.depth = childDepth;
|
||||
lm.lineKind = LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(
|
||||
tree.baseAddress + absAddr + child.offset, false,
|
||||
absAddr + child.offset, false,
|
||||
state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr + child.offset;
|
||||
lm.offsetAddr = absAddr + child.offset;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = child.kind;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = true;
|
||||
@@ -404,8 +393,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
int sz = tree.structSpan(node.id, &state.childMap);
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr + sz;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr + sz;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
|
||||
}
|
||||
|
||||
@@ -430,29 +420,43 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
QString ptrTargetName = resolvePointerTarget(tree, node.refId);
|
||||
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||
|
||||
// Check if this pointer has materialized children (from materializeRefChildren)
|
||||
QVector<int> ptrChildren = state.childMap.value(node.id);
|
||||
bool hasMaterialized = !ptrChildren.isEmpty();
|
||||
|
||||
// Force collapsed if this refId is already being virtually expanded
|
||||
// (prevents infinite recursion in virtual expansion mode).
|
||||
// Materialized children bypass this — they are real tree nodes with
|
||||
// independent collapsed state, so recursion is bounded by the tree.
|
||||
bool forceCollapsed = !hasMaterialized
|
||||
&& state.virtualPtrRefs.contains(node.refId);
|
||||
bool effectiveCollapsed = node.collapsed || forceCollapsed;
|
||||
|
||||
// Emit merged fold header: "Type* Name {" (expanded) or "Type* Name -> val" (collapsed)
|
||||
{
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress + absAddr;
|
||||
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.foldHead = true;
|
||||
lm.foldCollapsed = node.collapsed;
|
||||
lm.foldCollapsed = effectiveCollapsed;
|
||||
lm.foldLevel = computeFoldLevel(depth, true);
|
||||
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
|
||||
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
|
||||
lm.effectiveTypeW = typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
lm.pointerTargetName = ptrTargetName;
|
||||
state.emitLine(fmt::fmtPointerHeader(node, depth, node.collapsed,
|
||||
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
||||
prov, absAddr, ptrTypeOverride,
|
||||
typeW, nameW), lm);
|
||||
}
|
||||
|
||||
if (!node.collapsed) {
|
||||
if (!effectiveCollapsed) {
|
||||
int sz = node.byteSize();
|
||||
uint64_t ptrVal = 0;
|
||||
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
|
||||
@@ -462,38 +466,62 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
// Treat sentinel values as invalid pointers
|
||||
if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
|
||||
ptrVal = 0;
|
||||
else {
|
||||
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
|
||||
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if pointer target is actually readable
|
||||
uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0;
|
||||
// Pointer target address is used directly (absolute)
|
||||
uint64_t pBase = ptrVal;
|
||||
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
|
||||
|
||||
// For invalid/unreadable pointers: use NullProvider (shows zeros)
|
||||
// and reset margin offsets (unsigned wrap cancels baseAddress)
|
||||
static NullProvider s_nullProv;
|
||||
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv);
|
||||
if (!ptrReadable)
|
||||
pBase = (uint64_t)0 - tree.baseAddress;
|
||||
pBase = 0;
|
||||
|
||||
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
|
||||
if (!state.ptrVisiting.contains(key)) {
|
||||
state.ptrVisiting.insert(key);
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
const Node& ref = tree.nodes[refIdx];
|
||||
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array)
|
||||
composeParent(state, tree, childProv, refIdx,
|
||||
depth, pBase, ref.id,
|
||||
/*isArrayChild=*/true);
|
||||
uint64_t savedPtrBase = state.currentPtrBase;
|
||||
state.currentPtrBase = pBase;
|
||||
|
||||
if (hasMaterialized) {
|
||||
// Render materialized children at the pointer target address.
|
||||
// These are real tree nodes with independent state — use rootId
|
||||
// so resolveAddr computes offsets relative to the pointer target.
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
// Virtual expansion via ref struct definition.
|
||||
// Temporarily remove the ref struct from visiting so composeParent
|
||||
// doesn't hit the struct-level cycle guard. The ptrVisiting mechanism
|
||||
// handles actual address-level pointer cycles, and virtualPtrRefs
|
||||
// prevents infinite virtual recursion (inner self-referential pointers
|
||||
// are force-collapsed with M_CYCLE for the user to materialize).
|
||||
qulonglong key = pBase ^ (node.refId * kGoldenRatio);
|
||||
if (!state.ptrVisiting.contains(key)) {
|
||||
state.ptrVisiting.insert(key);
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
const Node& ref = tree.nodes[refIdx];
|
||||
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) {
|
||||
bool wasVisiting = state.visiting.remove(node.refId);
|
||||
state.virtualPtrRefs.insert(node.refId);
|
||||
composeParent(state, tree, childProv, refIdx,
|
||||
depth, pBase, ref.id,
|
||||
/*isArrayChild=*/true);
|
||||
state.virtualPtrRefs.remove(node.refId);
|
||||
if (wasVisiting) state.visiting.insert(node.refId);
|
||||
}
|
||||
}
|
||||
state.ptrVisiting.remove(key);
|
||||
}
|
||||
state.ptrVisiting.remove(key);
|
||||
}
|
||||
|
||||
state.currentPtrBase = savedPtrBase;
|
||||
|
||||
// Footer for pointer fold
|
||||
{
|
||||
LineMeta lm;
|
||||
@@ -527,16 +555,16 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
state.childMap[tree.nodes[i].parentId].append(i);
|
||||
|
||||
// Precompute absolute offsets
|
||||
// Precompute absolute offsets (baseAddress + structure-relative offset)
|
||||
state.absOffsets.resize(tree.nodes.size());
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
state.absOffsets[i] = tree.computeOffset(i);
|
||||
state.absOffsets[i] = tree.baseAddress + tree.computeOffset(i);
|
||||
|
||||
// Compute hex digit tier from max absolute address
|
||||
{
|
||||
uint64_t maxAddr = tree.baseAddress;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
uint64_t addr = tree.baseAddress + (uint64_t)state.absOffsets[i];
|
||||
uint64_t addr = (uint64_t)state.absOffsets[i];
|
||||
if (addr > maxAddr) maxAddr = addr;
|
||||
}
|
||||
if (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4;
|
||||
@@ -571,7 +599,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
// Include struct/array names - they now use columnar layout too
|
||||
int maxNameLen = kMinNameW;
|
||||
for (const Node& node : tree.nodes) {
|
||||
// Skip hex/padding (they show ASCII preview, not name column)
|
||||
// Skip hex (they show ASCII preview, not name column)
|
||||
if (isHexPreview(node.kind)) continue;
|
||||
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
||||
}
|
||||
@@ -590,7 +618,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
||||
|
||||
// Name width (skip hex/padding, but include containers)
|
||||
// Name width (skip hex, but include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
|
||||
}
|
||||
@@ -622,7 +650,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
||||
|
||||
// Name width (skip hex/padding, include containers)
|
||||
// Name width (skip hex, include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
||||
}
|
||||
@@ -632,7 +660,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
}
|
||||
|
||||
// Emit CommandRow as line 0 (combined: source + address + root class type + name)
|
||||
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct\u25BE NoName {");
|
||||
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct NoName {");
|
||||
{
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = -1;
|
||||
@@ -643,6 +671,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
lm.foldHead = false;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = tree.baseAddress;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.markerMask = 0;
|
||||
lm.effectiveTypeW = state.typeW;
|
||||
lm.effectiveNameW = state.nameW;
|
||||
@@ -703,20 +732,5 @@ QSet<uint64_t> NodeTree::normalizePreferDescendants(const QSet<uint64_t>& ids) c
|
||||
return result;
|
||||
}
|
||||
|
||||
int NodeTree::computeStructAlignment(uint64_t structId) const {
|
||||
int idx = indexOfId(structId);
|
||||
if (idx < 0) return 1;
|
||||
int maxAlign = 1;
|
||||
QVector<int> kids = childrenOf(structId);
|
||||
for (int ci : kids) {
|
||||
const Node& c = nodes[ci];
|
||||
if (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) {
|
||||
maxAlign = qMax(maxAlign, computeStructAlignment(c.id));
|
||||
} else {
|
||||
maxAlign = qMax(maxAlign, alignmentFor(c.kind));
|
||||
}
|
||||
}
|
||||
return maxAlign;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
||||
#include <QUndoCommand>
|
||||
#include <QTimer>
|
||||
#include <QFutureWatcher>
|
||||
#include <QPointer>
|
||||
#include <memory>
|
||||
|
||||
namespace rcx {
|
||||
@@ -84,6 +85,7 @@ public:
|
||||
void removeSplitEditor(RcxEditor* editor);
|
||||
QList<RcxEditor*> editors() const { return m_editors; }
|
||||
|
||||
void convertRootKeyword(const QString& newKeyword);
|
||||
void changeNodeKind(int nodeIdx, NodeKind newKind);
|
||||
void renameNode(int nodeIdx, const QString& newName);
|
||||
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
||||
@@ -92,6 +94,8 @@ public:
|
||||
void materializeRefChildren(int nodeIdx);
|
||||
void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false);
|
||||
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);
|
||||
@@ -112,6 +116,7 @@ public:
|
||||
|
||||
RcxDocument* document() const { return m_doc; }
|
||||
void setEditorFont(const QString& fontName);
|
||||
void setRefreshInterval(int ms);
|
||||
|
||||
// MCP bridge accessors
|
||||
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
||||
@@ -120,6 +125,13 @@ public:
|
||||
int activeSourceIndex() const { return m_activeSourceIdx; }
|
||||
void switchSource(int idx) { switchToSavedSource(idx); }
|
||||
|
||||
// Value tracking toggle (per-tab, off by default)
|
||||
bool trackValues() const { return m_trackValues; }
|
||||
void setTrackValues(bool on);
|
||||
|
||||
// Test accessor
|
||||
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||
|
||||
signals:
|
||||
void nodeSelected(int nodeIdx);
|
||||
void selectionChanged(int count);
|
||||
@@ -138,7 +150,7 @@ private:
|
||||
int m_activeSourceIdx = -1;
|
||||
|
||||
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
|
||||
TypeSelectorPopup* m_cachedPopup = nullptr;
|
||||
QPointer<TypeSelectorPopup> m_cachedPopup;
|
||||
|
||||
// ── Auto-refresh state ──
|
||||
using PageMap = QHash<uint64_t, QByteArray>;
|
||||
@@ -147,6 +159,8 @@ private:
|
||||
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
||||
PageMap m_prevPages;
|
||||
QSet<int64_t> m_changedOffsets;
|
||||
QHash<uint64_t, ValueHistory> m_valueHistory;
|
||||
bool m_trackValues = false;
|
||||
uint64_t m_refreshGen = 0;
|
||||
uint64_t m_readGen = 0;
|
||||
bool m_readInFlight = false;
|
||||
@@ -154,7 +168,6 @@ private:
|
||||
void connectEditor(RcxEditor* editor);
|
||||
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
|
||||
void updateCommandRow();
|
||||
void performRealignment(uint64_t structId, int targetAlign);
|
||||
void switchToSavedSource(int idx);
|
||||
void pushSavedSourcesToEditors();
|
||||
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);
|
||||
@@ -169,7 +182,7 @@ private:
|
||||
void resetSnapshot();
|
||||
void collectPointerRanges(uint64_t structId, uint64_t memBase,
|
||||
int depth, int maxDepth,
|
||||
QSet<uint64_t>& visited,
|
||||
QSet<QPair<uint64_t,uint64_t>>& visited,
|
||||
QVector<QPair<uint64_t,int>>& ranges) const;
|
||||
};
|
||||
|
||||
|
||||
101
src/core.h
101
src/core.h
@@ -8,6 +8,7 @@
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
|
||||
@@ -25,23 +26,23 @@ enum class NodeKind : uint8_t {
|
||||
UInt8, UInt16, UInt32, UInt64,
|
||||
Float, Double, Bool,
|
||||
Pointer32, Pointer64,
|
||||
FuncPtr32, FuncPtr64,
|
||||
Vec2, Vec3, Vec4, Mat4x4,
|
||||
UTF8, UTF16,
|
||||
Padding,
|
||||
Struct, Array
|
||||
};
|
||||
|
||||
} // namespace rcx (temporarily close for qHash)
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return ::qHash(static_cast<uint8_t>(key), seed); }
|
||||
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return qHash(static_cast<int>(key), seed); }
|
||||
#endif
|
||||
namespace rcx { // reopen
|
||||
|
||||
// ── Kind flags (replaces repeated Hex/Padding switches) ──
|
||||
// ── Kind flags (replaces repeated Hex switches) ──
|
||||
|
||||
enum KindFlags : uint32_t {
|
||||
KF_None = 0,
|
||||
KF_HexPreview = 1 << 0, // Hex8..Hex64 + Padding (ASCII+hex layout)
|
||||
KF_HexPreview = 1 << 0, // Hex8..Hex64 (ASCII+hex layout)
|
||||
KF_Container = 1 << 1, // Struct/Array
|
||||
KF_String = 1 << 2, // UTF8/UTF16
|
||||
KF_Vector = 1 << 3, // Vec2/3/4
|
||||
@@ -78,13 +79,14 @@ inline constexpr KindMeta kKindMeta[] = {
|
||||
{NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None},
|
||||
{NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None},
|
||||
{NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None},
|
||||
{NodeKind::FuncPtr32, "FuncPtr32", "fnptr32", 4, 1, 4, KF_None},
|
||||
{NodeKind::FuncPtr64, "FuncPtr64", "fnptr64", 8, 1, 8, KF_None},
|
||||
{NodeKind::Vec2, "Vec2", "vec2", 8, 1, 4, KF_Vector},
|
||||
{NodeKind::Vec3, "Vec3", "vec3", 12, 1, 4, KF_Vector},
|
||||
{NodeKind::Vec4, "Vec4", "vec4", 16, 1, 4, KF_Vector},
|
||||
{NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None},
|
||||
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String},
|
||||
{NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String},
|
||||
{NodeKind::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview},
|
||||
{NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
|
||||
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
|
||||
};
|
||||
@@ -137,6 +139,9 @@ inline constexpr bool isVectorKind(NodeKind k) {
|
||||
inline constexpr bool isMatrixKind(NodeKind k) {
|
||||
return k == NodeKind::Mat4x4;
|
||||
}
|
||||
inline constexpr bool isFuncPtr(NodeKind k) {
|
||||
return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64;
|
||||
}
|
||||
|
||||
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
||||
QStringList out;
|
||||
@@ -155,7 +160,6 @@ inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
||||
|
||||
enum Marker : int {
|
||||
M_CONT = 0,
|
||||
M_PAD = 1,
|
||||
M_PTR0 = 2,
|
||||
M_CYCLE = 3,
|
||||
M_ERR = 4,
|
||||
@@ -187,9 +191,12 @@ struct Node {
|
||||
int byteSize() const {
|
||||
switch (kind) {
|
||||
case NodeKind::UTF8: return strLen;
|
||||
case NodeKind::UTF16: return strLen * 2;
|
||||
case NodeKind::Padding: return qMax(1, arrayLen);
|
||||
case NodeKind::Array: return arrayLen * sizeForKind(elementKind);
|
||||
case NodeKind::UTF16: return qMin(strLen, INT_MAX / 2) * 2;
|
||||
case NodeKind::Array: {
|
||||
int elemSz = sizeForKind(elementKind);
|
||||
if (elemSz <= 0) return 0;
|
||||
return qMin(arrayLen, INT_MAX / elemSz) * elemSz;
|
||||
}
|
||||
default: return sizeForKind(kind);
|
||||
}
|
||||
}
|
||||
@@ -221,8 +228,8 @@ struct Node {
|
||||
n.classKeyword = o["classKeyword"].toString();
|
||||
n.parentId = o["parentId"].toString("0").toULongLong();
|
||||
n.offset = o["offset"].toInt(0);
|
||||
n.arrayLen = o["arrayLen"].toInt(1);
|
||||
n.strLen = o["strLen"].toInt(64);
|
||||
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
|
||||
n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
|
||||
n.collapsed = o["collapsed"].toBool(false);
|
||||
n.refId = o["refId"].toString("0").toULongLong();
|
||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||
@@ -373,9 +380,6 @@ struct NodeTree {
|
||||
return qMax(declaredSize, maxEnd);
|
||||
}
|
||||
|
||||
// Compute natural alignment of a struct (max alignment of direct children)
|
||||
int computeStructAlignment(uint64_t structId) const;
|
||||
|
||||
// Batch selection normalizers
|
||||
QSet<uint64_t> normalizePreferAncestors(const QSet<uint64_t>& ids) const;
|
||||
QSet<uint64_t> normalizePreferDescendants(const QSet<uint64_t>& ids) const;
|
||||
@@ -405,6 +409,49 @@ struct NodeTree {
|
||||
|
||||
};
|
||||
|
||||
// ── Value History (ring buffer for heatmap) ──
|
||||
|
||||
struct ValueHistory {
|
||||
static constexpr int kCapacity = 10;
|
||||
std::array<QString, kCapacity> values;
|
||||
int count = 0; // total unique values recorded
|
||||
int head = 0; // next write position in ring
|
||||
|
||||
void record(const QString& v) {
|
||||
if (count > 0) {
|
||||
int last = (head + kCapacity - 1) % kCapacity;
|
||||
if (values[last] == v) return; // no change
|
||||
}
|
||||
values[head] = v;
|
||||
head = (head + 1) % kCapacity;
|
||||
if (count < INT_MAX) count++;
|
||||
}
|
||||
|
||||
int uniqueCount() const { return qMin(count, kCapacity); }
|
||||
|
||||
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
|
||||
int heatLevel() const {
|
||||
if (count <= 1) return 0;
|
||||
if (count == 2) return 1;
|
||||
if (count <= 4) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
QString last() const {
|
||||
if (count == 0) return {};
|
||||
return values[(head + kCapacity - 1) % kCapacity];
|
||||
}
|
||||
|
||||
// Iterate from oldest to newest (up to uniqueCount entries)
|
||||
template<typename Fn>
|
||||
void forEach(Fn&& fn) const {
|
||||
int n = uniqueCount();
|
||||
int start = (head + kCapacity - n) % kCapacity;
|
||||
for (int i = 0; i < n; i++)
|
||||
fn(values[(start + i) % kCapacity]);
|
||||
}
|
||||
};
|
||||
|
||||
// ── LineMeta ──
|
||||
|
||||
enum class LineKind : uint8_t {
|
||||
@@ -437,8 +484,10 @@ struct LineMeta {
|
||||
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
|
||||
QString offsetText;
|
||||
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
|
||||
uint64_t ptrBase = 0; // Pointer expansion base (non-zero = use for RVA)
|
||||
uint32_t markerMask = 0;
|
||||
bool dataChanged = false; // true if any byte in this node changed since last refresh
|
||||
int heatLevel = 0; // 0=static, 1=cold, 2=warm, 3=hot (from ValueHistory)
|
||||
QVector<int> changedByteIndices; // Hex preview: which byte indices (0-based) changed on this line
|
||||
int lineByteCount = 0; // Hex preview: actual data byte count on this line
|
||||
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
||||
@@ -535,7 +584,7 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int start = ind + typeW + kSepWidth;
|
||||
|
||||
// Hex/Padding: ASCII preview occupies the name column (padded to nameW)
|
||||
// Hex: ASCII preview occupies the name column (padded to nameW)
|
||||
if (isHexPreview(lm.nodeKind))
|
||||
return {start, start + nameW, true};
|
||||
|
||||
@@ -547,9 +596,9 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
||||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
// Hex/Padding uses nameW for ASCII column (same as regular name column)
|
||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHexPad ? 23 : kColValue;
|
||||
// Hex uses nameW for ASCII column (same as regular name column)
|
||||
bool isHex = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHex ? 23 : kColValue;
|
||||
|
||||
int prefixW = typeW + nameW + 2 * kSepWidth;
|
||||
|
||||
@@ -567,8 +616,8 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHexPad ? 23 : kColValue;
|
||||
bool isHex = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHex ? 23 : kColValue;
|
||||
|
||||
int prefixW = typeW + nameW + 2 * kSepWidth;
|
||||
int start;
|
||||
@@ -608,16 +657,17 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
|
||||
}
|
||||
|
||||
// ── CommandRow root-class spans ──
|
||||
// Combined CommandRow format ends with: " struct▾ ClassName {"
|
||||
// Combined CommandRow format ends with: " struct ClassName {"
|
||||
|
||||
inline int commandRowRootStart(const QString& lineText) {
|
||||
int best = -1;
|
||||
int i;
|
||||
i = lineText.lastIndexOf(QStringLiteral("struct\u25BE"));
|
||||
// Match "struct " / "class " / "enum " as whole words before the class name
|
||||
i = lineText.lastIndexOf(QStringLiteral("struct "));
|
||||
if (i > best) best = i;
|
||||
i = lineText.lastIndexOf(QStringLiteral("class\u25BE"));
|
||||
i = lineText.lastIndexOf(QStringLiteral("class "));
|
||||
if (i > best) best = i;
|
||||
i = lineText.lastIndexOf(QStringLiteral("enum\u25BE"));
|
||||
i = lineText.lastIndexOf(QStringLiteral("enum "));
|
||||
if (i > best) best = i;
|
||||
return best;
|
||||
}
|
||||
@@ -626,8 +676,7 @@ inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) {
|
||||
int start = commandRowRootStart(lineText);
|
||||
if (start < 0) return {};
|
||||
int end = start;
|
||||
while (end < lineText.size() && lineText[end] != QChar(' ')
|
||||
&& lineText[end] != QChar(0x25BE)) end++;
|
||||
while (end < lineText.size() && lineText[end] != QChar(' ')) end++;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
}
|
||||
|
||||
76
src/disasm.cpp
Normal file
76
src/disasm.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "disasm.h"
|
||||
|
||||
extern "C" {
|
||||
#include <fadec.h>
|
||||
}
|
||||
|
||||
namespace rcx {
|
||||
|
||||
QString disassemble(const QByteArray& bytes, uint64_t baseAddr, int bitness, int maxBytes) {
|
||||
if (bytes.isEmpty() || (bitness != 32 && bitness != 64))
|
||||
return {};
|
||||
|
||||
int len = qMin((int)bytes.size(), maxBytes);
|
||||
const auto* buf = reinterpret_cast<const uint8_t*>(bytes.constData());
|
||||
|
||||
QString result;
|
||||
int off = 0;
|
||||
while (off < len) {
|
||||
FdInstr instr;
|
||||
int ret = fd_decode(buf + off, len - off, bitness, baseAddr + off, &instr);
|
||||
if (ret < 0)
|
||||
break;
|
||||
|
||||
char fmtBuf[128];
|
||||
fd_format(&instr, fmtBuf, sizeof(fmtBuf));
|
||||
|
||||
if (!result.isEmpty())
|
||||
result += QLatin1Char('\n');
|
||||
result += QStringLiteral("%1 %2")
|
||||
.arg(baseAddr + off, bitness == 64 ? 16 : 8, 16, QLatin1Char('0'))
|
||||
.arg(QString::fromLatin1(fmtBuf));
|
||||
|
||||
off += ret;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QString hexDump(const QByteArray& bytes, uint64_t baseAddr, int maxBytes) {
|
||||
if (bytes.isEmpty())
|
||||
return {};
|
||||
|
||||
int len = qMin((int)bytes.size(), maxBytes);
|
||||
QString result;
|
||||
|
||||
for (int off = 0; off < len; off += 16) {
|
||||
int lineLen = qMin(16, len - off);
|
||||
|
||||
if (!result.isEmpty())
|
||||
result += QLatin1Char('\n');
|
||||
|
||||
// Address
|
||||
bool wide = (baseAddr + len > 0xFFFFFFFFULL);
|
||||
result += QStringLiteral("%1 ").arg(baseAddr + off, wide ? 16 : 8, 16, QLatin1Char('0'));
|
||||
|
||||
// Hex bytes
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (i < lineLen) {
|
||||
uint8_t b = static_cast<uint8_t>(bytes[off + i]);
|
||||
result += QStringLiteral("%1 ").arg(b, 2, 16, QLatin1Char('0'));
|
||||
} else {
|
||||
result += QStringLiteral(" ");
|
||||
}
|
||||
if (i == 7) result += QLatin1Char(' ');
|
||||
}
|
||||
|
||||
// ASCII
|
||||
result += QLatin1Char(' ');
|
||||
for (int i = 0; i < lineLen; i++) {
|
||||
char c = bytes[off + i];
|
||||
result += (c >= 0x20 && c < 0x7f) ? QLatin1Char(c) : QLatin1Char('.');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
15
src/disasm.h
Normal file
15
src/disasm.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
#include <cstdint>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Disassemble up to maxBytes of x86 code, returning formatted asm lines.
|
||||
// bitness: 32 or 64. Returns one line per instruction, prefixed with offset.
|
||||
QString disassemble(const QByteArray& bytes, uint64_t baseAddr, int bitness, int maxBytes = 128);
|
||||
|
||||
// Format bytes as hex dump lines (16 bytes per line with ASCII sidebar).
|
||||
QString hexDump(const QByteArray& bytes, uint64_t baseAddr, int maxBytes = 128);
|
||||
|
||||
} // namespace rcx
|
||||
896
src/editor.cpp
896
src/editor.cpp
File diff suppressed because it is too large
Load Diff
21
src/editor.h
21
src/editor.h
@@ -27,6 +27,7 @@ public:
|
||||
void restoreViewState(const ViewState& vs);
|
||||
|
||||
QsciScintilla* scintilla() const { return m_sci; }
|
||||
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
|
||||
const LineMeta* metaForLine(int line) const;
|
||||
int currentNodeIndex() const;
|
||||
void scrollToNodeId(uint64_t nodeId);
|
||||
@@ -54,6 +55,10 @@ public:
|
||||
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
|
||||
QString textWithMargins() const;
|
||||
void setCustomTypeNames(const QStringList& names);
|
||||
void setValueHistoryRef(const QHash<uint64_t, ValueHistory>* ref) { m_valueHistory = ref; }
|
||||
void setProviderRef(const Provider* prov, const Provider* realProv, const NodeTree* tree) {
|
||||
m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree;
|
||||
}
|
||||
|
||||
// Saved sources for quick-switch in source picker
|
||||
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
||||
@@ -61,6 +66,7 @@ public:
|
||||
signals:
|
||||
void marginClicked(int margin, int line, Qt::KeyboardModifiers mods);
|
||||
void contextMenuRequested(int line, int nodeIdx, int subLine, QPoint globalPos);
|
||||
void keywordConvertRequested(const QString& newKeyword);
|
||||
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
|
||||
void inlineEditCommitted(int nodeIdx, int subLine,
|
||||
EditTarget target, const QString& text);
|
||||
@@ -78,7 +84,7 @@ private:
|
||||
LayoutInfo m_layout; // cached from ComposeResult
|
||||
|
||||
// ── Toggle: absolute vs relative offset margin
|
||||
bool m_relativeOffsets = false;
|
||||
bool m_relativeOffsets = true;
|
||||
|
||||
int m_marginStyleBase = -1;
|
||||
int m_hintLine = -1;
|
||||
@@ -129,7 +135,17 @@ private:
|
||||
// ── Saved sources for quick-switch ──
|
||||
QVector<SavedSourceDisplay> m_savedSourceDisplay;
|
||||
|
||||
// ── Value history ref (owned by controller) ──
|
||||
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
|
||||
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
|
||||
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
|
||||
QWidget* m_structPreviewPopup = nullptr; // StructPreviewPopup (file-local class in editor.cpp)
|
||||
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
|
||||
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||
const NodeTree* m_disasmTree = nullptr;
|
||||
|
||||
// ── Reentrancy guards ──
|
||||
bool m_applyingDocument = false;
|
||||
bool m_clampingSelection = false;
|
||||
bool m_updatingComment = false;
|
||||
|
||||
@@ -145,7 +161,8 @@ private:
|
||||
void applyMarkers(const QVector<LineMeta>& meta);
|
||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||
void applyDataChangedHighlight(const QVector<LineMeta>& meta);
|
||||
void applyHeatmapHighlight(const QVector<LineMeta>& meta);
|
||||
void applySymbolColoring(const QVector<LineMeta>& meta);
|
||||
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||
void applyCommandRowPills();
|
||||
|
||||
|
||||
1316
src/examples/KUSER_SHARED_DATA.rcx
Normal file
1316
src/examples/KUSER_SHARED_DATA.rcx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,344 +0,0 @@
|
||||
{
|
||||
"baseAddress": "400000",
|
||||
"nextId": "29",
|
||||
"nodes": [
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "1",
|
||||
"kind": "Struct",
|
||||
"name": "aBall",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"strLen": 64,
|
||||
"structTypeName": "ball"
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "2",
|
||||
"kind": "Hex64",
|
||||
"name": "field_00",
|
||||
"offset": 0,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "3",
|
||||
"kind": "Hex64",
|
||||
"name": "field_08",
|
||||
"offset": 8,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "4",
|
||||
"kind": "Vec4",
|
||||
"name": "position",
|
||||
"offset": 16,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "5",
|
||||
"kind": "Vec3",
|
||||
"name": "velocity",
|
||||
"offset": 32,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "6",
|
||||
"kind": "Hex32",
|
||||
"name": "field_2C",
|
||||
"offset": 44,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "7",
|
||||
"kind": "Float",
|
||||
"name": "speed",
|
||||
"offset": 48,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "8",
|
||||
"kind": "UInt32",
|
||||
"name": "color",
|
||||
"offset": 52,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "9",
|
||||
"kind": "Float",
|
||||
"name": "radius",
|
||||
"offset": 56,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "10",
|
||||
"kind": "Hex32",
|
||||
"name": "field_3C",
|
||||
"offset": 60,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "11",
|
||||
"kind": "Float",
|
||||
"name": "mass",
|
||||
"offset": 64,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "12",
|
||||
"kind": "Hex64",
|
||||
"name": "field_44",
|
||||
"offset": 68,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "13",
|
||||
"kind": "Bool",
|
||||
"name": "bouncy",
|
||||
"offset": 76,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "14",
|
||||
"kind": "Hex8",
|
||||
"name": "field_4D",
|
||||
"offset": 77,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "15",
|
||||
"kind": "Hex16",
|
||||
"name": "field_4E",
|
||||
"offset": 78,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "16",
|
||||
"kind": "UInt32",
|
||||
"name": "color",
|
||||
"offset": 80,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "17",
|
||||
"kind": "Hex32",
|
||||
"name": "field_54",
|
||||
"offset": 84,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "18",
|
||||
"kind": "Hex64",
|
||||
"name": "field_58",
|
||||
"offset": 88,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "19",
|
||||
"kind": "Hex64",
|
||||
"name": "field_60",
|
||||
"offset": 96,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "20",
|
||||
"kind": "Struct",
|
||||
"name": "aPhysics",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"strLen": 64,
|
||||
"structTypeName": "Physics"
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "21",
|
||||
"kind": "Hex64",
|
||||
"name": "field_00",
|
||||
"offset": 0,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "22",
|
||||
"kind": "Hex64",
|
||||
"name": "field_08",
|
||||
"offset": 8,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "23",
|
||||
"kind": "Hex64",
|
||||
"name": "field_10",
|
||||
"offset": 16,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "24",
|
||||
"kind": "Hex64",
|
||||
"name": "field_18",
|
||||
"offset": 24,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "25",
|
||||
"kind": "Hex64",
|
||||
"name": "field_20",
|
||||
"offset": 32,
|
||||
"parentId": "20",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": true,
|
||||
"elementKind": "UInt8",
|
||||
"id": "26",
|
||||
"kind": "Pointer64",
|
||||
"name": "physics",
|
||||
"offset": 104,
|
||||
"parentId": "1",
|
||||
"refId": "20",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 4,
|
||||
"collapsed": false,
|
||||
"elementKind": "Float",
|
||||
"id": "27",
|
||||
"kind": "Array",
|
||||
"name": "scores",
|
||||
"offset": 112,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 2,
|
||||
"collapsed": false,
|
||||
"elementKind": "Struct",
|
||||
"id": "28",
|
||||
"kind": "Array",
|
||||
"name": "materials",
|
||||
"offset": 128,
|
||||
"parentId": "1",
|
||||
"refId": "20",
|
||||
"strLen": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
204
src/export_reclass_xml.cpp
Normal file
204
src/export_reclass_xml.cpp
Normal file
@@ -0,0 +1,204 @@
|
||||
#include "export_reclass_xml.h"
|
||||
#include <QFile>
|
||||
#include <QXmlStreamWriter>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Reverse type map: NodeKind -> ReClassEx V2016 XML Type integer
|
||||
static int xmlTypeForKind(NodeKind kind) {
|
||||
switch (kind) {
|
||||
case NodeKind::Struct: return 1; // ClassInstance
|
||||
case NodeKind::Hex32: return 4;
|
||||
case NodeKind::Hex64: return 5;
|
||||
case NodeKind::Hex16: return 6;
|
||||
case NodeKind::Hex8: return 7;
|
||||
case NodeKind::Pointer64: return 8; // ClassPointer
|
||||
case NodeKind::Pointer32: return 8;
|
||||
case NodeKind::Int64: return 9;
|
||||
case NodeKind::Int32: return 10;
|
||||
case NodeKind::Int16: return 11;
|
||||
case NodeKind::Int8: return 12;
|
||||
case NodeKind::Float: return 13;
|
||||
case NodeKind::Double: return 14;
|
||||
case NodeKind::UInt32: return 15;
|
||||
case NodeKind::UInt16: return 16;
|
||||
case NodeKind::UInt8: return 17;
|
||||
case NodeKind::UInt64: return 32;
|
||||
case NodeKind::UTF8: return 18;
|
||||
case NodeKind::UTF16: return 19;
|
||||
case NodeKind::Bool: return 17; // No native bool in ReClass, map to UInt8
|
||||
case NodeKind::Vec2: return 22;
|
||||
case NodeKind::Vec3: return 23;
|
||||
case NodeKind::Vec4: return 24;
|
||||
case NodeKind::Mat4x4: return 25;
|
||||
case NodeKind::Array: return 27; // ClassInstanceArray
|
||||
}
|
||||
return 7; // fallback to Hex8
|
||||
}
|
||||
|
||||
static int nodeSizeForExport(const Node& node) {
|
||||
switch (node.kind) {
|
||||
case NodeKind::UTF8: return node.strLen;
|
||||
case NodeKind::UTF16: return node.strLen * 2;
|
||||
case NodeKind::Array: {
|
||||
int elemSz = sizeForKind(node.elementKind);
|
||||
return node.arrayLen * (elemSz > 0 ? elemSz : 0);
|
||||
}
|
||||
default: return sizeForKind(node.kind);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a struct type name from a node ID
|
||||
static QString resolveStructName(const NodeTree& tree, uint64_t refId) {
|
||||
int idx = tree.indexOfId(refId);
|
||||
if (idx < 0) return {};
|
||||
const Node& ref = tree.nodes[idx];
|
||||
if (!ref.structTypeName.isEmpty()) return ref.structTypeName;
|
||||
return ref.name;
|
||||
}
|
||||
|
||||
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg) {
|
||||
if (tree.nodes.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No nodes to export");
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file for writing: ") + filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build child map
|
||||
QHash<uint64_t, QVector<int>> childMap;
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
childMap[tree.nodes[i].parentId].append(i);
|
||||
|
||||
QXmlStreamWriter xml(&file);
|
||||
xml.setAutoFormatting(true);
|
||||
xml.setAutoFormattingIndent(4);
|
||||
xml.writeStartDocument();
|
||||
|
||||
xml.writeStartElement(QStringLiteral("ReClass"));
|
||||
xml.writeComment(QStringLiteral("ReClassEx"));
|
||||
|
||||
// Get root structs
|
||||
QVector<int> roots = childMap.value(0);
|
||||
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
int classCount = 0;
|
||||
|
||||
for (int ri : roots) {
|
||||
const Node& root = tree.nodes[ri];
|
||||
if (root.kind != NodeKind::Struct) continue;
|
||||
|
||||
xml.writeStartElement(QStringLiteral("Class"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), root.name.isEmpty() ? root.structTypeName : root.name);
|
||||
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("28"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||
xml.writeAttribute(QStringLiteral("Offset"), QStringLiteral("0"));
|
||||
xml.writeAttribute(QStringLiteral("strOffset"), QStringLiteral("0"));
|
||||
xml.writeAttribute(QStringLiteral("Code"), QString());
|
||||
|
||||
// Get children sorted by offset
|
||||
QVector<int> children = childMap.value(root.id);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
int i = 0;
|
||||
while (i < children.size()) {
|
||||
const Node& child = tree.nodes[children[i]];
|
||||
|
||||
// Collapse consecutive hex nodes into a single Custom node (Type=21)
|
||||
if (isHexNode(child.kind)) {
|
||||
int runStart = child.offset;
|
||||
int runEnd = child.offset + child.byteSize();
|
||||
int j = i + 1;
|
||||
while (j < children.size()) {
|
||||
const Node& next = tree.nodes[children[j]];
|
||||
if (!isHexNode(next.kind)) break;
|
||||
if (next.offset < runEnd) break; // overlap
|
||||
runEnd = next.offset + next.byteSize();
|
||||
j++;
|
||||
}
|
||||
int totalSize = runEnd - runStart;
|
||||
xml.writeStartElement(QStringLiteral("Node"));
|
||||
// Use first hex node's name if it's a single node, otherwise generate
|
||||
QString hexName = (j - i == 1 && !child.name.isEmpty()) ? child.name : QString();
|
||||
xml.writeAttribute(QStringLiteral("Name"), hexName);
|
||||
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("21")); // Custom
|
||||
xml.writeAttribute(QStringLiteral("Size"), QString::number(totalSize));
|
||||
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||
xml.writeEndElement(); // Node
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
xml.writeStartElement(QStringLiteral("Node"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), child.name);
|
||||
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(child.kind)));
|
||||
xml.writeAttribute(QStringLiteral("Size"), QString::number(nodeSizeForExport(child)));
|
||||
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||
|
||||
// Pointer with target
|
||||
if ((child.kind == NodeKind::Pointer64 || child.kind == NodeKind::Pointer32) && child.refId != 0) {
|
||||
QString target = resolveStructName(tree, child.refId);
|
||||
if (!target.isEmpty())
|
||||
xml.writeAttribute(QStringLiteral("Pointer"), target);
|
||||
}
|
||||
|
||||
// Embedded struct instance
|
||||
if (child.kind == NodeKind::Struct) {
|
||||
QString instName = child.structTypeName.isEmpty() ? child.name : child.structTypeName;
|
||||
xml.writeAttribute(QStringLiteral("Instance"), instName);
|
||||
}
|
||||
|
||||
// Array: Total attribute and child <Array> element
|
||||
if (child.kind == NodeKind::Array) {
|
||||
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
|
||||
|
||||
// Resolve element type name
|
||||
QString elemName;
|
||||
if (child.elementKind == NodeKind::Struct && !child.structTypeName.isEmpty()) {
|
||||
elemName = child.structTypeName;
|
||||
} else if (child.refId != 0) {
|
||||
elemName = resolveStructName(tree, child.refId);
|
||||
}
|
||||
if (elemName.isEmpty())
|
||||
elemName = kindToString(child.elementKind);
|
||||
|
||||
xml.writeStartElement(QStringLiteral("Array"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), elemName);
|
||||
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
|
||||
xml.writeEndElement(); // Array
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // Node
|
||||
i++;
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // Class
|
||||
classCount++;
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // ReClass
|
||||
xml.writeEndDocument();
|
||||
file.close();
|
||||
|
||||
if (classCount == 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No struct classes found to export");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
10
src/export_reclass_xml.h
Normal file
10
src/export_reclass_xml.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Export a NodeTree to ReClass .NET / ReClassEx compatible XML format.
|
||||
// Returns true on success; populates errorMsg on failure if non-null.
|
||||
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg = nullptr);
|
||||
|
||||
} // namespace rcx
|
||||
@@ -262,7 +262,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
if (!display) return rawHex(val, 8);
|
||||
QString s = fmtPointer32(val);
|
||||
QString sym = prov.getSymbol((uint64_t)val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::Pointer64: {
|
||||
@@ -270,7 +270,23 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
if (!display) return rawHex(val, 16);
|
||||
QString s = fmtPointer64(val);
|
||||
QString sym = prov.getSymbol(val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::FuncPtr32: {
|
||||
uint32_t val = prov.readU32(addr);
|
||||
if (!display) return rawHex(val, 8);
|
||||
QString s = fmtPointer32(val);
|
||||
QString sym = prov.getSymbol((uint64_t)val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::FuncPtr64: {
|
||||
uint64_t val = prov.readU64(addr);
|
||||
if (!display) return rawHex(val, 16);
|
||||
QString s = fmtPointer64(val);
|
||||
QString sym = prov.getSymbol(val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::Vec2:
|
||||
@@ -293,7 +309,6 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
line += QStringLiteral("]");
|
||||
return line;
|
||||
}
|
||||
case NodeKind::Padding: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
|
||||
case NodeKind::UTF8: {
|
||||
QByteArray bytes = prov.readBytes(addr, node.strLen);
|
||||
int end = bytes.indexOf('\0');
|
||||
@@ -344,21 +359,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
||||
}
|
||||
|
||||
// Hex nodes and Padding: hex byte preview (ASCII padded to colName to align with value column)
|
||||
// Hex nodes: hex byte preview (ASCII padded to colName to align with value column)
|
||||
if (isHexPreview(node.kind)) {
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
const int totalSz = qMax(1, node.arrayLen);
|
||||
const int lineOff = subLine * 8;
|
||||
const int lineBytes = qMin(8, totalSz - lineOff);
|
||||
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
|
||||
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
|
||||
QString ascii = bytesToAscii(b, lineBytes).leftJustified(colName, ' ');
|
||||
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
|
||||
if (subLine == 0)
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
||||
}
|
||||
// Hex8..Hex64: single line, ASCII padded to colName so hex column aligns with value column
|
||||
const int sz = sizeForKind(node.kind);
|
||||
QByteArray b = prov.isReadable(addr, sz)
|
||||
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
||||
@@ -557,6 +559,14 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
|
||||
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
||||
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
|
||||
}
|
||||
case NodeKind::FuncPtr32: {
|
||||
uint val = stripHex(s).toUInt(ok, 16);
|
||||
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
|
||||
}
|
||||
case NodeKind::FuncPtr64: {
|
||||
qulonglong val = stripHex(s).toULongLong(ok, 16);
|
||||
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
|
||||
}
|
||||
case NodeKind::UTF8: {
|
||||
*ok = true;
|
||||
if (s.startsWith('"') && s.endsWith('"'))
|
||||
@@ -585,7 +595,8 @@ QString validateValue(NodeKind kind, const QString& text) {
|
||||
|
||||
// For integer/hex types, validate character set first
|
||||
bool isHexKind = (kind >= NodeKind::Hex8 && kind <= NodeKind::Hex64)
|
||||
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64;
|
||||
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64
|
||||
|| kind == NodeKind::FuncPtr32 || kind == NodeKind::FuncPtr64;
|
||||
bool isIntKind = (kind >= NodeKind::Int8 && kind <= NodeKind::UInt64);
|
||||
|
||||
if (isHexKind || isIntKind) {
|
||||
|
||||
@@ -44,13 +44,14 @@ static QString cTypeName(NodeKind kind) {
|
||||
case NodeKind::Bool: return QStringLiteral("bool");
|
||||
case NodeKind::Pointer32: return QStringLiteral("uint32_t");
|
||||
case NodeKind::Pointer64: return QStringLiteral("uint64_t");
|
||||
case NodeKind::FuncPtr32: return QStringLiteral("uint32_t");
|
||||
case NodeKind::FuncPtr64: return QStringLiteral("uint64_t");
|
||||
case NodeKind::Vec2: return QStringLiteral("float");
|
||||
case NodeKind::Vec3: return QStringLiteral("float");
|
||||
case NodeKind::Vec4: return QStringLiteral("float");
|
||||
case NodeKind::Mat4x4: return QStringLiteral("float");
|
||||
case NodeKind::UTF8: return QStringLiteral("char");
|
||||
case NodeKind::UTF16: return QStringLiteral("wchar_t");
|
||||
case NodeKind::Padding: return QStringLiteral("uint8_t");
|
||||
default: return QStringLiteral("uint8_t");
|
||||
}
|
||||
}
|
||||
@@ -123,8 +124,6 @@ static QString emitField(GenContext& ctx, const Node& node) {
|
||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
|
||||
case NodeKind::UTF16:
|
||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
|
||||
case NodeKind::Padding:
|
||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::Padding), name).arg(qMax(1, node.arrayLen)) + oc;
|
||||
case NodeKind::Pointer32: {
|
||||
if (node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
@@ -146,6 +145,10 @@ static QString emitField(GenContext& ctx, const Node& node) {
|
||||
}
|
||||
return QStringLiteral(" void* %1;").arg(name) + oc;
|
||||
}
|
||||
case NodeKind::FuncPtr32:
|
||||
return QStringLiteral(" void (*%1)();").arg(name) + oc;
|
||||
case NodeKind::FuncPtr64:
|
||||
return QStringLiteral(" void (*%1)();").arg(name) + oc;
|
||||
default:
|
||||
return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name) + oc;
|
||||
}
|
||||
@@ -169,7 +172,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
auto emitPadRun = [&](int offset, int size) {
|
||||
if (size <= 0) return;
|
||||
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
|
||||
.arg(ctx.cType(NodeKind::Padding))
|
||||
.arg(QStringLiteral("uint8_t"))
|
||||
.arg(ctx.uniquePadName())
|
||||
.arg(QString::number(size, 16).toUpper())
|
||||
.arg(offsetComment(offset));
|
||||
|
||||
388
src/import_reclass_xml.cpp
Normal file
388
src/import_reclass_xml.cpp
Normal file
@@ -0,0 +1,388 @@
|
||||
#include "import_reclass_xml.h"
|
||||
#include <QFile>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
#include <QDebug>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Version-specific type maps ──
|
||||
// Maps XML Type attribute (integer) → NodeKind.
|
||||
// Entries with no rcx equivalent use Hex8 as fallback.
|
||||
|
||||
enum class XmlVersion { V2013, V2016 };
|
||||
|
||||
// 2016 / ReClassEx / MemeClsEx type map (35 entries, index = XML Type value)
|
||||
static const struct { int xmlType; NodeKind kind; } kTypeMap2016[] = {
|
||||
// 0: null (unused)
|
||||
{ 1, NodeKind::Struct }, // ClassInstance
|
||||
// 2,3: null
|
||||
{ 4, NodeKind::Hex32 },
|
||||
{ 5, NodeKind::Hex64 },
|
||||
{ 6, NodeKind::Hex16 },
|
||||
{ 7, NodeKind::Hex8 },
|
||||
{ 8, NodeKind::Pointer64 }, // ClassPointer
|
||||
{ 9, NodeKind::Int64 },
|
||||
{ 10, NodeKind::Int32 },
|
||||
{ 11, NodeKind::Int16 },
|
||||
{ 12, NodeKind::Int8 },
|
||||
{ 13, NodeKind::Float },
|
||||
{ 14, NodeKind::Double },
|
||||
{ 15, NodeKind::UInt32 },
|
||||
{ 16, NodeKind::UInt16 },
|
||||
{ 17, NodeKind::UInt8 },
|
||||
{ 18, NodeKind::UTF8 }, // UTF8Text
|
||||
{ 19, NodeKind::UTF16 }, // UTF16Text
|
||||
{ 20, NodeKind::Pointer64 }, // FunctionPtr
|
||||
{ 21, NodeKind::Hex8 }, // Custom (expanded by Size)
|
||||
{ 22, NodeKind::Vec2 },
|
||||
{ 23, NodeKind::Vec3 },
|
||||
{ 24, NodeKind::Vec4 },
|
||||
{ 25, NodeKind::Mat4x4 },
|
||||
{ 26, NodeKind::Pointer64 }, // VTable
|
||||
{ 27, NodeKind::Array }, // ClassInstanceArray
|
||||
// 28: null (used for Class elements, not nodes)
|
||||
{ 29, NodeKind::Pointer64 }, // UTF8TextPtr
|
||||
{ 30, NodeKind::Pointer64 }, // UTF16TextPtr
|
||||
// 31: BitField → UInt8 fallback
|
||||
{ 31, NodeKind::UInt8 },
|
||||
{ 32, NodeKind::UInt64 },
|
||||
{ 33, NodeKind::Pointer64 }, // Function
|
||||
};
|
||||
|
||||
// 2013 / ReClass 2011 type map (31 entries)
|
||||
static const struct { int xmlType; NodeKind kind; } kTypeMap2013[] = {
|
||||
{ 1, NodeKind::Struct }, // ClassInstance
|
||||
{ 4, NodeKind::Hex32 },
|
||||
{ 5, NodeKind::Hex16 },
|
||||
{ 6, NodeKind::Hex8 },
|
||||
{ 7, NodeKind::Pointer64 }, // ClassPointer
|
||||
{ 8, NodeKind::Int32 },
|
||||
{ 9, NodeKind::Int16 },
|
||||
{ 10, NodeKind::Int8 },
|
||||
{ 11, NodeKind::Float },
|
||||
{ 12, NodeKind::UInt32 },
|
||||
{ 13, NodeKind::UInt16 },
|
||||
{ 14, NodeKind::UInt8 },
|
||||
{ 15, NodeKind::UTF8 }, // UTF8Text
|
||||
{ 16, NodeKind::Pointer64 }, // FunctionPtr
|
||||
{ 17, NodeKind::Hex8 }, // Custom
|
||||
{ 18, NodeKind::Vec2 },
|
||||
{ 19, NodeKind::Vec3 },
|
||||
{ 20, NodeKind::Vec4 },
|
||||
{ 21, NodeKind::Mat4x4 },
|
||||
{ 22, NodeKind::Pointer64 }, // VTable
|
||||
{ 23, NodeKind::Array }, // ClassInstanceArray
|
||||
{ 27, NodeKind::Int64 },
|
||||
{ 28, NodeKind::Double },
|
||||
{ 29, NodeKind::UTF16 }, // UTF16Text
|
||||
{ 30, NodeKind::Array }, // ClassPointerArray
|
||||
};
|
||||
|
||||
static NodeKind lookupKind(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) {
|
||||
for (const auto& e : kTypeMap2016)
|
||||
if (e.xmlType == xmlType) return e.kind;
|
||||
} else {
|
||||
for (const auto& e : kTypeMap2013)
|
||||
if (e.xmlType == xmlType) return e.kind;
|
||||
}
|
||||
return NodeKind::Hex8; // fallback
|
||||
}
|
||||
|
||||
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
|
||||
static bool isPointerType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016)
|
||||
return xmlType == 8 || xmlType == 20 || xmlType == 26 || xmlType == 29 || xmlType == 30 || xmlType == 33;
|
||||
else
|
||||
return xmlType == 7 || xmlType == 16 || xmlType == 22;
|
||||
}
|
||||
|
||||
// Is this XML type a ClassInstance (embedded struct)?
|
||||
static bool isClassInstanceType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) return xmlType == 1;
|
||||
else return xmlType == 1;
|
||||
}
|
||||
|
||||
// Is this XML type a ClassInstanceArray?
|
||||
static bool isClassInstanceArrayType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) return xmlType == 27;
|
||||
else return xmlType == 23 || xmlType == 30;
|
||||
}
|
||||
|
||||
// Is this XML type a text node?
|
||||
static bool isTextType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) return xmlType == 18 || xmlType == 19;
|
||||
else return xmlType == 15 || xmlType == 29;
|
||||
}
|
||||
|
||||
// Is this XML type a UTF16 text node?
|
||||
static bool isUtf16TextType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) return xmlType == 19;
|
||||
else return xmlType == 29;
|
||||
}
|
||||
|
||||
// Is this XML type a Custom node (expanded to hex)?
|
||||
static bool isCustomType(int xmlType, XmlVersion ver) {
|
||||
if (ver == XmlVersion::V2016) return xmlType == 21;
|
||||
else return xmlType == 17;
|
||||
}
|
||||
|
||||
// Deferred pointer resolution entry
|
||||
struct PendingRef {
|
||||
uint64_t nodeId;
|
||||
QString className;
|
||||
};
|
||||
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||
qDebug() << "[ImportXML] Opening file:" << filePath;
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qDebug() << "[ImportXML] ERROR: Cannot open file";
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file: ") + filePath;
|
||||
return {};
|
||||
}
|
||||
|
||||
qDebug() << "[ImportXML] File size:" << file.size() << "bytes";
|
||||
|
||||
QXmlStreamReader xml(&file);
|
||||
XmlVersion version = XmlVersion::V2016; // default to 2016 (most common)
|
||||
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x00400000;
|
||||
|
||||
// Class name → struct node ID (for pointer resolution)
|
||||
QHash<QString, uint64_t> classIds;
|
||||
// Deferred pointer refs to resolve after all classes are parsed
|
||||
QVector<PendingRef> pendingRefs;
|
||||
|
||||
// Detect version from first comment
|
||||
bool versionDetected = false;
|
||||
|
||||
while (!xml.atEnd()) {
|
||||
xml.readNext();
|
||||
|
||||
// Detect version from XML comments
|
||||
if (!versionDetected && xml.isComment()) {
|
||||
QString comment = xml.text().toString().trimmed();
|
||||
if (comment.contains(QStringLiteral("ReClassEx"), Qt::CaseInsensitive) ||
|
||||
comment.contains(QStringLiteral("MemeClsEx"), Qt::CaseInsensitive) ||
|
||||
comment.contains(QStringLiteral("2016"), Qt::CaseInsensitive) ||
|
||||
comment.contains(QStringLiteral("2015"), Qt::CaseInsensitive)) {
|
||||
version = XmlVersion::V2016;
|
||||
} else if (comment.contains(QStringLiteral("2013"), Qt::CaseInsensitive) ||
|
||||
comment.contains(QStringLiteral("2011"), Qt::CaseInsensitive)) {
|
||||
version = XmlVersion::V2013;
|
||||
}
|
||||
// else keep default V2016
|
||||
versionDetected = true;
|
||||
qDebug() << "[ImportXML] Detected version:" << (version == XmlVersion::V2016 ? "V2016" : "V2013");
|
||||
}
|
||||
|
||||
if (!xml.isStartElement()) continue;
|
||||
|
||||
if (xml.name() == QStringLiteral("Class")) {
|
||||
// Parse a class element into a root Struct node
|
||||
QString className = xml.attributes().value(QStringLiteral("Name")).toString();
|
||||
QString strOffset = xml.attributes().value(QStringLiteral("strOffset")).toString();
|
||||
|
||||
// Create root struct node (collapsed by default for large files)
|
||||
Node structNode;
|
||||
structNode.kind = NodeKind::Struct;
|
||||
structNode.name = className;
|
||||
structNode.structTypeName = className;
|
||||
structNode.parentId = 0; // root level
|
||||
structNode.offset = 0;
|
||||
structNode.collapsed = true;
|
||||
|
||||
int structIdx = tree.addNode(structNode);
|
||||
uint64_t structId = tree.nodes[structIdx].id;
|
||||
classIds[className] = structId;
|
||||
qDebug() << "[ImportXML] Class:" << className << "id:" << structId;
|
||||
|
||||
// Parse child Node elements
|
||||
int childOffset = 0;
|
||||
while (!xml.atEnd()) {
|
||||
xml.readNext();
|
||||
|
||||
if (xml.isEndElement() && xml.name() == QStringLiteral("Class"))
|
||||
break;
|
||||
|
||||
if (!xml.isStartElement() || xml.name() != QStringLiteral("Node"))
|
||||
continue;
|
||||
|
||||
int xmlType = xml.attributes().value(QStringLiteral("Type")).toInt();
|
||||
QString nodeName = xml.attributes().value(QStringLiteral("Name")).toString();
|
||||
int nodeSize = xml.attributes().value(QStringLiteral("Size")).toInt();
|
||||
QString ptrClass = xml.attributes().value(QStringLiteral("Pointer")).toString();
|
||||
QString instClass = xml.attributes().value(QStringLiteral("Instance")).toString();
|
||||
|
||||
qDebug() << "[ImportXML] Node:" << nodeName << "type:" << xmlType
|
||||
<< "size:" << nodeSize << "ptr:" << ptrClass << "inst:" << instClass;
|
||||
|
||||
// Handle Custom type: expand to appropriate hex nodes
|
||||
if (isCustomType(xmlType, version) && nodeSize > 0) {
|
||||
// Pick best-fit hex kind
|
||||
NodeKind hexKind;
|
||||
int hexSize;
|
||||
if (nodeSize >= 8 && nodeSize % 8 == 0) {
|
||||
hexKind = NodeKind::Hex64; hexSize = 8;
|
||||
} else if (nodeSize >= 4 && nodeSize % 4 == 0) {
|
||||
hexKind = NodeKind::Hex32; hexSize = 4;
|
||||
} else if (nodeSize >= 2 && nodeSize % 2 == 0) {
|
||||
hexKind = NodeKind::Hex16; hexSize = 2;
|
||||
} else {
|
||||
hexKind = NodeKind::Hex8; hexSize = 1;
|
||||
}
|
||||
int count = nodeSize / hexSize;
|
||||
for (int i = 0; i < count; i++) {
|
||||
Node n;
|
||||
n.kind = hexKind;
|
||||
n.name = (count == 1) ? nodeName : QString();
|
||||
n.parentId = structId;
|
||||
n.offset = childOffset;
|
||||
tree.addNode(n);
|
||||
childOffset += hexSize;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
NodeKind kind = lookupKind(xmlType, version);
|
||||
|
||||
// Handle ClassInstanceArray: read child <Array> element
|
||||
if (isClassInstanceArrayType(xmlType, version)) {
|
||||
qDebug() << "[ImportXML] -> ClassInstanceArray";
|
||||
int total = xml.attributes().value(QStringLiteral("Total")).toInt();
|
||||
if (total <= 0)
|
||||
total = xml.attributes().value(QStringLiteral("Count")).toInt();
|
||||
if (total <= 0) total = 1;
|
||||
|
||||
// Read child <Array> element for class name
|
||||
QString arrayClassName;
|
||||
while (!xml.atEnd()) {
|
||||
xml.readNext();
|
||||
if (xml.isEndElement() && xml.name() == QStringLiteral("Node"))
|
||||
break;
|
||||
if (xml.isStartElement() && xml.name() == QStringLiteral("Array")) {
|
||||
arrayClassName = xml.attributes().value(QStringLiteral("Name")).toString();
|
||||
int arrayTotal = xml.attributes().value(QStringLiteral("Total")).toInt();
|
||||
if (arrayTotal <= 0)
|
||||
arrayTotal = xml.attributes().value(QStringLiteral("Count")).toInt();
|
||||
if (arrayTotal > 0) total = arrayTotal;
|
||||
}
|
||||
}
|
||||
|
||||
// Create an Array node wrapping Struct elements
|
||||
Node arrNode;
|
||||
arrNode.kind = NodeKind::Array;
|
||||
arrNode.name = nodeName;
|
||||
arrNode.parentId = structId;
|
||||
arrNode.offset = childOffset;
|
||||
arrNode.arrayLen = total;
|
||||
arrNode.elementKind = NodeKind::Struct;
|
||||
if (!arrayClassName.isEmpty())
|
||||
arrNode.structTypeName = arrayClassName;
|
||||
int arrIdx = tree.addNode(arrNode);
|
||||
uint64_t arrId = tree.nodes[arrIdx].id;
|
||||
|
||||
// Defer ref resolution if array references a class
|
||||
if (!arrayClassName.isEmpty()) {
|
||||
pendingRefs.append({arrId, arrayClassName});
|
||||
}
|
||||
|
||||
childOffset += nodeSize > 0 ? nodeSize : 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
Node n;
|
||||
n.kind = kind;
|
||||
n.name = nodeName;
|
||||
n.parentId = structId;
|
||||
n.offset = childOffset;
|
||||
|
||||
// Handle text nodes
|
||||
if (isTextType(xmlType, version)) {
|
||||
if (isUtf16TextType(xmlType, version))
|
||||
n.strLen = qMax(1, nodeSize / 2);
|
||||
else
|
||||
n.strLen = qMax(1, nodeSize);
|
||||
}
|
||||
|
||||
// Handle pointer types
|
||||
if (isPointerType(xmlType, version) && !ptrClass.isEmpty()) {
|
||||
qDebug() << "[ImportXML] -> Pointer to class:" << ptrClass;
|
||||
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
pendingRefs.append({nodeId, ptrClass});
|
||||
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle embedded class instance
|
||||
if (isClassInstanceType(xmlType, version)) {
|
||||
QString resolvedClass = instClass.isEmpty() ? ptrClass : instClass;
|
||||
qDebug() << "[ImportXML] -> ClassInstance:" << resolvedClass;
|
||||
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
|
||||
n.structTypeName = resolvedClass;
|
||||
if (!n.structTypeName.isEmpty()) {
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
pendingRefs.append({nodeId, n.structTypeName});
|
||||
} else {
|
||||
tree.addNode(n);
|
||||
}
|
||||
childOffset += nodeSize > 0 ? nodeSize : 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
tree.addNode(n);
|
||||
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (xml.hasError() && xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
|
||||
qDebug() << "[ImportXML] XML parse error at line" << xml.lineNumber() << ":" << xml.errorString();
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("XML parse error at line %1: %2")
|
||||
.arg(xml.lineNumber())
|
||||
.arg(xml.errorString());
|
||||
return {};
|
||||
}
|
||||
|
||||
qDebug() << "[ImportXML] Parsing complete. Total nodes:" << tree.nodes.size()
|
||||
<< "classes:" << classIds.size() << "pending refs:" << pendingRefs.size();
|
||||
|
||||
if (tree.nodes.isEmpty()) {
|
||||
qDebug() << "[ImportXML] ERROR: No classes found";
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No classes found in file");
|
||||
return {};
|
||||
}
|
||||
|
||||
// Resolve deferred pointer/struct references
|
||||
int resolved = 0, unresolved = 0;
|
||||
for (const auto& ref : pendingRefs) {
|
||||
int nodeIdx = tree.indexOfId(ref.nodeId);
|
||||
if (nodeIdx < 0) continue;
|
||||
|
||||
auto it = classIds.find(ref.className);
|
||||
if (it != classIds.end()) {
|
||||
tree.nodes[nodeIdx].refId = it.value();
|
||||
tree.invalidateIdCache();
|
||||
resolved++;
|
||||
} else {
|
||||
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
|
||||
unresolved++;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "[ImportXML] Refs resolved:" << resolved << "unresolved:" << unresolved;
|
||||
qDebug() << "[ImportXML] Import complete. Returning tree with" << tree.nodes.size() << "nodes";
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
11
src/import_reclass_xml.h
Normal file
11
src/import_reclass_xml.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Import a ReClass XML file (.reclass, .MemeCls, etc.) into a NodeTree.
|
||||
// Supports ReClassEx, MemeClsEx, ReClass 2011/2013/2016 XML formats.
|
||||
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr);
|
||||
|
||||
} // namespace rcx
|
||||
1066
src/import_source.cpp
Normal file
1066
src/import_source.cpp
Normal file
File diff suppressed because it is too large
Load Diff
13
src/import_source.h
Normal file
13
src/import_source.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Import C/C++ struct definitions from source code into a NodeTree.
|
||||
// Supports two modes (auto-detected):
|
||||
// 1. With comment offsets (// 0xNN) - trusts the offset values
|
||||
// 2. Without comment offsets - computes offsets from type sizes
|
||||
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr);
|
||||
|
||||
} // namespace rcx
|
||||
759
src/main.cpp
759
src/main.cpp
@@ -1,5 +1,8 @@
|
||||
#include "mainwindow.h"
|
||||
#include "generator.h"
|
||||
#include "import_reclass_xml.h"
|
||||
#include "import_source.h"
|
||||
#include "export_reclass_xml.h"
|
||||
#include "mcp/mcp_bridge.h"
|
||||
#include <QApplication>
|
||||
#include <QMainWindow>
|
||||
@@ -43,6 +46,7 @@
|
||||
#include <QDesktopServices>
|
||||
#include "themes/thememanager.h"
|
||||
#include "themes/themeeditor.h"
|
||||
#include "optionsdialog.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -63,29 +67,56 @@ static void setDarkTitleBar(QWidget* widget) {
|
||||
}
|
||||
}
|
||||
|
||||
// Guard flag to prevent re-entrant crash inside the handler
|
||||
static volatile LONG s_inCrashHandler = 0;
|
||||
|
||||
static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
||||
// Prevent re-entrant crash: if we fault inside the handler, skip the
|
||||
// risky dbghelp work and just terminate with what we already printed.
|
||||
if (InterlockedCompareExchange(&s_inCrashHandler, 1, 0) != 0) {
|
||||
fprintf(stderr, "\n(re-entrant fault inside crash handler — aborting)\n");
|
||||
fflush(stderr);
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
// Phase 1: always-safe output (no allocations, no complex APIs)
|
||||
fprintf(stderr, "\n=== UNHANDLED EXCEPTION ===\n");
|
||||
fprintf(stderr, "Code : 0x%08lX\n", ep->ExceptionRecord->ExceptionCode);
|
||||
fprintf(stderr, "Addr : %p\n", ep->ExceptionRecord->ExceptionAddress);
|
||||
#ifdef _M_X64
|
||||
fprintf(stderr, "RIP : 0x%016llx\n", (unsigned long long)ep->ContextRecord->Rip);
|
||||
fprintf(stderr, "RSP : 0x%016llx\n", (unsigned long long)ep->ContextRecord->Rsp);
|
||||
#else
|
||||
fprintf(stderr, "EIP : 0x%08lx\n", (unsigned long)ep->ContextRecord->Eip);
|
||||
#endif
|
||||
fflush(stderr);
|
||||
|
||||
// Phase 2: attempt symbol resolution + stack walk
|
||||
// Copy context so StackWalk64 can mutate it safely
|
||||
CONTEXT ctxCopy = *ep->ContextRecord;
|
||||
|
||||
HANDLE process = GetCurrentProcess();
|
||||
HANDLE thread = GetCurrentThread();
|
||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
|
||||
SymInitialize(process, NULL, TRUE);
|
||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_FAIL_CRITICAL_ERRORS);
|
||||
if (!SymInitialize(process, NULL, TRUE)) {
|
||||
fprintf(stderr, "\n(SymInitialize failed — no stack trace available)\n");
|
||||
fprintf(stderr, "=== END CRASH ===\n");
|
||||
fflush(stderr);
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
CONTEXT* ctx = ep->ContextRecord;
|
||||
STACKFRAME64 frame = {};
|
||||
DWORD machineType;
|
||||
#ifdef _M_X64
|
||||
machineType = IMAGE_FILE_MACHINE_AMD64;
|
||||
frame.AddrPC.Offset = ctx->Rip;
|
||||
frame.AddrFrame.Offset = ctx->Rbp;
|
||||
frame.AddrStack.Offset = ctx->Rsp;
|
||||
frame.AddrPC.Offset = ctxCopy.Rip;
|
||||
frame.AddrFrame.Offset = ctxCopy.Rbp;
|
||||
frame.AddrStack.Offset = ctxCopy.Rsp;
|
||||
#else
|
||||
machineType = IMAGE_FILE_MACHINE_I386;
|
||||
frame.AddrPC.Offset = ctx->Eip;
|
||||
frame.AddrFrame.Offset = ctx->Ebp;
|
||||
frame.AddrStack.Offset = ctx->Esp;
|
||||
frame.AddrPC.Offset = ctxCopy.Eip;
|
||||
frame.AddrFrame.Offset = ctxCopy.Ebp;
|
||||
frame.AddrStack.Offset = ctxCopy.Esp;
|
||||
#endif
|
||||
frame.AddrPC.Mode = AddrModeFlat;
|
||||
frame.AddrFrame.Mode = AddrModeFlat;
|
||||
@@ -93,7 +124,7 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
||||
|
||||
fprintf(stderr, "\nStack trace:\n");
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (!StackWalk64(machineType, process, thread, &frame, ctx,
|
||||
if (!StackWalk64(machineType, process, thread, &frame, &ctxCopy,
|
||||
NULL, SymFunctionTableAccess64,
|
||||
SymGetModuleBase64, NULL))
|
||||
break;
|
||||
@@ -141,7 +172,9 @@ public:
|
||||
if ((w->windowFlags() & Qt::Window) == Qt::Window
|
||||
&& !w->property("DarkTitleBar").toBool()) {
|
||||
w->setProperty("DarkTitleBar", true);
|
||||
#ifdef _WIN32
|
||||
setDarkTitleBar(w);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
return QApplication::notify(receiver, event);
|
||||
@@ -160,6 +193,13 @@ public:
|
||||
s = QSize(s.width() + 24, s.height() + 4);
|
||||
return s;
|
||||
}
|
||||
int pixelMetric(PixelMetric metric, const QStyleOption* opt,
|
||||
const QWidget* w) const override {
|
||||
// Kill the 1px frame margin Fusion reserves around QMenu contents
|
||||
if (metric == PM_MenuPanelWidth)
|
||||
return 0;
|
||||
return QProxyStyle::pixelMetric(metric, opt, w);
|
||||
}
|
||||
void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt,
|
||||
QPainter* p, const QWidget* w) const override {
|
||||
// Kill Fusion's 3D bevel on QMenu — the OS drop shadow is enough
|
||||
@@ -205,16 +245,16 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.background);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
pal.setColor(QPalette::Base, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::Base, theme.background);
|
||||
pal.setColor(QPalette::AlternateBase, theme.surface);
|
||||
pal.setColor(QPalette::Text, theme.text);
|
||||
pal.setColor(QPalette::Button, theme.button);
|
||||
pal.setColor(QPalette::ButtonText, theme.text);
|
||||
pal.setColor(QPalette::Highlight, theme.hover);
|
||||
pal.setColor(QPalette::HighlightedText, theme.indHoverSpan);
|
||||
pal.setColor(QPalette::Highlight, theme.selection);
|
||||
pal.setColor(QPalette::HighlightedText, theme.text);
|
||||
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::ToolTipText, theme.text);
|
||||
pal.setColor(QPalette::Mid, theme.border);
|
||||
pal.setColor(QPalette::Mid, theme.hover);
|
||||
pal.setColor(QPalette::Dark, theme.background);
|
||||
pal.setColor(QPalette::Light, theme.textFaint);
|
||||
pal.setColor(QPalette::Link, theme.indHoverSpan);
|
||||
@@ -301,6 +341,13 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
createMenus();
|
||||
createStatusBar();
|
||||
|
||||
// Restore menu bar title case setting (after menus are created)
|
||||
{
|
||||
QSettings s("Reclass", "Reclass");
|
||||
m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", true).toBool());
|
||||
if (s.value("showIcon", false).toBool())
|
||||
m_titleBar->setShowIcon(true);
|
||||
}
|
||||
|
||||
// MenuBarStyle is set as app style in main() — covers both QMenuBar and QMenu
|
||||
|
||||
@@ -310,9 +357,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
// Load plugins
|
||||
m_pluginManager.LoadPlugins();
|
||||
|
||||
// MCP bridge (on by default)
|
||||
// Start MCP bridge
|
||||
m_mcp = new McpBridge(this, this);
|
||||
m_mcp->start();
|
||||
if (QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool())
|
||||
m_mcp->start();
|
||||
|
||||
connect(m_mdiArea, &QMdiArea::subWindowActivated,
|
||||
this, [this](QMdiSubWindow*) {
|
||||
@@ -338,33 +386,64 @@ QIcon MainWindow::makeIcon(const QString& svgPath) {
|
||||
return QIcon(svgPath);
|
||||
}
|
||||
|
||||
template < typename...Args >
|
||||
inline QAction* Qt5Qt6AddAction(QMenu* menu, const QString &text, const QKeySequence &shortcut, const QIcon &icon, Args&&...args)
|
||||
{
|
||||
QAction *result = menu->addAction(icon, text);
|
||||
if (!shortcut.isEmpty())
|
||||
result->setShortcut(shortcut);
|
||||
QObject::connect(result, &QAction::triggered, std::forward<Args>(args)...);
|
||||
return result;
|
||||
}
|
||||
|
||||
void MainWindow::createMenus() {
|
||||
// File
|
||||
auto* file = m_titleBar->menuBar()->addMenu("&File");
|
||||
file->addAction("&New", this, &MainWindow::newDocument, QKeySequence::New);
|
||||
file->addAction("New &Tab", this, &MainWindow::newFile, QKeySequence(Qt::CTRL | Qt::Key_T));
|
||||
file->addAction(makeIcon(":/vsicons/folder-opened.svg"), "&Open...", this, &MainWindow::openFile, QKeySequence::Open);
|
||||
Qt5Qt6AddAction(file, "New &Class", QKeySequence::New, QIcon(), this, &MainWindow::newClass);
|
||||
Qt5Qt6AddAction(file, "New &Struct", QKeySequence(Qt::CTRL | Qt::Key_T), QIcon(), this, &MainWindow::newStruct);
|
||||
Qt5Qt6AddAction(file, "New &Enum", QKeySequence(Qt::CTRL | Qt::Key_E), QIcon(), this, &MainWindow::newEnum);
|
||||
Qt5Qt6AddAction(file, "&Open...", QKeySequence::Open, makeIcon(":/vsicons/folder-opened.svg"), this, &MainWindow::openFile);
|
||||
file->addSeparator();
|
||||
file->addAction(makeIcon(":/vsicons/save.svg"), "&Save", this, &MainWindow::saveFile, QKeySequence::Save);
|
||||
file->addAction(makeIcon(":/vsicons/save-as.svg"), "Save &As...", this, &MainWindow::saveFileAs, QKeySequence::SaveAs);
|
||||
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile);
|
||||
Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs);
|
||||
file->addSeparator();
|
||||
file->addAction(makeIcon(":/vsicons/export.svg"), "Export &C++ Header...", this, &MainWindow::exportCpp);
|
||||
Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
|
||||
file->addSeparator();
|
||||
m_mcpAction = file->addAction("Stop &MCP Server", this, &MainWindow::toggleMcp);
|
||||
Qt5Qt6AddAction(file, "Export &C++ Header...", QKeySequence::UnknownKey, makeIcon(":/vsicons/export.svg"), this, &MainWindow::exportCpp);
|
||||
Qt5Qt6AddAction(file, "Export ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
|
||||
Qt5Qt6AddAction(file, "Import from &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource);
|
||||
Qt5Qt6AddAction(file, "&Import ReClass XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml);
|
||||
// Examples submenu — scan once at init
|
||||
{
|
||||
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
|
||||
QStringList rcxFiles = exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name);
|
||||
if (!rcxFiles.isEmpty()) {
|
||||
auto* examples = file->addMenu("&Examples");
|
||||
for (const QString& fn : rcxFiles) {
|
||||
QString fullPath = exDir.absoluteFilePath(fn);
|
||||
examples->addAction(fn, this, [this, fullPath]() { project_open(fullPath); });
|
||||
}
|
||||
}
|
||||
}
|
||||
file->addSeparator();
|
||||
file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", this, &QMainWindow::close, QKeySequence(Qt::Key_Close));
|
||||
const auto itemName = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
|
||||
m_mcpAction = Qt5Qt6AddAction(file, itemName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp);
|
||||
file->addSeparator();
|
||||
Qt5Qt6AddAction(file, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, &MainWindow::showOptionsDialog);
|
||||
file->addSeparator();
|
||||
Qt5Qt6AddAction(file, "E&xit", QKeySequence(Qt::Key_Close), makeIcon(":/vsicons/close.svg"), this, &QMainWindow::close);
|
||||
|
||||
// Edit
|
||||
auto* edit = m_titleBar->menuBar()->addMenu("&Edit");
|
||||
edit->addAction(makeIcon(":/vsicons/arrow-left.svg"), "&Undo", this, &MainWindow::undo, QKeySequence::Undo);
|
||||
edit->addAction(makeIcon(":/vsicons/arrow-right.svg"), "&Redo", this, &MainWindow::redo, QKeySequence::Redo);
|
||||
Qt5Qt6AddAction(edit, "&Undo", QKeySequence::Undo, makeIcon(":/vsicons/arrow-left.svg"), this, &MainWindow::undo);
|
||||
Qt5Qt6AddAction(edit, "&Redo", QKeySequence::Redo, makeIcon(":/vsicons/arrow-right.svg"), this, &MainWindow::redo);
|
||||
edit->addSeparator();
|
||||
edit->addAction("&Type Aliases...", this, &MainWindow::showTypeAliasesDialog);
|
||||
Qt5Qt6AddAction(edit, "&Type Aliases...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showTypeAliasesDialog);
|
||||
|
||||
// View
|
||||
auto* view = m_titleBar->menuBar()->addMenu("&View");
|
||||
view->addAction(makeIcon(":/vsicons/split-horizontal.svg"), "Split &Horizontal", this, &MainWindow::splitView);
|
||||
view->addAction(makeIcon(":/vsicons/chrome-close.svg"), "&Unsplit", this, &MainWindow::unsplitView);
|
||||
Qt5Qt6AddAction(view, "Split &Horizontal", QKeySequence::UnknownKey, makeIcon(":/vsicons/split-horizontal.svg"), this, &MainWindow::splitView);
|
||||
Qt5Qt6AddAction(view, "&Unsplit", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView);
|
||||
view->addSeparator();
|
||||
auto* fontMenu = view->addMenu(makeIcon(":/vsicons/text-size.svg"), "&Font");
|
||||
auto* fontGroup = new QActionGroup(this);
|
||||
@@ -399,35 +478,18 @@ void MainWindow::createMenus() {
|
||||
});
|
||||
}
|
||||
themeMenu->addSeparator();
|
||||
themeMenu->addAction("Edit Theme...", this, &MainWindow::editTheme);
|
||||
Qt5Qt6AddAction(themeMenu, "Edit Theme...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::editTheme);
|
||||
|
||||
view->addSeparator();
|
||||
auto* actShowIcon = view->addAction("Show &Icon");
|
||||
actShowIcon->setCheckable(true);
|
||||
actShowIcon->setChecked(settings.value("showIcon", false).toBool());
|
||||
if (actShowIcon->isChecked()) m_titleBar->setShowIcon(true);
|
||||
connect(actShowIcon, &QAction::toggled, this, [this](bool checked) {
|
||||
m_titleBar->setShowIcon(checked);
|
||||
QSettings s("Reclass", "Reclass");
|
||||
s.setValue("showIcon", checked);
|
||||
});
|
||||
view->addAction(m_workspaceDock->toggleViewAction());
|
||||
|
||||
// Node
|
||||
auto* node = m_titleBar->menuBar()->addMenu("&Node");
|
||||
node->addAction(makeIcon(":/vsicons/add.svg"), "&Add Field", this, &MainWindow::addNode, QKeySequence(Qt::Key_Insert));
|
||||
node->addAction(makeIcon(":/vsicons/remove.svg"), "&Remove Field", this, &MainWindow::removeNode, QKeySequence::Delete);
|
||||
node->addAction(makeIcon(":/vsicons/symbol-structure.svg"), "Change &Type", this, &MainWindow::changeNodeType, QKeySequence(Qt::Key_T));
|
||||
node->addAction(makeIcon(":/vsicons/edit.svg"), "Re&name", this, &MainWindow::renameNodeAction, QKeySequence(Qt::Key_F2));
|
||||
node->addAction(makeIcon(":/vsicons/files.svg"), "D&uplicate", this, &MainWindow::duplicateNodeAction)->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_D));
|
||||
|
||||
// Plugins
|
||||
auto* plugins = m_titleBar->menuBar()->addMenu("&Plugins");
|
||||
plugins->addAction("&Manage Plugins...", this, &MainWindow::showPluginsDialog);
|
||||
Qt5Qt6AddAction(plugins, "&Manage Plugins...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::showPluginsDialog);
|
||||
|
||||
// Help
|
||||
auto* help = m_titleBar->menuBar()->addMenu("&Help");
|
||||
help->addAction(makeIcon(":/vsicons/question.svg"), "&About Reclass", this, &MainWindow::about);
|
||||
Qt5Qt6AddAction(help, "&About Reclass", QKeySequence::UnknownKey, makeIcon(":/vsicons/question.svg"), this, &MainWindow::about);
|
||||
}
|
||||
|
||||
void MainWindow::createStatusBar() {
|
||||
@@ -665,6 +727,7 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
sub->setWindowTitle(rootName(it2->doc->tree, it2->ctrl->viewRootId()));
|
||||
}
|
||||
updateWindowTitle();
|
||||
rebuildWorkspaceModel();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -682,144 +745,133 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
return sub;
|
||||
}
|
||||
|
||||
// Build Ball + Material demo structs into a tree
|
||||
static void buildBallDemo(NodeTree& tree) {
|
||||
// Ball struct (128 bytes = 0x80)
|
||||
Node ball;
|
||||
ball.kind = NodeKind::Struct;
|
||||
ball.name = "aBall";
|
||||
ball.structTypeName = "Ball";
|
||||
ball.parentId = 0;
|
||||
ball.offset = 0;
|
||||
int bi = tree.addNode(ball);
|
||||
uint64_t ballId = tree.nodes[bi].id;
|
||||
// Build a minimal empty struct for new documents
|
||||
static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QString()) {
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "instance";
|
||||
root.structTypeName = "Unnamed";
|
||||
root.classKeyword = classKeyword;
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = ballId; n.offset = 0; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = ballId; n.offset = 8; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 16; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Vec3; n.name = "velocity"; n.parentId = ballId; n.offset = 32; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_2C"; n.parentId = ballId; n.offset = 44; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 48; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 52; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "radius"; n.parentId = ballId; n.offset = 56; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_3C"; n.parentId = ballId; n.offset = 60; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "mass"; n.parentId = ballId; n.offset = 64; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_44"; n.parentId = ballId; n.offset = 68; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 76; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_4D"; n.parentId = ballId; n.offset = 77; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_4E"; n.parentId = ballId; n.offset = 78; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 80; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_54"; n.parentId = ballId; n.offset = 84; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_58"; n.parentId = ballId; n.offset = 88; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_60"; n.parentId = ballId; n.offset = 96; tree.addNode(n); }
|
||||
|
||||
// Material struct (renamed from Physics, 40 bytes = 0x28)
|
||||
Node mat;
|
||||
mat.kind = NodeKind::Struct;
|
||||
mat.name = "aMaterial";
|
||||
mat.structTypeName = "Material";
|
||||
mat.parentId = 0;
|
||||
mat.offset = 0;
|
||||
int mi = tree.addNode(mat);
|
||||
uint64_t matId = tree.nodes[mi].id;
|
||||
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = matId; n.offset = 0; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = matId; n.offset = 8; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_10"; n.parentId = matId; n.offset = 16; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_18"; n.parentId = matId; n.offset = 24; tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_20"; n.parentId = matId; n.offset = 32; tree.addNode(n); }
|
||||
|
||||
// Pointer to Material in Ball struct
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = "material"; n.parentId = ballId; n.offset = 104; n.refId = matId; n.collapsed = true; tree.addNode(n); }
|
||||
|
||||
// float[4] scores at offset 112
|
||||
{ Node n; n.kind = NodeKind::Array; n.name = "scores"; n.parentId = ballId; n.offset = 112; n.elementKind = NodeKind::Float; n.arrayLen = 4; tree.addNode(n); }
|
||||
|
||||
// Material[2] materials at offset 128 (112 + 16 for float[4])
|
||||
{ Node n; n.kind = NodeKind::Array; n.name = "materials"; n.parentId = ballId; n.offset = 128; n.elementKind = NodeKind::Struct; n.arrayLen = 2; n.refId = matId; tree.addNode(n); }
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Hex64;
|
||||
n.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
|
||||
n.parentId = rootId;
|
||||
n.offset = i * 8;
|
||||
tree.addNode(n);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::newFile() {
|
||||
void MainWindow::newClass() {
|
||||
project_new(QStringLiteral("class"));
|
||||
}
|
||||
|
||||
void MainWindow::newStruct() {
|
||||
project_new();
|
||||
}
|
||||
|
||||
void MainWindow::newDocument() {
|
||||
auto* tab = activeTab();
|
||||
if (!tab) {
|
||||
project_new();
|
||||
return;
|
||||
void MainWindow::newEnum() {
|
||||
project_new(QStringLiteral("enum"));
|
||||
}
|
||||
|
||||
static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) {
|
||||
tree.nodes.clear();
|
||||
tree.invalidateIdCache();
|
||||
tree.m_nextId = 1;
|
||||
tree.baseAddress = static_cast<uint64_t>(editorAddr);
|
||||
|
||||
// ── Root struct: RcxEditor ──
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = QStringLiteral("editor");
|
||||
root.structTypeName = QStringLiteral("RcxEditor");
|
||||
root.classKeyword = QStringLiteral("class");
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// ── VTable struct definition (separate root) ──
|
||||
Node vtStruct;
|
||||
vtStruct.kind = NodeKind::Struct;
|
||||
vtStruct.name = QStringLiteral("VTable");
|
||||
vtStruct.structTypeName = QStringLiteral("QWidgetVTable");
|
||||
int vti = tree.addNode(vtStruct);
|
||||
uint64_t vtId = tree.nodes[vti].id;
|
||||
|
||||
// VTable entries — these are real virtual function pointers from QObject/QWidget
|
||||
static const char* vfNames[] = {
|
||||
"deleting_dtor", "metaObject", "qt_metacast", "qt_metacall",
|
||||
"event", "eventFilter", "timerEvent", "childEvent",
|
||||
"customEvent", "connectNotify", "disconnectNotify", "devType",
|
||||
"setVisible", "sizeHint", "minimumSizeHint", "heightForWidth",
|
||||
};
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Node fn;
|
||||
fn.kind = NodeKind::FuncPtr64;
|
||||
fn.name = QString::fromLatin1(vfNames[i]);
|
||||
fn.parentId = vtId;
|
||||
fn.offset = i * 8;
|
||||
tree.addNode(fn);
|
||||
}
|
||||
auto* doc = tab->doc;
|
||||
auto* ctrl = tab->ctrl;
|
||||
|
||||
// Clear everything
|
||||
doc->undoStack.clear();
|
||||
doc->tree = NodeTree();
|
||||
doc->tree.baseAddress = 0x00400000;
|
||||
doc->filePath.clear();
|
||||
doc->typeAliases.clear();
|
||||
doc->modified = false;
|
||||
|
||||
// Build Ball + Material structs
|
||||
buildBallDemo(doc->tree);
|
||||
|
||||
// Cross-platform writable buffer, zeroed (256 bytes covers Ball + spare)
|
||||
QByteArray data(256, '\0');
|
||||
doc->provider = std::make_shared<BufferProvider>(data);
|
||||
|
||||
// Focus on Ball struct
|
||||
ctrl->setViewRootId(0);
|
||||
for (const auto& n : doc->tree.nodes) {
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
||||
ctrl->setViewRootId(n.id);
|
||||
break;
|
||||
}
|
||||
// ── RcxEditor fields ──
|
||||
// offset 0: vtable pointer → QWidgetVTable
|
||||
{
|
||||
Node n;
|
||||
n.kind = NodeKind::Pointer64;
|
||||
n.name = QStringLiteral("__vptr");
|
||||
n.parentId = rootId;
|
||||
n.offset = 0;
|
||||
n.refId = vtId;
|
||||
tree.addNode(n);
|
||||
}
|
||||
// offset 8: QObjectData* d_ptr (QObject internals)
|
||||
{
|
||||
Node n;
|
||||
n.kind = NodeKind::Pointer64;
|
||||
n.name = QStringLiteral("d_ptr");
|
||||
n.parentId = rootId;
|
||||
n.offset = 8;
|
||||
tree.addNode(n);
|
||||
}
|
||||
// The rest of the object: raw memory visible as Hex64 fields
|
||||
// QWidget base is large (~200+ bytes), then RcxEditor members follow.
|
||||
// Lay out enough to cover the interesting editor state.
|
||||
for (int off = 16; off < 512; off += 8) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Hex64;
|
||||
n.name = QStringLiteral("field_%1").arg(off, 3, 16, QLatin1Char('0'));
|
||||
n.parentId = rootId;
|
||||
n.offset = off;
|
||||
tree.addNode(n);
|
||||
}
|
||||
ctrl->clearSelection();
|
||||
emit doc->documentChanged();
|
||||
|
||||
auto* sub = m_mdiArea->activeSubWindow();
|
||||
if (sub) sub->setWindowTitle(rootName(doc->tree, ctrl->viewRootId()));
|
||||
updateWindowTitle();
|
||||
rebuildWorkspaceModel();
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
// Tab 1: Ball demo
|
||||
#ifdef Q_OS_WIN
|
||||
// Create a new project, then point it at the live editor object
|
||||
project_new();
|
||||
|
||||
// Tab 2: Unnamed struct with hex64 fields
|
||||
{
|
||||
auto* doc = new RcxDocument(this);
|
||||
QByteArray data(256, '\0');
|
||||
doc->loadData(data);
|
||||
doc->tree.baseAddress = 0x00400000;
|
||||
auto* ctrl = activeController();
|
||||
if (!ctrl || ctrl->editors().isEmpty()) return;
|
||||
|
||||
Node s;
|
||||
s.kind = NodeKind::Struct;
|
||||
s.name = "instance";
|
||||
s.structTypeName = "Unnamed";
|
||||
s.parentId = 0;
|
||||
s.offset = 0;
|
||||
int si = doc->tree.addNode(s);
|
||||
uint64_t sId = doc->tree.nodes[si].id;
|
||||
auto* editor = ctrl->editors().first();
|
||||
auto* doc = ctrl->document();
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Hex64;
|
||||
n.name = QStringLiteral("field_%1").arg(i * 8, 2, 16, QChar('0'));
|
||||
n.parentId = sId;
|
||||
n.offset = i * 8;
|
||||
doc->tree.addNode(n);
|
||||
}
|
||||
// Build a tree describing RcxEditor, based at the real object address
|
||||
buildEditorDemo(doc->tree, reinterpret_cast<uintptr_t>(editor));
|
||||
|
||||
createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
}
|
||||
|
||||
// Focus Ball tab
|
||||
if (auto* first = m_mdiArea->subWindowList().value(0))
|
||||
m_mdiArea->setActiveSubWindow(first);
|
||||
// Attach process memory to self — provider base will be set to the editor address
|
||||
DWORD pid = GetCurrentProcessId();
|
||||
QString target = QString("%1:Reclass.exe").arg(pid);
|
||||
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
|
||||
#else
|
||||
project_new();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::openFile() {
|
||||
@@ -834,6 +886,10 @@ void MainWindow::saveFileAs() {
|
||||
project_save(nullptr, true);
|
||||
}
|
||||
|
||||
void MainWindow::closeFile() {
|
||||
project_close();
|
||||
}
|
||||
|
||||
void MainWindow::addNode() {
|
||||
auto* ctrl = activeController();
|
||||
if (!ctrl) return;
|
||||
@@ -1005,6 +1061,13 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
statusBar()->setPalette(sbPal);
|
||||
}
|
||||
|
||||
// Workspace tree: text color matches menu bar
|
||||
if (m_workspaceTree) {
|
||||
QPalette tp = m_workspaceTree->palette();
|
||||
tp.setColor(QPalette::Text, theme.textDim);
|
||||
m_workspaceTree->setPalette(tp);
|
||||
}
|
||||
|
||||
// Split pane tab widgets
|
||||
for (auto& state : m_tabs) {
|
||||
for (auto& pane : state.panes) {
|
||||
@@ -1024,6 +1087,52 @@ void MainWindow::editTheme() {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: when adding more and more options, this func becomes very clunky. Fix
|
||||
void MainWindow::showOptionsDialog() {
|
||||
auto& tm = ThemeManager::instance();
|
||||
OptionsResult current;
|
||||
current.themeIndex = tm.currentIndex();
|
||||
current.fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
|
||||
current.menuBarTitleCase = m_titleBar->menuBarTitleCase();
|
||||
current.showIcon = QSettings("Reclass", "Reclass").value("showIcon", false).toBool();
|
||||
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
|
||||
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool();
|
||||
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||
|
||||
OptionsDialog dlg(current, this);
|
||||
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK
|
||||
|
||||
auto r = dlg.result();
|
||||
|
||||
if (r.themeIndex != current.themeIndex)
|
||||
tm.setCurrent(r.themeIndex);
|
||||
|
||||
if (r.fontName != current.fontName)
|
||||
setEditorFont(r.fontName);
|
||||
|
||||
if (r.menuBarTitleCase != current.menuBarTitleCase) {
|
||||
m_titleBar->setMenuBarTitleCase(r.menuBarTitleCase);
|
||||
QSettings("Reclass", "Reclass").setValue("menuBarTitleCase", r.menuBarTitleCase);
|
||||
}
|
||||
|
||||
if (r.showIcon != current.showIcon) {
|
||||
m_titleBar->setShowIcon(r.showIcon);
|
||||
QSettings("Reclass", "Reclass").setValue("showIcon", r.showIcon);
|
||||
}
|
||||
|
||||
if (r.safeMode != current.safeMode)
|
||||
QSettings("Reclass", "Reclass").setValue("safeMode", r.safeMode);
|
||||
|
||||
if (r.autoStartMcp != current.autoStartMcp)
|
||||
QSettings("Reclass", "Reclass").setValue("autoStartMcp", r.autoStartMcp);
|
||||
|
||||
if (r.refreshMs != current.refreshMs) {
|
||||
QSettings("Reclass", "Reclass").setValue("refreshMs", r.refreshMs);
|
||||
for (auto& tab : m_tabs)
|
||||
tab.ctrl->setRefreshInterval(r.refreshMs);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::setEditorFont(const QString& fontName) {
|
||||
QSettings settings("Reclass", "Reclass");
|
||||
settings.setValue("font", fontName);
|
||||
@@ -1259,6 +1368,110 @@ void MainWindow::exportCpp() {
|
||||
m_statusLabel->setText("Exported to " + QFileInfo(path).fileName());
|
||||
}
|
||||
|
||||
// ── Export ReClass XML ──
|
||||
|
||||
void MainWindow::exportReclassXmlAction() {
|
||||
auto* tab = activeTab();
|
||||
if (!tab) return;
|
||||
|
||||
QString path = QFileDialog::getSaveFileName(this,
|
||||
"Export ReClass XML", {}, "ReClass XML (*.reclass);;All Files (*)");
|
||||
if (path.isEmpty()) return;
|
||||
|
||||
QString error;
|
||||
if (!rcx::exportReclassXml(tab->doc->tree, path, &error)) {
|
||||
QMessageBox::warning(this, "Export Failed",
|
||||
error.isEmpty() ? QStringLiteral("Could not export") : error);
|
||||
return;
|
||||
}
|
||||
|
||||
int classCount = 0;
|
||||
for (const auto& n : tab->doc->tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||
|
||||
m_statusLabel->setText(QStringLiteral("Exported %1 classes to %2")
|
||||
.arg(classCount).arg(QFileInfo(path).fileName()));
|
||||
}
|
||||
|
||||
// ── Import ReClass XML ──
|
||||
|
||||
void MainWindow::importReclassXml() {
|
||||
QString filePath = QFileDialog::getOpenFileName(this,
|
||||
"Import ReClass XML", {},
|
||||
"ReClass XML (*.reclass *.MemeCls *.xml);;All Files (*)");
|
||||
if (filePath.isEmpty()) return;
|
||||
|
||||
QString error;
|
||||
NodeTree tree = rcx::importReclassXml(filePath, &error);
|
||||
if (tree.nodes.isEmpty()) {
|
||||
QMessageBox::warning(this, "Import Failed", error.isEmpty()
|
||||
? QStringLiteral("No data found in file") : error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Count root structs for status message
|
||||
int classCount = 0;
|
||||
for (const auto& n : tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||
|
||||
auto* doc = new RcxDocument(this);
|
||||
doc->tree = std::move(tree);
|
||||
|
||||
m_mdiArea->closeAllSubWindows();
|
||||
createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
|
||||
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||
}
|
||||
|
||||
// ── Import from Source ──
|
||||
|
||||
void MainWindow::importFromSource() {
|
||||
QDialog dlg(this);
|
||||
dlg.setWindowTitle("Import from Source");
|
||||
dlg.resize(700, 600);
|
||||
|
||||
auto* layout = new QVBoxLayout(&dlg);
|
||||
|
||||
auto* sci = new QsciScintilla(&dlg);
|
||||
setupRenderedSci(sci);
|
||||
sci->setReadOnly(false);
|
||||
sci->setMarginWidth(0, "00000");
|
||||
layout->addWidget(sci);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
|
||||
buttons->button(QDialogButtonBox::Ok)->setText("Import");
|
||||
layout->addWidget(buttons);
|
||||
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
|
||||
|
||||
if (dlg.exec() != QDialog::Accepted) return;
|
||||
|
||||
QString source = sci->text();
|
||||
if (source.trimmed().isEmpty()) return;
|
||||
|
||||
QString error;
|
||||
NodeTree tree = rcx::importFromSource(source, &error);
|
||||
if (tree.nodes.isEmpty()) {
|
||||
QMessageBox::warning(this, "Import Failed", error.isEmpty()
|
||||
? QStringLiteral("No struct definitions found") : error);
|
||||
return;
|
||||
}
|
||||
|
||||
int classCount = 0;
|
||||
for (const auto& n : tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||
|
||||
auto* doc = new RcxDocument(this);
|
||||
doc->tree = std::move(tree);
|
||||
|
||||
m_mdiArea->closeAllSubWindows();
|
||||
createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from source").arg(classCount));
|
||||
}
|
||||
|
||||
// ── Type Aliases Dialog ──
|
||||
|
||||
void MainWindow::showTypeAliasesDialog() {
|
||||
@@ -1318,16 +1531,14 @@ void MainWindow::showTypeAliasesDialog() {
|
||||
|
||||
// ── Project Lifecycle API ──
|
||||
|
||||
QMdiSubWindow* MainWindow::project_new() {
|
||||
QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
|
||||
auto* doc = new RcxDocument(this);
|
||||
|
||||
// Cross-platform writable buffer, zeroed (256 bytes covers Ball struct + spare)
|
||||
QByteArray data(256, '\0');
|
||||
doc->loadData(data);
|
||||
doc->tree.baseAddress = 0x00400000;
|
||||
|
||||
// Build Ball + Material demo structs
|
||||
buildBallDemo(doc->tree);
|
||||
buildEmptyStruct(doc->tree, classKeyword);
|
||||
|
||||
auto* sub = createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
@@ -1338,16 +1549,57 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
||||
QString filePath = path;
|
||||
if (filePath.isEmpty()) {
|
||||
filePath = QFileDialog::getOpenFileName(this,
|
||||
"Open Definition", {}, "Reclass (*.rcx);;JSON (*.json);;All (*)");
|
||||
"Open Definition", {},
|
||||
"All Supported (*.rcx *.json *.reclass *.MemeCls *.xml)"
|
||||
";;Reclass (*.rcx)"
|
||||
";;JSON (*.json)"
|
||||
";;ReClass XML (*.reclass *.MemeCls *.xml)"
|
||||
";;All (*)");
|
||||
if (filePath.isEmpty()) return nullptr;
|
||||
}
|
||||
|
||||
// Detect if this is an XML-based ReClass file by checking first bytes
|
||||
bool isXml = false;
|
||||
{
|
||||
QFile probe(filePath);
|
||||
if (probe.open(QIODevice::ReadOnly)) {
|
||||
QByteArray head = probe.read(64);
|
||||
isXml = head.trimmed().startsWith("<?xml") || head.trimmed().startsWith("<ReClass")
|
||||
|| head.trimmed().startsWith("<MemeCls");
|
||||
}
|
||||
}
|
||||
|
||||
if (isXml) {
|
||||
QString error;
|
||||
NodeTree tree = rcx::importReclassXml(filePath, &error);
|
||||
if (tree.nodes.isEmpty()) {
|
||||
QMessageBox::warning(this, "Import Failed", error.isEmpty()
|
||||
? QStringLiteral("No data found in file") : error);
|
||||
return nullptr;
|
||||
}
|
||||
auto* doc = new RcxDocument(this);
|
||||
doc->tree = std::move(tree);
|
||||
m_mdiArea->closeAllSubWindows();
|
||||
auto* sub = createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
int classCount = 0;
|
||||
for (const auto& n : doc->tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
|
||||
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||
return sub;
|
||||
}
|
||||
|
||||
auto* doc = new RcxDocument(this);
|
||||
if (!doc->load(filePath)) {
|
||||
QMessageBox::warning(this, "Error", "Failed to load: " + filePath);
|
||||
delete doc;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Close all existing tabs so the project replaces the current state
|
||||
m_mdiArea->closeAllSubWindows();
|
||||
|
||||
auto* sub = createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
return sub;
|
||||
@@ -1380,7 +1632,7 @@ void MainWindow::project_close(QMdiSubWindow* sub) {
|
||||
// ── Workspace Dock ──
|
||||
|
||||
void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceDock = new QDockWidget("Workspace", this);
|
||||
m_workspaceDock = new QDockWidget("Project Tree", this);
|
||||
m_workspaceDock->setObjectName("WorkspaceDock");
|
||||
m_workspaceDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
|
||||
|
||||
@@ -1390,81 +1642,106 @@ void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceTree->setModel(m_workspaceModel);
|
||||
m_workspaceTree->setHeaderHidden(true);
|
||||
m_workspaceTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_workspaceTree->setExpandsOnDoubleClick(false);
|
||||
m_workspaceTree->setMouseTracking(true);
|
||||
|
||||
// Match editor font
|
||||
{
|
||||
QSettings settings("Reclass", "Reclass");
|
||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
||||
QFont f(fontName, 12);
|
||||
f.setFixedPitch(true);
|
||||
m_workspaceTree->setFont(f);
|
||||
}
|
||||
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_workspaceTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
QModelIndex index = m_workspaceTree->indexAt(pos);
|
||||
if (!index.isValid()) return;
|
||||
|
||||
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
|
||||
|
||||
// Right-click on "Project" group → New Class / New Struct / New Enum
|
||||
if (structId == rcx::kGroupSentinel) {
|
||||
QMenu menu;
|
||||
auto* actClass = menu.addAction("New Class");
|
||||
auto* actStruct = menu.addAction("New Struct");
|
||||
auto* actEnum = menu.addAction("New Enum");
|
||||
QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos));
|
||||
if (chosen == actClass) newClass();
|
||||
else if (chosen == actStruct) newStruct();
|
||||
else if (chosen == actEnum) newEnum();
|
||||
return;
|
||||
}
|
||||
|
||||
if (structId == 0) return;
|
||||
|
||||
auto subVar = index.data(Qt::UserRole);
|
||||
if (!subVar.isValid()) return;
|
||||
auto* sub = static_cast<QMdiSubWindow*>(subVar.value<void*>());
|
||||
if (!sub || !m_tabs.contains(sub)) return;
|
||||
|
||||
auto& tab = m_tabs[sub];
|
||||
int ni = tab.doc->tree.indexOfId(structId);
|
||||
if (ni < 0) return;
|
||||
QString kw = tab.doc->tree.nodes[ni].resolvedClassKeyword();
|
||||
|
||||
QMenu menu;
|
||||
QAction* actConvert = nullptr;
|
||||
// class↔struct conversion only (no enum conversion)
|
||||
if (kw == QStringLiteral("class"))
|
||||
actConvert = menu.addAction("Convert to Struct");
|
||||
else if (kw == QStringLiteral("struct"))
|
||||
actConvert = menu.addAction("Convert to Class");
|
||||
auto* actDelete = menu.addAction(QIcon(":/vsicons/remove.svg"), "Delete");
|
||||
|
||||
QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos));
|
||||
if (chosen == actDelete) {
|
||||
tab.ctrl->removeNode(ni);
|
||||
rebuildWorkspaceModel();
|
||||
} else if (chosen && chosen == actConvert) {
|
||||
QString newKw = kw == QStringLiteral("class")
|
||||
? QStringLiteral("struct") : QStringLiteral("class");
|
||||
QString oldKw = tab.doc->tree.nodes[ni].resolvedClassKeyword();
|
||||
tab.doc->undoStack.push(new rcx::RcxCommand(tab.ctrl,
|
||||
rcx::cmd::ChangeClassKeyword{structId, oldKw, newKw}));
|
||||
rebuildWorkspaceModel();
|
||||
}
|
||||
});
|
||||
|
||||
m_workspaceDock->setWidget(m_workspaceTree);
|
||||
addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock);
|
||||
m_workspaceDock->hide();
|
||||
|
||||
connect(m_workspaceTree, &QTreeView::doubleClicked, this, [this](const QModelIndex& index) {
|
||||
// Data roles: UserRole=QMdiSubWindow*, UserRole+1=structId, UserRole+2=nodeId
|
||||
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
|
||||
|
||||
if (structId == rcx::kGroupSentinel) {
|
||||
// "Project" folder: toggle expand/collapse
|
||||
m_workspaceTree->setExpanded(index, !m_workspaceTree->isExpanded(index));
|
||||
return;
|
||||
}
|
||||
|
||||
auto subVar = index.data(Qt::UserRole);
|
||||
if (!subVar.isValid()) return;
|
||||
|
||||
auto* sub = static_cast<QMdiSubWindow*>(subVar.value<void*>());
|
||||
if (!sub || !m_tabs.contains(sub)) return;
|
||||
|
||||
m_mdiArea->setActiveSubWindow(sub);
|
||||
|
||||
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||
auto nodeIdVar = index.data(Qt::UserRole + 2);
|
||||
|
||||
if (structIdVar.isValid()) {
|
||||
// Double-clicked a struct: set as view root
|
||||
uint64_t structId = structIdVar.toULongLong();
|
||||
auto& tree = m_tabs[sub].doc->tree;
|
||||
int ni = tree.indexOfId(structId);
|
||||
if (ni >= 0) tree.nodes[ni].collapsed = false;
|
||||
m_tabs[sub].ctrl->setViewRootId(structId);
|
||||
m_tabs[sub].ctrl->scrollToNodeId(structId);
|
||||
} else if (nodeIdVar.isValid()) {
|
||||
// Double-clicked a field: find its root struct, set as view root, scroll to field
|
||||
uint64_t nodeId = nodeIdVar.toULongLong();
|
||||
auto& tree = m_tabs[sub].doc->tree;
|
||||
// Walk up to find root struct
|
||||
uint64_t rootId = 0;
|
||||
uint64_t cur = nodeId;
|
||||
while (cur != 0) {
|
||||
int idx = tree.indexOfId(cur);
|
||||
if (idx < 0) break;
|
||||
if (tree.nodes[idx].parentId == 0) { rootId = cur; break; }
|
||||
cur = tree.nodes[idx].parentId;
|
||||
}
|
||||
if (rootId != 0) {
|
||||
int ri = tree.indexOfId(rootId);
|
||||
if (ri >= 0) tree.nodes[ri].collapsed = false;
|
||||
m_tabs[sub].ctrl->setViewRootId(rootId);
|
||||
}
|
||||
m_tabs[sub].ctrl->scrollToNodeId(nodeId);
|
||||
} else if (!index.parent().isValid()) {
|
||||
// Double-clicked project root: clear view root to show all
|
||||
m_tabs[sub].ctrl->setViewRootId(0);
|
||||
}
|
||||
// Type/Enum node: navigate to it
|
||||
auto& tree = m_tabs[sub].doc->tree;
|
||||
int ni = tree.indexOfId(structId);
|
||||
if (ni >= 0) tree.nodes[ni].collapsed = false;
|
||||
m_tabs[sub].ctrl->setViewRootId(structId);
|
||||
m_tabs[sub].ctrl->scrollToNodeId(structId);
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::rebuildWorkspaceModel() {
|
||||
m_workspaceModel->clear();
|
||||
|
||||
auto* sub = m_mdiArea->activeSubWindow();
|
||||
if (!sub || !m_tabs.contains(sub)) return;
|
||||
|
||||
TabState& tab = m_tabs[sub];
|
||||
QString tabName = tab.doc->filePath.isEmpty()
|
||||
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
|
||||
: QFileInfo(tab.doc->filePath).fileName();
|
||||
|
||||
buildWorkspaceModel(m_workspaceModel, tab.doc->tree, tabName,
|
||||
static_cast<void*>(sub));
|
||||
m_workspaceTree->expandAll();
|
||||
QVector<rcx::TabInfo> tabs;
|
||||
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
|
||||
TabState& tab = it.value();
|
||||
QString name = tab.doc->filePath.isEmpty()
|
||||
? rootName(tab.doc->tree, tab.ctrl->viewRootId())
|
||||
: QFileInfo(tab.doc->filePath).fileName();
|
||||
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
|
||||
}
|
||||
rcx::buildProjectExplorer(m_workspaceModel, tabs);
|
||||
m_workspaceTree->expandToDepth(1);
|
||||
}
|
||||
|
||||
void MainWindow::showPluginsDialog() {
|
||||
@@ -1621,27 +1898,11 @@ int main(int argc, char* argv[]) {
|
||||
rcx::MainWindow window;
|
||||
window.setWindowIcon(QIcon(":/icons/class.png"));
|
||||
|
||||
bool screenshotMode = app.arguments().contains("--screenshot");
|
||||
if (screenshotMode)
|
||||
window.setWindowOpacity(0.0);
|
||||
window.show();
|
||||
|
||||
// Auto-open demo project from saved .rcx file
|
||||
QMetaObject::invokeMethod(&window, "selfTest");
|
||||
|
||||
if (screenshotMode) {
|
||||
QString out = "screenshot.png";
|
||||
int idx = app.arguments().indexOf("--screenshot");
|
||||
if (idx + 1 < app.arguments().size())
|
||||
out = app.arguments().at(idx + 1);
|
||||
|
||||
QTimer::singleShot(1000, [&window, out]() {
|
||||
QDir().mkpath(QFileInfo(out).absolutePath());
|
||||
window.grab().save(out);
|
||||
::_Exit(0); // immediate exit — no need for clean shutdown in screenshot mode
|
||||
});
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,13 +25,14 @@ public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void newFile();
|
||||
void newDocument();
|
||||
void newClass();
|
||||
void newStruct();
|
||||
void newEnum();
|
||||
void selfTest();
|
||||
void openFile();
|
||||
void saveFile();
|
||||
void saveFileAs();
|
||||
|
||||
void closeFile();
|
||||
|
||||
void addNode();
|
||||
void removeNode();
|
||||
@@ -47,12 +48,16 @@ private slots:
|
||||
void toggleMcp();
|
||||
void setEditorFont(const QString& fontName);
|
||||
void exportCpp();
|
||||
void exportReclassXmlAction();
|
||||
void importFromSource();
|
||||
void importReclassXml();
|
||||
void showTypeAliasesDialog();
|
||||
void editTheme();
|
||||
void showOptionsDialog();
|
||||
|
||||
public:
|
||||
// Project Lifecycle API
|
||||
QMdiSubWindow* project_new();
|
||||
QMdiSubWindow* project_new(const QString& classKeyword = QString());
|
||||
QMdiSubWindow* project_open(const QString& path = {});
|
||||
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
|
||||
void project_close(QMdiSubWindow* sub = nullptr);
|
||||
|
||||
@@ -248,7 +248,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
"collapse: {op:'collapse', nodeId:'ID', collapsed:true}. "
|
||||
"Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. "
|
||||
"Kinds: Hex8 Hex16 Hex32 Hex64 Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 "
|
||||
"Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Padding Struct Array"},
|
||||
"Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Struct Array"},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
@@ -287,7 +287,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
{"name", "hex.read"},
|
||||
{"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type "
|
||||
"interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). "
|
||||
"Offset is provider-relative (0-based) unless baseRelative=true."},
|
||||
"Offset is tree-relative (0-based, baseAddress added automatically) "
|
||||
"unless baseRelative=true (offset is absolute)."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
@@ -793,7 +794,7 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
|
||||
}
|
||||
|
||||
if (args.contains("pid")) {
|
||||
uint32_t pid = (uint32_t)args.value("pid").toInteger();
|
||||
uint32_t pid = (uint32_t)args.value("pid").toInt();
|
||||
QString name = args.value("processName").toString();
|
||||
if (name.isEmpty()) name = QString("PID %1").arg(pid);
|
||||
QString target = QString("%1:%2").arg(pid).arg(name);
|
||||
@@ -825,8 +826,8 @@ QJsonObject McpBridge::toolHexRead(const QJsonObject& args) {
|
||||
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
|
||||
int length = qMin(args.value("length").toInt(64), 4096);
|
||||
|
||||
if (args.value("baseRelative").toBool())
|
||||
offset -= (int64_t)tab->doc->tree.baseAddress;
|
||||
if (!args.value("baseRelative").toBool())
|
||||
offset += (int64_t)tab->doc->tree.baseAddress;
|
||||
|
||||
if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
|
||||
return makeTextResult("Cannot read at offset " + QString::number(offset), true);
|
||||
@@ -907,8 +908,8 @@ QJsonObject McpBridge::toolHexWrite(const QJsonObject& args) {
|
||||
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
|
||||
QString hexStr = args.value("hexBytes").toString().remove(' ');
|
||||
|
||||
if (args.value("baseRelative").toBool())
|
||||
offset -= (int64_t)doc->tree.baseAddress;
|
||||
if (!args.value("baseRelative").toBool())
|
||||
offset += (int64_t)doc->tree.baseAddress;
|
||||
|
||||
if (hexStr.size() % 2 != 0)
|
||||
return makeTextResult("Hex string must have even length", true);
|
||||
|
||||
261
src/optionsdialog.cpp
Normal file
261
src/optionsdialog.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
#include "optionsdialog.h"
|
||||
#include "themes/thememanager.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QPushButton>
|
||||
#include <QGroupBox>
|
||||
#include <QLabel>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <functional>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle("Options");
|
||||
setFixedSize(700, 450);
|
||||
|
||||
auto* mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setSpacing(8);
|
||||
mainLayout->setContentsMargins(10, 10, 10, 10);
|
||||
|
||||
// -- Middle: left column (search + tree) | right column (pages) --
|
||||
auto* middleLayout = new QHBoxLayout;
|
||||
middleLayout->setSpacing(8);
|
||||
|
||||
// Left column: search bar + tree
|
||||
auto* leftColumn = new QVBoxLayout;
|
||||
leftColumn->setSpacing(4);
|
||||
|
||||
m_search = new QLineEdit;
|
||||
m_search->setPlaceholderText("Search Options (Ctrl+E)");
|
||||
m_search->setClearButtonEnabled(true);
|
||||
connect(m_search, &QLineEdit::textChanged, this, &OptionsDialog::filterTree);
|
||||
leftColumn->addWidget(m_search);
|
||||
|
||||
m_tree = new QTreeWidget;
|
||||
m_tree->setHeaderHidden(true);
|
||||
m_tree->setRootIsDecorated(true);
|
||||
m_tree->setFixedWidth(200);
|
||||
|
||||
auto* envItem = new QTreeWidgetItem(m_tree, {"Environment"});
|
||||
auto* generalItem = new QTreeWidgetItem(envItem, {"General"});
|
||||
m_tree->expandAll();
|
||||
m_tree->setCurrentItem(generalItem);
|
||||
leftColumn->addWidget(m_tree, 1);
|
||||
|
||||
middleLayout->addLayout(leftColumn);
|
||||
|
||||
// Right column: stacked pages with group boxes
|
||||
m_pages = new QStackedWidget;
|
||||
|
||||
// -- General page --
|
||||
auto* generalPage = new QWidget;
|
||||
auto* generalLayout = new QVBoxLayout(generalPage);
|
||||
generalLayout->setContentsMargins(0, 0, 0, 0);
|
||||
generalLayout->setSpacing(8);
|
||||
|
||||
// Refresh Rate group box
|
||||
auto* refreshGroup = new QGroupBox("Refresh Rate");
|
||||
auto* refreshLayout = new QFormLayout(refreshGroup);
|
||||
refreshLayout->setSpacing(8);
|
||||
refreshLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||
|
||||
m_refreshSpin = new QSpinBox;
|
||||
m_refreshSpin->setRange(1, 60000);
|
||||
m_refreshSpin->setSingleStep(50);
|
||||
m_refreshSpin->setValue(current.refreshMs);
|
||||
m_refreshSpin->setSuffix(" ms");
|
||||
m_refreshSpin->setObjectName("refreshSpin");
|
||||
refreshLayout->addRow("Interval:", m_refreshSpin);
|
||||
|
||||
auto* refreshDesc = new QLabel(
|
||||
"How often live memory is re-read and the view is updated, in milliseconds. "
|
||||
"Lower values give faster updates but use more CPU. Default: 660 ms.");
|
||||
refreshDesc->setWordWrap(true);
|
||||
refreshDesc->setContentsMargins(0, 0, 0, 0);
|
||||
refreshLayout->addRow(refreshDesc);
|
||||
|
||||
generalLayout->addWidget(refreshGroup);
|
||||
|
||||
// Visual Experience group box
|
||||
auto* visualGroup = new QGroupBox("Visual Experience");
|
||||
auto* visualLayout = new QFormLayout(visualGroup);
|
||||
visualLayout->setSpacing(8);
|
||||
visualLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||
|
||||
m_themeCombo = new QComboBox;
|
||||
auto& tm = ThemeManager::instance();
|
||||
for (const auto& theme : tm.themes())
|
||||
m_themeCombo->addItem(theme.name);
|
||||
m_themeCombo->setCurrentIndex(current.themeIndex);
|
||||
m_themeCombo->setObjectName("themeCombo");
|
||||
visualLayout->addRow("Color theme:", m_themeCombo);
|
||||
|
||||
m_fontCombo = new QComboBox;
|
||||
m_fontCombo->addItem("JetBrains Mono");
|
||||
m_fontCombo->addItem("Consolas");
|
||||
m_fontCombo->setCurrentText(current.fontName);
|
||||
m_fontCombo->setObjectName("fontCombo");
|
||||
visualLayout->addRow("Editor Font:", m_fontCombo);
|
||||
|
||||
m_titleCaseCheck = new QCheckBox("Apply title case styling to menu bar");
|
||||
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
|
||||
visualLayout->addRow(m_titleCaseCheck);
|
||||
|
||||
m_showIconCheck = new QCheckBox("Show icon in title bar");
|
||||
m_showIconCheck->setChecked(current.showIcon);
|
||||
visualLayout->addRow(m_showIconCheck);
|
||||
|
||||
generalLayout->addWidget(visualGroup);
|
||||
|
||||
// Safe Mode group box
|
||||
auto* safeModeGroup = new QGroupBox("Preview Features");
|
||||
auto* safeModeLayout = new QVBoxLayout(safeModeGroup);
|
||||
safeModeLayout->setSpacing(4);
|
||||
|
||||
m_safeModeCheck = new QCheckBox("Safe Mode");
|
||||
m_safeModeCheck->setChecked(current.safeMode);
|
||||
safeModeLayout->addWidget(m_safeModeCheck);
|
||||
|
||||
auto* safeModeDesc = new QLabel(
|
||||
"Enable to use the default OS icon for this application and "
|
||||
"create the window with the name of the executable file.");
|
||||
safeModeDesc->setWordWrap(true);
|
||||
safeModeDesc->setContentsMargins(20, 0, 0, 0); // indent under checkbox
|
||||
safeModeLayout->addWidget(safeModeDesc);
|
||||
|
||||
generalLayout->addWidget(safeModeGroup);
|
||||
generalLayout->addStretch();
|
||||
|
||||
m_pages->addWidget(generalPage); // index 0
|
||||
m_pageKeywords[generalItem] = collectPageKeywords(generalPage);
|
||||
|
||||
// -- AI Features page --
|
||||
auto* aiItem = new QTreeWidgetItem(envItem, {"AI Features"});
|
||||
|
||||
auto* aiPage = new QWidget;
|
||||
auto* aiLayout = new QVBoxLayout(aiPage);
|
||||
aiLayout->setContentsMargins(0, 0, 0, 0);
|
||||
aiLayout->setSpacing(8);
|
||||
|
||||
auto* mcpGroup = new QGroupBox("MCP Server");
|
||||
auto* mcpLayout = new QVBoxLayout(mcpGroup);
|
||||
mcpLayout->setSpacing(4);
|
||||
|
||||
m_autoMcpCheck = new QCheckBox("Auto-start MCP server");
|
||||
m_autoMcpCheck->setChecked(current.autoStartMcp);
|
||||
mcpLayout->addWidget(m_autoMcpCheck);
|
||||
|
||||
auto* mcpDesc = new QLabel(
|
||||
"Automatically start the MCP bridge server when the application launches, "
|
||||
"allowing external AI tools to connect and interact with the editor.");
|
||||
mcpDesc->setWordWrap(true);
|
||||
mcpDesc->setContentsMargins(20, 0, 0, 0);
|
||||
mcpLayout->addWidget(mcpDesc);
|
||||
|
||||
aiLayout->addWidget(mcpGroup);
|
||||
aiLayout->addStretch();
|
||||
|
||||
m_pages->addWidget(aiPage); // index 1
|
||||
m_pageKeywords[aiItem] = collectPageKeywords(aiPage);
|
||||
|
||||
// -- Generator page --
|
||||
auto* generatorItem = new QTreeWidgetItem(envItem, {"Generator"});
|
||||
|
||||
auto* generatorPage = new QWidget;
|
||||
auto* generatorLayout = new QVBoxLayout(generatorPage);
|
||||
generatorLayout->setContentsMargins(0, 0, 0, 0);
|
||||
generatorLayout->setSpacing(8);
|
||||
generatorLayout->addStretch();
|
||||
|
||||
m_pages->addWidget(generatorPage); // index 2
|
||||
m_pageKeywords[generatorItem] = collectPageKeywords(generatorPage);
|
||||
|
||||
middleLayout->addWidget(m_pages, 1);
|
||||
|
||||
mainLayout->addLayout(middleLayout, 1);
|
||||
|
||||
// Tree <-> page connection
|
||||
m_itemPageIndex[generalItem] = 0;
|
||||
m_itemPageIndex[aiItem] = 1;
|
||||
m_itemPageIndex[generatorItem] = 2;
|
||||
connect(m_tree, &QTreeWidget::currentItemChanged, this,
|
||||
[this](QTreeWidgetItem* item, QTreeWidgetItem*) {
|
||||
if (!item) return;
|
||||
auto it = m_itemPageIndex.find(item);
|
||||
if (it != m_itemPageIndex.end())
|
||||
m_pages->setCurrentIndex(it.value());
|
||||
});
|
||||
|
||||
// -- Button box --
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
mainLayout->addWidget(buttons);
|
||||
|
||||
}
|
||||
|
||||
OptionsResult OptionsDialog::result() const {
|
||||
OptionsResult r;
|
||||
r.themeIndex = m_themeCombo->currentIndex();
|
||||
r.fontName = m_fontCombo->currentText();
|
||||
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
|
||||
r.showIcon = m_showIconCheck->isChecked();
|
||||
r.safeMode = m_safeModeCheck->isChecked();
|
||||
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
||||
r.refreshMs = m_refreshSpin->value();
|
||||
return r;
|
||||
}
|
||||
|
||||
QStringList OptionsDialog::collectPageKeywords(QWidget* page) {
|
||||
QStringList keywords;
|
||||
for (auto* child : page->findChildren<QWidget*>()) {
|
||||
if (auto* label = qobject_cast<QLabel*>(child))
|
||||
keywords << label->text();
|
||||
else if (auto* cb = qobject_cast<QCheckBox*>(child))
|
||||
keywords << cb->text();
|
||||
else if (auto* gb = qobject_cast<QGroupBox*>(child))
|
||||
keywords << gb->title();
|
||||
else if (auto* combo = qobject_cast<QComboBox*>(child)) {
|
||||
for (int i = 0; i < combo->count(); ++i)
|
||||
keywords << combo->itemText(i);
|
||||
}
|
||||
}
|
||||
return keywords;
|
||||
}
|
||||
|
||||
void OptionsDialog::filterTree(const QString& text) {
|
||||
std::function<bool(QTreeWidgetItem*)> filter = [&](QTreeWidgetItem* item) -> bool {
|
||||
bool anyChildVisible = false;
|
||||
for (int i = 0; i < item->childCount(); ++i) {
|
||||
if (filter(item->child(i)))
|
||||
anyChildVisible = true;
|
||||
}
|
||||
|
||||
bool selfMatch = item->text(0).contains(text, Qt::CaseInsensitive);
|
||||
if (!selfMatch) {
|
||||
for (const auto& kw : m_pageKeywords.value(item)) {
|
||||
if (kw.contains(text, Qt::CaseInsensitive)) {
|
||||
selfMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool visible = selfMatch || anyChildVisible;
|
||||
item->setHidden(!visible);
|
||||
|
||||
if (visible && item->childCount() > 0)
|
||||
item->setExpanded(true);
|
||||
|
||||
return visible;
|
||||
};
|
||||
|
||||
for (int i = 0; i < m_tree->topLevelItemCount(); ++i)
|
||||
filter(m_tree->topLevelItem(i));
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
51
src/optionsdialog.h
Normal file
51
src/optionsdialog.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QTreeWidget>
|
||||
#include <QStackedWidget>
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
#include <QHash>
|
||||
#include <QSpinBox>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct OptionsResult {
|
||||
int themeIndex = 0;
|
||||
QString fontName;
|
||||
bool menuBarTitleCase = true;
|
||||
bool showIcon = false;
|
||||
bool safeMode = false;
|
||||
bool autoStartMcp = false;
|
||||
int refreshMs = 660;
|
||||
};
|
||||
|
||||
class OptionsDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OptionsDialog(const OptionsResult& current, QWidget* parent = nullptr);
|
||||
|
||||
OptionsResult result() const;
|
||||
|
||||
private:
|
||||
void filterTree(const QString& text);
|
||||
static QStringList collectPageKeywords(QWidget* page);
|
||||
|
||||
QLineEdit* m_search = nullptr;
|
||||
QTreeWidget* m_tree = nullptr;
|
||||
QStackedWidget* m_pages = nullptr;
|
||||
QComboBox* m_themeCombo = nullptr;
|
||||
QComboBox* m_fontCombo = nullptr;
|
||||
QCheckBox* m_titleCaseCheck = nullptr;
|
||||
QCheckBox* m_showIconCheck = nullptr;
|
||||
QCheckBox* m_safeModeCheck = nullptr;
|
||||
QCheckBox* m_autoMcpCheck = nullptr;
|
||||
QSpinBox* m_refreshSpin = nullptr;
|
||||
|
||||
// searchable keywords per leaf tree item
|
||||
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||
// tree item → stacked widget page index
|
||||
QHash<QTreeWidgetItem*, int> m_itemPageIndex;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
@@ -33,10 +33,10 @@ public:
|
||||
// Examples: "File", "Process", "Socket"
|
||||
virtual QString kind() const { return QStringLiteral("File"); }
|
||||
|
||||
// Base address for providers that offset reads (e.g. process memory).
|
||||
// Initial base address discovered by the provider (e.g. main module base).
|
||||
// Used by the controller to set tree.baseAddress on first attach.
|
||||
// For file/buffer providers this is always 0.
|
||||
virtual uint64_t base() const { return 0; }
|
||||
virtual void setBase(uint64_t newBase) { Q_UNUSED(newBase); }
|
||||
|
||||
// Resolve an absolute address to a symbol name.
|
||||
// Returns empty string if no symbol is known.
|
||||
|
||||
@@ -47,5 +47,9 @@
|
||||
<file alias="selection.svg">vsicons/list-selection.svg</file>
|
||||
<file alias="symbol-numeric.svg">vsicons/symbol-numeric.svg</file>
|
||||
<file alias="symbol-ruler.svg">vsicons/symbol-ruler.svg</file>
|
||||
<file alias="settings-gear.svg">vsicons/settings-gear.svg</file>
|
||||
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
|
||||
<file alias="folder.svg">vsicons/folder.svg</file>
|
||||
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
32
src/themes/defaults/mid.json
Normal file
32
src/themes/defaults/mid.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Mid",
|
||||
"background": "#0D1219",
|
||||
"backgroundAlt": "#121720",
|
||||
"surface": "#161C28",
|
||||
"border": "#1E2636",
|
||||
"borderFocused": "#485068",
|
||||
"button": "#181E2C",
|
||||
"text": "#B0B8CC",
|
||||
"textDim": "#505C74",
|
||||
"textMuted": "#384258",
|
||||
"textFaint": "#2C3448",
|
||||
"hover": "#121720",
|
||||
"selected": "#121720",
|
||||
"selection": "#1A2038",
|
||||
"syntaxKeyword": "#5688C0",
|
||||
"syntaxNumber": "#90B480",
|
||||
"syntaxString": "#B88060",
|
||||
"syntaxComment": "#385030",
|
||||
"syntaxPreproc": "#9868A8",
|
||||
"syntaxType": "#8FDBFE",
|
||||
"indHoverSpan": "#C09038",
|
||||
"indCmdPill": "#141A26",
|
||||
"indDataChanged": "#608C54",
|
||||
"indHeatCold": "#B09030",
|
||||
"indHeatWarm": "#C09038",
|
||||
"indHeatHot": "#C83838",
|
||||
"indHintGreen": "#385830",
|
||||
"markerPtr": "#C83838",
|
||||
"markerCycle": "#B89028",
|
||||
"markerError": "#481818"
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#858585",
|
||||
"textMuted": "#585858",
|
||||
"textFaint": "#505050",
|
||||
"hover": "#2b2b2b",
|
||||
"selected": "#232323",
|
||||
"hover": "#1e1e1e",
|
||||
"selected": "#1e1e1e",
|
||||
"selection": "#2b2b2b",
|
||||
"syntaxKeyword": "#569cd6",
|
||||
"syntaxNumber": "#b5cea8",
|
||||
@@ -22,6 +22,9 @@
|
||||
"indHoverSpan": "#E6B450",
|
||||
"indCmdPill": "#2a2a2a",
|
||||
"indDataChanged": "#8fbc7a",
|
||||
"indHeatCold": "#D4A945",
|
||||
"indHeatWarm": "#E6B450",
|
||||
"indHeatHot": "#f44747",
|
||||
"indHintGreen": "#5a8248",
|
||||
"markerPtr": "#f44747",
|
||||
"markerCycle": "#e5a00d",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#858585",
|
||||
"textMuted": "#636369",
|
||||
"textFaint": "#4d4d55",
|
||||
"hover": "#3e3e42",
|
||||
"selected": "#2d2d30",
|
||||
"hover": "#2c2c2f",
|
||||
"selected": "#262629",
|
||||
"selection": "#264f78",
|
||||
"syntaxKeyword": "#569cd6",
|
||||
"syntaxNumber": "#b5cea8",
|
||||
@@ -22,6 +22,9 @@
|
||||
"indHoverSpan": "#b180d7",
|
||||
"indCmdPill": "#2d2d30",
|
||||
"indDataChanged": "#8fbc7a",
|
||||
"indHeatCold": "#D4A945",
|
||||
"indHeatWarm": "#d69d85",
|
||||
"indHeatHot": "#f44747",
|
||||
"indHintGreen": "#5a8248",
|
||||
"markerPtr": "#f44747",
|
||||
"markerCycle": "#e5a00d",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"textDim": "#7a7a6e",
|
||||
"textMuted": "#555550",
|
||||
"textFaint": "#464646",
|
||||
"hover": "#373737",
|
||||
"selected": "#2d2d2d",
|
||||
"hover": "#282828",
|
||||
"selected": "#262626",
|
||||
"selection": "#21213A",
|
||||
"syntaxKeyword": "#AA9565",
|
||||
"syntaxNumber": "#AAA98C",
|
||||
@@ -22,6 +22,9 @@
|
||||
"indHoverSpan": "#AA9565",
|
||||
"indCmdPill": "#2a2a2a",
|
||||
"indDataChanged": "#6B959F",
|
||||
"indHeatCold": "#C4A44A",
|
||||
"indHeatWarm": "#AA9565",
|
||||
"indHeatHot": "#A05040",
|
||||
"indHintGreen": "#464646",
|
||||
"markerPtr": "#6B3B21",
|
||||
"markerCycle": "#AA9565",
|
||||
|
||||
@@ -28,6 +28,9 @@ const ThemeFieldMeta kThemeFields[] = {
|
||||
{"indHoverSpan", "Hover Span", "Indicators", &Theme::indHoverSpan},
|
||||
{"indCmdPill", "Cmd Pill", "Indicators", &Theme::indCmdPill},
|
||||
{"indDataChanged","Data Changed", "Indicators", &Theme::indDataChanged},
|
||||
{"indHeatCold", "Heat Cold", "Indicators", &Theme::indHeatCold},
|
||||
{"indHeatWarm", "Heat Warm", "Indicators", &Theme::indHeatWarm},
|
||||
{"indHeatHot", "Heat Hot", "Indicators", &Theme::indHeatHot},
|
||||
{"indHintGreen", "Hint Green", "Indicators", &Theme::indHintGreen},
|
||||
{"markerPtr", "Pointer", "Markers", &Theme::markerPtr},
|
||||
{"markerCycle", "Cycle", "Markers", &Theme::markerCycle},
|
||||
@@ -50,6 +53,14 @@ Theme Theme::fromJson(const QJsonObject& o) {
|
||||
if (o.contains(kThemeFields[i].key))
|
||||
t.*kThemeFields[i].ptr = QColor(o[kThemeFields[i].key].toString());
|
||||
}
|
||||
// Derive heat colors from the theme's own palette when keys are absent
|
||||
// cold = muted yellow, warm = hover/string amber, hot = marker red
|
||||
if (!t.indHeatCold.isValid())
|
||||
t.indHeatCold = QColor("#D4A945");
|
||||
if (!t.indHeatWarm.isValid())
|
||||
t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString;
|
||||
if (!t.indHeatHot.isValid())
|
||||
t.indHeatHot = t.markerPtr;
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,10 @@ struct Theme {
|
||||
// ── Indicators ──
|
||||
QColor indHoverSpan; // hover link text
|
||||
QColor indCmdPill; // command row pill bg
|
||||
QColor indDataChanged; // changed data values
|
||||
QColor indDataChanged; // changed data values (legacy, fallback for old themes)
|
||||
QColor indHeatCold; // heatmap level 1 (changed once)
|
||||
QColor indHeatWarm; // heatmap level 2 (moderate changes)
|
||||
QColor indHeatHot; // heatmap level 3 (frequent changes)
|
||||
QColor indHintGreen; // comment/hint text
|
||||
|
||||
// ── Markers ──
|
||||
|
||||
@@ -114,6 +114,33 @@ void TitleBarWidget::setShowIcon(bool show) {
|
||||
}
|
||||
}
|
||||
|
||||
void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
|
||||
m_titleCase = titleCase;
|
||||
for (QAction* action : m_menuBar->actions()) {
|
||||
QString text = action->text();
|
||||
QString clean = text;
|
||||
clean.remove('&');
|
||||
|
||||
if (titleCase) {
|
||||
action->setText("&" + clean.toUpper());
|
||||
} else {
|
||||
QString result;
|
||||
bool capitalizeNext = true;
|
||||
for (int i = 0; i < clean.length(); ++i) {
|
||||
QChar ch = clean[i];
|
||||
if (ch.isLetter()) {
|
||||
result += capitalizeNext ? ch.toUpper() : ch.toLower();
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
result += ch;
|
||||
if (ch.isSpace()) capitalizeNext = true;
|
||||
}
|
||||
}
|
||||
action->setText("&" + result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TitleBarWidget::updateMaximizeIcon() {
|
||||
if (window()->isMaximized())
|
||||
m_btnMax->setIcon(QIcon(":/vsicons/chrome-restore.svg"));
|
||||
|
||||
@@ -16,6 +16,8 @@ public:
|
||||
QMenuBar* menuBar() const { return m_menuBar; }
|
||||
void applyTheme(const Theme& theme);
|
||||
void setShowIcon(bool show);
|
||||
void setMenuBarTitleCase(bool titleCase);
|
||||
bool menuBarTitleCase() const { return m_titleCase; }
|
||||
|
||||
void updateMaximizeIcon();
|
||||
|
||||
@@ -32,6 +34,7 @@ private:
|
||||
QToolButton* m_btnClose = nullptr;
|
||||
|
||||
Theme m_theme;
|
||||
bool m_titleCase = true;
|
||||
|
||||
QToolButton* makeChromeButton(const QString& iconPath);
|
||||
void toggleMaximize();
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
#include <QIntValidator>
|
||||
#include <QElapsedTimer>
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
namespace rcx {
|
||||
@@ -96,6 +97,12 @@ public:
|
||||
int h = option.rect.height();
|
||||
int w = option.rect.width();
|
||||
|
||||
// Scale metrics from font height
|
||||
QFontMetrics fmMain(m_font);
|
||||
int iconSz = fmMain.height(); // icon matches text height
|
||||
int gutterW = fmMain.horizontalAdvance(QChar(0x25B8)) + 4;
|
||||
int iconColW = iconSz + 4;
|
||||
|
||||
// Section: centered dim text with horizontal rules
|
||||
if (isSection) {
|
||||
painter->setPen(t.textDim);
|
||||
@@ -121,7 +128,7 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
// 18px gutter: side triangle if current
|
||||
// Gutter: side triangle if current
|
||||
if (m_hasCurrent && m_filtered && row >= 0 && row < m_filtered->size()) {
|
||||
const TypeEntry& entry = (*m_filtered)[row];
|
||||
bool isCurrent = false;
|
||||
@@ -130,20 +137,20 @@ public:
|
||||
else if (m_current->entryKind == TypeEntry::Composite && entry.entryKind == TypeEntry::Composite)
|
||||
isCurrent = (entry.structId == m_current->structId);
|
||||
if (isCurrent) {
|
||||
painter->setPen(t.syntaxType);
|
||||
painter->setPen(t.text);
|
||||
painter->setFont(m_font);
|
||||
painter->drawText(QRect(x, y, 18, h), Qt::AlignCenter,
|
||||
painter->drawText(QRect(x, y, gutterW, h), Qt::AlignCenter,
|
||||
QString(QChar(0x25B8)));
|
||||
}
|
||||
}
|
||||
x += 18;
|
||||
x += gutterW;
|
||||
|
||||
// Icon 16x16 — only for composite entries
|
||||
// Icon (scaled to font height) — only for composite entries
|
||||
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
|
||||
&& (*m_filtered)[row].entryKind == TypeEntry::Composite);
|
||||
if (hasIcon) {
|
||||
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg"));
|
||||
QPixmap pm = structIcon.pixmap(16, 16);
|
||||
QPixmap pm = structIcon.pixmap(iconSz, iconSz);
|
||||
if (isDisabled) {
|
||||
// Paint dimmed
|
||||
QPixmap dimmed(pm.size());
|
||||
@@ -152,12 +159,12 @@ public:
|
||||
p.setOpacity(0.35);
|
||||
p.drawPixmap(0, 0, pm);
|
||||
p.end();
|
||||
painter->drawPixmap(x, y + (h - 16) / 2, dimmed);
|
||||
painter->drawPixmap(x, y + (h - iconSz) / 2, dimmed);
|
||||
} else {
|
||||
structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16);
|
||||
structIcon.paint(painter, x, y + (h - iconSz) / 2, iconSz, iconSz);
|
||||
}
|
||||
}
|
||||
x += 20;
|
||||
x += iconColW;
|
||||
|
||||
// Text
|
||||
QColor textColor;
|
||||
@@ -272,14 +279,14 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
|
||||
// Separator
|
||||
{
|
||||
auto* sep = new QFrame;
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setFrameShadow(QFrame::Plain);
|
||||
m_separator = new QFrame;
|
||||
m_separator->setFrameShape(QFrame::HLine);
|
||||
m_separator->setFrameShadow(QFrame::Plain);
|
||||
QPalette sepPal = pal;
|
||||
sepPal.setColor(QPalette::WindowText, theme.border);
|
||||
sep->setPalette(sepPal);
|
||||
sep->setFixedHeight(1);
|
||||
layout->addWidget(sep);
|
||||
m_separator->setPalette(sepPal);
|
||||
m_separator->setFixedHeight(1);
|
||||
layout->addWidget(m_separator);
|
||||
}
|
||||
|
||||
// Row 3: Modifier toggles [ plain ] [ * ] [ ** ] [ [n] ]
|
||||
@@ -333,8 +340,14 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
this, [this](int id, bool checked) {
|
||||
if (!checked) return;
|
||||
m_arrayCountEdit->setVisible(id == 3);
|
||||
if (id == 3) m_arrayCountEdit->setFocus();
|
||||
if (id == 3) {
|
||||
if (m_arrayCountEdit->text().trimmed().isEmpty())
|
||||
m_arrayCountEdit->setText(QStringLiteral("1"));
|
||||
m_arrayCountEdit->setFocus();
|
||||
m_arrayCountEdit->selectAll();
|
||||
}
|
||||
updateModifierPreview();
|
||||
applyFilter(m_filterEdit->text());
|
||||
});
|
||||
connect(m_arrayCountEdit, &QLineEdit::textChanged,
|
||||
this, [this]() { updateModifierPreview(); });
|
||||
@@ -368,6 +381,7 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
m_listView->setFrameShape(QFrame::NoFrame);
|
||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_listView->setMouseTracking(true);
|
||||
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
|
||||
m_listView->installEventFilter(this);
|
||||
|
||||
@@ -384,10 +398,33 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::warmUp() {
|
||||
// One-time per-process cost (~170ms): Qt lazily initializes the style/font/DLL
|
||||
// subsystem the first time a popup with complex children is shown. Pre-pay it
|
||||
// by briefly showing a throwaway dummy popup with a QListView, then show+hide
|
||||
// ourselves.
|
||||
{
|
||||
auto* primer = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint);
|
||||
primer->resize(300, 400);
|
||||
auto* lay = new QVBoxLayout(primer);
|
||||
lay->addWidget(new QLabel(QStringLiteral("x")));
|
||||
lay->addWidget(new QLineEdit);
|
||||
auto* model = new QStringListModel(primer);
|
||||
QStringList items; for (int i = 0; i < 10; i++) items << QStringLiteral("x");
|
||||
model->setStringList(items);
|
||||
auto* lv = new QListView;
|
||||
lv->setModel(model);
|
||||
lay->addWidget(lv);
|
||||
primer->show();
|
||||
QApplication::processEvents();
|
||||
primer->hide();
|
||||
QApplication::processEvents();
|
||||
delete primer;
|
||||
}
|
||||
|
||||
TypeEntry dummy;
|
||||
dummy.entryKind = TypeEntry::Primitive;
|
||||
dummy.primitiveKind = NodeKind::Hex8;
|
||||
dummy.displayName = "warmup";
|
||||
dummy.displayName = QStringLiteral("warmup");
|
||||
setTypes({dummy});
|
||||
popup(QPoint(-9999, -9999));
|
||||
hide();
|
||||
@@ -419,6 +456,60 @@ void TypeSelectorPopup::setFont(const QFont& font) {
|
||||
delegate->setFont(font);
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::applyTheme(const Theme& theme) {
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
pal.setColor(QPalette::Base, theme.background);
|
||||
pal.setColor(QPalette::AlternateBase, theme.surface);
|
||||
pal.setColor(QPalette::Text, theme.text);
|
||||
pal.setColor(QPalette::Button, theme.button);
|
||||
pal.setColor(QPalette::ButtonText, theme.text);
|
||||
pal.setColor(QPalette::Highlight, theme.hover);
|
||||
pal.setColor(QPalette::HighlightedText, theme.text);
|
||||
setPalette(pal);
|
||||
|
||||
m_titleLabel->setPalette(pal);
|
||||
m_filterEdit->setPalette(pal);
|
||||
m_listView->setPalette(pal);
|
||||
m_previewLabel->setPalette(pal);
|
||||
m_arrayCountEdit->setPalette(pal);
|
||||
|
||||
// Separator
|
||||
QPalette sepPal = pal;
|
||||
sepPal.setColor(QPalette::WindowText, theme.border);
|
||||
m_separator->setPalette(sepPal);
|
||||
|
||||
// Esc button
|
||||
m_escLabel->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { color: %1; border: none; padding: 2px 6px; }"
|
||||
"QToolButton:hover { color: %2; }")
|
||||
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
|
||||
|
||||
// Create button
|
||||
m_createBtn->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { color: %1; border: none; padding: 3px 6px; }"
|
||||
"QToolButton:hover { color: %2; background: %3; }")
|
||||
.arg(theme.textMuted.name(), theme.text.name(), theme.hover.name()));
|
||||
|
||||
// Modifier toggle buttons
|
||||
QString btnStyle = QStringLiteral(
|
||||
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
|
||||
" padding: 2px 8px; border-radius: 3px; }"
|
||||
"QToolButton:checked { color: %4; background: %5; border-color: %5; }"
|
||||
"QToolButton:hover:!checked { background: %6; }")
|
||||
.arg(theme.textDim.name(), theme.background.name(), theme.border.name(),
|
||||
theme.text.name(), theme.selected.name(), theme.hover.name());
|
||||
m_btnPlain->setStyleSheet(btnStyle);
|
||||
m_btnPtr->setStyleSheet(btnStyle);
|
||||
m_btnDblPtr->setStyleSheet(btnStyle);
|
||||
m_btnArray->setStyleSheet(btnStyle);
|
||||
|
||||
// Preview label
|
||||
m_previewLabel->setStyleSheet(QStringLiteral(
|
||||
"QLabel { color: %1; padding: 1px 6px; }").arg(theme.syntaxType.name()));
|
||||
}
|
||||
|
||||
void TypeSelectorPopup::setTitle(const QString& title) {
|
||||
m_titleLabel->setText(title);
|
||||
}
|
||||
@@ -467,7 +558,9 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
||||
QString text = t.classKeyword.isEmpty()
|
||||
? t.displayName
|
||||
: (t.classKeyword + QStringLiteral(" ") + t.displayName);
|
||||
int w = 18 + 20 + fm.horizontalAdvance(text) + 16;
|
||||
int gutterW = fm.horizontalAdvance(QChar(0x25B8)) + 4;
|
||||
int iconColW = fm.height() + 4;
|
||||
int w = gutterW + iconColW + fm.horizontalAdvance(text) + 16;
|
||||
if (w > maxTextW) maxTextW = w;
|
||||
}
|
||||
int popupW = qBound(280, maxTextW + 24, 500);
|
||||
@@ -537,6 +630,10 @@ 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
|
||||
QVector<TypeEntry> primitives, composites;
|
||||
for (const auto& t : m_allTypes) {
|
||||
@@ -546,9 +643,10 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
|
||||
if (!matchesFilter) continue;
|
||||
|
||||
if (t.entryKind == TypeEntry::Primitive)
|
||||
primitives.append(t);
|
||||
else if (t.entryKind == TypeEntry::Composite)
|
||||
if (t.entryKind == TypeEntry::Primitive) {
|
||||
if (!hideprimitives)
|
||||
primitives.append(t);
|
||||
} else if (t.entryKind == TypeEntry::Composite)
|
||||
composites.append(t);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ class QWidget;
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct Theme;
|
||||
|
||||
// ── Popup mode ──
|
||||
|
||||
enum class TypePopupMode { Root, FieldType, ArrayElement, PointerTarget };
|
||||
@@ -53,6 +55,7 @@ public:
|
||||
void setFont(const QFont& font);
|
||||
void setTitle(const QString& title);
|
||||
void setMode(TypePopupMode mode);
|
||||
void applyTheme(const Theme& theme);
|
||||
void setCurrentNodeSize(int bytes);
|
||||
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
|
||||
void popup(const QPoint& globalPos);
|
||||
@@ -77,6 +80,7 @@ private:
|
||||
QLabel* m_previewLabel = nullptr;
|
||||
QListView* m_listView = nullptr;
|
||||
QStringListModel* m_model = nullptr;
|
||||
QFrame* m_separator = nullptr;
|
||||
|
||||
// Modifier toggles
|
||||
QWidget* m_modRow = nullptr;
|
||||
|
||||
@@ -1,62 +1,76 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include <QIcon>
|
||||
#include <QStandardItemModel>
|
||||
#include <QStandardItem>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Recursively add children of parentId as tree items under parentItem.
|
||||
inline void addWorkspaceChildren(QStandardItem* parentItem,
|
||||
const NodeTree& tree,
|
||||
uint64_t parentId,
|
||||
void* subPtr) {
|
||||
QVector<int> children = tree.childrenOf(parentId);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
struct TabInfo {
|
||||
const NodeTree* tree;
|
||||
QString name;
|
||||
void* subPtr; // QMdiSubWindow* as void*
|
||||
};
|
||||
|
||||
for (int idx : children) {
|
||||
const Node& node = tree.nodes[idx];
|
||||
// Sentinel value stored in UserRole+1 to mark the Project group node.
|
||||
static constexpr uint64_t kGroupSentinel = ~uint64_t(0);
|
||||
|
||||
// Skip hex preview nodes — they are padding/filler, not meaningful fields
|
||||
if (isHexNode(node.kind)) continue;
|
||||
|
||||
QString display;
|
||||
if (node.kind == NodeKind::Struct) {
|
||||
QString typeName = node.structTypeName.isEmpty()
|
||||
? node.name : node.structTypeName;
|
||||
display = QStringLiteral("%1 (%2)")
|
||||
.arg(typeName, node.resolvedClassKeyword());
|
||||
} else {
|
||||
display = QStringLiteral("%1 (%2)")
|
||||
.arg(node.name, QString::fromLatin1(kindToString(node.kind)));
|
||||
}
|
||||
|
||||
auto* item = new QStandardItem(display);
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
if (node.kind == NodeKind::Struct)
|
||||
item->setData(QVariant::fromValue(node.id), Qt::UserRole + 1);
|
||||
item->setData(QVariant::fromValue(node.id), Qt::UserRole + 2); // nodeId for scroll
|
||||
|
||||
if (node.kind == NodeKind::Struct)
|
||||
addWorkspaceChildren(item, tree, node.id, subPtr);
|
||||
|
||||
parentItem->appendRow(item);
|
||||
}
|
||||
}
|
||||
|
||||
inline void buildWorkspaceModel(QStandardItemModel* model,
|
||||
const NodeTree& tree,
|
||||
const QString& projectName,
|
||||
void* subPtr = nullptr) {
|
||||
inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
const QVector<TabInfo>& tabs) {
|
||||
model->clear();
|
||||
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||
|
||||
auto* projectItem = new QStandardItem(projectName);
|
||||
projectItem->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
// Single "Project" root with folder icon
|
||||
void* firstSub = tabs.isEmpty() ? nullptr : tabs[0].subPtr;
|
||||
auto* projectItem = new QStandardItem(QIcon(":/vsicons/folder.svg"),
|
||||
QStringLiteral("Project"));
|
||||
projectItem->setData(QVariant::fromValue(firstSub), Qt::UserRole);
|
||||
projectItem->setData(QVariant::fromValue(kGroupSentinel), Qt::UserRole + 1);
|
||||
|
||||
addWorkspaceChildren(projectItem, tree, 0, subPtr);
|
||||
// Collect all top-level structs/enums across all tabs
|
||||
QVector<std::pair<const Node*, void*>> types, enums;
|
||||
for (const auto& tab : tabs) {
|
||||
QVector<int> topLevel = tab.tree->childrenOf(0);
|
||||
for (int idx : topLevel) {
|
||||
const Node& n = tab.tree->nodes[idx];
|
||||
if (n.kind != NodeKind::Struct) continue;
|
||||
if (n.resolvedClassKeyword() == QStringLiteral("enum"))
|
||||
enums.append({&n, tab.subPtr});
|
||||
else
|
||||
types.append({&n, tab.subPtr});
|
||||
}
|
||||
}
|
||||
|
||||
auto nameOf = [](const Node* n) {
|
||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||
};
|
||||
auto cmpName = [&](const std::pair<const Node*, void*>& a,
|
||||
const std::pair<const Node*, void*>& b) {
|
||||
return nameOf(a.first).compare(nameOf(b.first), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
std::sort(types.begin(), types.end(), cmpName);
|
||||
std::sort(enums.begin(), enums.end(), cmpName);
|
||||
|
||||
for (const auto& [n, subPtr] : types) {
|
||||
QString display = QStringLiteral("%1 (%2)")
|
||||
.arg(nameOf(n), n->resolvedClassKeyword());
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(":/vsicons/symbol-structure.svg"), display);
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
||||
projectItem->appendRow(item);
|
||||
}
|
||||
|
||||
for (const auto& [n, subPtr] : enums) {
|
||||
QString display = QStringLiteral("%1 (%2)")
|
||||
.arg(nameOf(n), n->resolvedClassKeyword());
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(":/vsicons/symbol-enum.svg"), display);
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
item->setData(QVariant::fromValue(n->id), Qt::UserRole + 1);
|
||||
projectItem->appendRow(item);
|
||||
}
|
||||
|
||||
model->appendRow(projectItem);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ private slots:
|
||||
QCOMPARE(result.meta[2].lineKind, LineKind::Footer);
|
||||
}
|
||||
|
||||
void testPaddingMarker() {
|
||||
void testHexNodeCompose() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
@@ -100,19 +100,18 @@ private slots:
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node pad;
|
||||
pad.kind = NodeKind::Padding;
|
||||
pad.name = "pad";
|
||||
pad.parentId = rootId;
|
||||
pad.offset = 0;
|
||||
tree.addNode(pad);
|
||||
Node hex;
|
||||
hex.kind = NodeKind::Hex8;
|
||||
hex.name = "pad";
|
||||
hex.parentId = rootId;
|
||||
hex.offset = 0;
|
||||
tree.addNode(hex);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// CommandRow + padding + root footer = 3
|
||||
// CommandRow + hex node + root footer = 3
|
||||
QCOMPARE(result.meta.size(), 3);
|
||||
QVERIFY(result.meta[1].markerMask & (1u << M_PAD));
|
||||
QCOMPARE(result.meta[1].depth, 1);
|
||||
|
||||
// Line 2 is root footer
|
||||
@@ -1018,7 +1017,7 @@ private slots:
|
||||
void testPrimitiveArrayElements() {
|
||||
// Expanded primitive array should synthesize element lines dynamically
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x1000;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
@@ -1921,54 +1920,9 @@ private slots:
|
||||
}
|
||||
}
|
||||
|
||||
void testComputeStructAlignment() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Int32 has alignment 4
|
||||
Node f1;
|
||||
f1.kind = NodeKind::Int32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
QCOMPARE(tree.computeStructAlignment(rootId), 4);
|
||||
|
||||
// Add Hex64 (alignment 8) — max should become 8
|
||||
Node f2;
|
||||
f2.kind = NodeKind::Hex64;
|
||||
f2.name = "b";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 8;
|
||||
tree.addNode(f2);
|
||||
|
||||
QCOMPARE(tree.computeStructAlignment(rootId), 8);
|
||||
}
|
||||
|
||||
void testComputeStructAlignmentEmpty() {
|
||||
NodeTree tree;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Empty";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Empty struct → alignment 1
|
||||
QCOMPARE(tree.computeStructAlignment(rootId), 1);
|
||||
}
|
||||
|
||||
void testCommandRowRootNameSpan() {
|
||||
// Name span should cover the class name in the merged command row
|
||||
QString text = "source\u25BE \u00B7 0x0 \u00B7 struct\u25BE MyClass {";
|
||||
QString text = "source\u25BE \u00B7 0x0 \u00B7 struct MyClass {";
|
||||
ColumnSpan nameSpan = commandRowRootNameSpan(text);
|
||||
QVERIFY(nameSpan.valid);
|
||||
|
||||
@@ -1980,7 +1934,7 @@ private slots:
|
||||
void testTextIsNonEmpty() {
|
||||
// Verify composed text is actually generated (not empty)
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x1000;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
using namespace rcx;
|
||||
|
||||
static void buildTree(NodeTree& tree) {
|
||||
tree.baseAddress = 0x1000;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
@@ -394,6 +394,65 @@ private slots:
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(countNodes(), before);
|
||||
}
|
||||
|
||||
// ── Change to Ptr* creates class and sets refId ──
|
||||
|
||||
void testChangeToPtrStarCreatesClassAndSetsRef() {
|
||||
// Add a Hex64 node to the root struct
|
||||
uint64_t rootId = m_doc->tree.nodes[0].id;
|
||||
m_ctrl->insertNode(rootId, 16, NodeKind::Hex64, "ptrField");
|
||||
QApplication::processEvents();
|
||||
|
||||
int ptrIdx = findNode("ptrField");
|
||||
QVERIFY(ptrIdx >= 0);
|
||||
uint64_t ptrNodeId = m_doc->tree.nodes[ptrIdx].id;
|
||||
int before = countNodes();
|
||||
|
||||
// Convert to typed pointer
|
||||
m_ctrl->convertToTypedPointer(ptrNodeId);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Re-find after tree mutation
|
||||
ptrIdx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; }
|
||||
}
|
||||
QVERIFY(ptrIdx >= 0);
|
||||
|
||||
// Verify: node kind changed to Pointer64
|
||||
QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Pointer64);
|
||||
|
||||
// Verify: node.refId != 0
|
||||
uint64_t refId = m_doc->tree.nodes[ptrIdx].refId;
|
||||
QVERIFY(refId != 0);
|
||||
|
||||
// Verify: a new Struct node exists with the refId as its id
|
||||
int structIdx = m_doc->tree.indexOfId(refId);
|
||||
QVERIFY(structIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[structIdx].kind, NodeKind::Struct);
|
||||
|
||||
// Verify: the new struct has children (Hex64 fields)
|
||||
auto children = m_doc->tree.childrenOf(refId);
|
||||
QVERIFY(children.size() == 16);
|
||||
for (int ci : children)
|
||||
QCOMPARE(m_doc->tree.nodes[ci].kind, NodeKind::Hex64);
|
||||
|
||||
// Verify: total nodes increased by 1 struct + 16 children = 17
|
||||
QCOMPARE(countNodes(), before + 17);
|
||||
|
||||
// Verify: undo restores the original Hex64 kind and refId==0
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
|
||||
ptrIdx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; }
|
||||
}
|
||||
QVERIFY(ptrIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Hex64);
|
||||
QCOMPARE(m_doc->tree.nodes[ptrIdx].refId, (uint64_t)0);
|
||||
QCOMPARE(countNodes(), before);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestContextMenu)
|
||||
|
||||
@@ -8,10 +8,29 @@
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
// Provider with a configurable base address (for testing source-switch logic)
|
||||
class BaseAwareProvider : public Provider {
|
||||
QByteArray m_data;
|
||||
uint64_t m_base;
|
||||
public:
|
||||
BaseAwareProvider(QByteArray data, uint64_t base)
|
||||
: m_data(std::move(data)), m_base(base) {}
|
||||
bool read(uint64_t addr, void* buf, int len) const override {
|
||||
if (addr + len > (uint64_t)m_data.size()) return false;
|
||||
std::memcpy(buf, m_data.constData() + addr, len);
|
||||
return true;
|
||||
}
|
||||
int size() const override { return m_data.size(); }
|
||||
uint64_t base() const override { return m_base; }
|
||||
bool isLive() const override { return true; }
|
||||
QString name() const override { return QStringLiteral("test"); }
|
||||
QString kind() const override { return QStringLiteral("Process"); }
|
||||
};
|
||||
|
||||
// Small tree: one root struct with a few typed fields at known offsets.
|
||||
// Keeps tests fast and deterministic (no giant PEB tree).
|
||||
static void buildSmallTree(NodeTree& tree) {
|
||||
tree.baseAddress = 0x1000;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
@@ -34,9 +53,8 @@ static void buildSmallTree(NodeTree& tree) {
|
||||
field(0, NodeKind::UInt32, "field_u32"); // 4 bytes
|
||||
field(4, NodeKind::Float, "field_float"); // 4 bytes
|
||||
field(8, NodeKind::UInt8, "field_u8"); // 1 byte
|
||||
field(9, NodeKind::Padding, "pad0"); // 3 bytes padding
|
||||
// Set padding arrayLen = 3 for 3-byte padding
|
||||
tree.nodes.last().arrayLen = 3;
|
||||
field(9, NodeKind::Hex16, "pad0"); // 2 bytes
|
||||
field(11, NodeKind::Hex8, "pad1"); // 1 byte
|
||||
field(12, NodeKind::Hex32, "field_hex"); // 4 bytes
|
||||
}
|
||||
|
||||
@@ -282,47 +300,6 @@ private slots:
|
||||
QVERIFY(newIdx >= 0);
|
||||
}
|
||||
|
||||
// ── Test: Padding value edit is effectively blocked at controller level ──
|
||||
void testPaddingValueEditIsBlocked() {
|
||||
// Find the padding node
|
||||
int padIdx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].kind == NodeKind::Padding) { padIdx = i; break; }
|
||||
}
|
||||
QVERIFY(padIdx >= 0);
|
||||
uint64_t addr = m_doc->tree.computeOffset(padIdx);
|
||||
|
||||
// Read original data at padding offset
|
||||
int padSize = m_doc->tree.nodes[padIdx].byteSize();
|
||||
QByteArray origData = m_doc->provider->readBytes(addr, padSize);
|
||||
|
||||
// The context menu blocks Padding editing, so the controller's setNodeValue
|
||||
// would only be called if the editing UI somehow allows it. But let's verify
|
||||
// the editor correctly blocks it.
|
||||
// Find padding line in composed output
|
||||
ComposeResult result = m_doc->compose();
|
||||
int paddingLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeKind == NodeKind::Padding &&
|
||||
result.meta[i].lineKind == LineKind::Field) {
|
||||
paddingLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(paddingLine >= 0);
|
||||
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// beginInlineEdit(Value) on Padding line must be rejected
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine));
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
|
||||
// Data must be unchanged
|
||||
QByteArray afterData = m_doc->provider->readBytes(addr, padSize);
|
||||
QCOMPARE(afterData, origData);
|
||||
}
|
||||
|
||||
// ── Test: setNodeValue with Hex32 (space-separated hex bytes) ──
|
||||
void testSetNodeValueHex() {
|
||||
int idx = -1;
|
||||
@@ -425,6 +402,44 @@ private slots:
|
||||
QCOMPARE((uint8_t)bytes[0], (uint8_t)0xFF);
|
||||
}
|
||||
|
||||
// ── Test: source switch preserves existing base address ──
|
||||
void testSourceSwitchPreservesBase() {
|
||||
// Set a non-zero baseAddress to simulate a loaded .rcx file
|
||||
m_doc->tree.baseAddress = 0x1000;
|
||||
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
|
||||
|
||||
// Simulate attaching a new provider whose base differs (e.g. 0x400000)
|
||||
auto prov = std::make_shared<BaseAwareProvider>(makeSmallBuffer(), 0x400000);
|
||||
uint64_t newBase = prov->base();
|
||||
QCOMPARE(newBase, (uint64_t)0x400000);
|
||||
|
||||
m_doc->provider = prov;
|
||||
// Controller logic: keep existing baseAddress when non-zero
|
||||
if (m_doc->tree.baseAddress == 0)
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
|
||||
// baseAddress must stay at the original value
|
||||
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
|
||||
// provider base is unchanged (no setBase sync) — provider reports its own initial base
|
||||
QCOMPARE(m_doc->provider->base(), (uint64_t)0x400000);
|
||||
}
|
||||
|
||||
// ── Test: source switch on fresh doc uses provider default ──
|
||||
void testSourceSwitchFreshDocUsesProviderBase() {
|
||||
// Simulate a fresh document (no loaded .rcx → baseAddress == 0)
|
||||
m_doc->tree.baseAddress = 0;
|
||||
|
||||
auto prov = std::make_shared<BaseAwareProvider>(makeSmallBuffer(), 0x7FFE0000);
|
||||
uint64_t newBase = prov->base();
|
||||
|
||||
m_doc->provider = prov;
|
||||
if (m_doc->tree.baseAddress == 0)
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
|
||||
// Fresh doc should adopt the provider's default base
|
||||
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000);
|
||||
}
|
||||
|
||||
// ── Test: toggleCollapse + undo ──
|
||||
void testToggleCollapse() {
|
||||
// Root is index 0, a Struct node
|
||||
@@ -448,6 +463,211 @@ private slots:
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_doc->tree.nodes[0].collapsed, false);
|
||||
}
|
||||
// ── Test: value history popup only appears during inline editing ──
|
||||
void testValueHistoryPopupOnlyDuringEdit() {
|
||||
// Record value history for field_u32 so it has heat
|
||||
auto& tree = m_doc->tree;
|
||||
int idx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == "field_u32") { idx = i; break; }
|
||||
}
|
||||
QVERIFY(idx >= 0);
|
||||
uint64_t nodeId = tree.nodes[idx].id;
|
||||
|
||||
QHash<uint64_t, ValueHistory> history;
|
||||
history[nodeId].record("100");
|
||||
history[nodeId].record("200");
|
||||
history[nodeId].record("300");
|
||||
QVERIFY(history[nodeId].uniqueCount() > 1);
|
||||
|
||||
m_editor->setValueHistoryRef(&history);
|
||||
|
||||
// Refresh and compose so editor has meta with heatLevel
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
ComposeResult result = m_doc->compose();
|
||||
// Manually set heat on the node's line meta
|
||||
for (auto& lm : result.meta) {
|
||||
if (lm.nodeId == nodeId) lm.heatLevel = 2;
|
||||
}
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Popup should not exist or not be visible (no editing active)
|
||||
auto* popup = m_editor->findChild<QWidget*>(QString(), Qt::FindDirectChildrenOnly);
|
||||
// Even if popup widget exists, it should not be visible
|
||||
bool popupVisible = false;
|
||||
for (auto* child : m_editor->findChildren<QFrame*>(QString(), Qt::FindDirectChildrenOnly)) {
|
||||
if (child->isVisible() && child->windowFlags() & Qt::ToolTip)
|
||||
popupVisible = true;
|
||||
}
|
||||
QVERIFY2(!popupVisible, "Popup should not be visible when not editing");
|
||||
|
||||
// Start inline edit on value column of field_u32
|
||||
int fieldLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeId == nodeId && result.meta[i].lineKind == LineKind::Field) {
|
||||
fieldLine = i; break;
|
||||
}
|
||||
}
|
||||
QVERIFY(fieldLine >= 0);
|
||||
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Value, fieldLine);
|
||||
QVERIFY(ok);
|
||||
QVERIFY(m_editor->isEditing());
|
||||
|
||||
// Trigger hover cursor update (simulates mouse move during editing)
|
||||
QApplication::processEvents();
|
||||
|
||||
// Cancel edit to clean up
|
||||
m_editor->cancelInlineEdit();
|
||||
QApplication::processEvents();
|
||||
|
||||
m_editor->setValueHistoryRef(nullptr);
|
||||
}
|
||||
|
||||
// ── Test: delete node clears value history for shifted siblings ──
|
||||
void testDeleteClearsHeatForShiftedNodes() {
|
||||
// Replace with a live provider so refresh() actually records values
|
||||
m_doc->provider = std::make_unique<BaseAwareProvider>(makeSmallBuffer(), 0x1000);
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
auto& tree = m_doc->tree;
|
||||
|
||||
// Locate field_u32 (the node we'll delete) and the siblings after it.
|
||||
// The small tree has: field_u32(0), field_float(4), field_u8(8),
|
||||
// pad0/Hex16(9), pad1/Hex8(11), field_hex/Hex32(12)
|
||||
// field_float and field_u8 are regular (non-hex) types.
|
||||
int delIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == "field_u32") { delIdx = i; break; }
|
||||
}
|
||||
QVERIFY(delIdx >= 0);
|
||||
uint64_t delId = tree.nodes[delIdx].id;
|
||||
|
||||
// Collect sibling node IDs that come after field_u32 (will be shifted)
|
||||
uint64_t parentId = tree.nodes[delIdx].parentId;
|
||||
int deletedSize = tree.nodes[delIdx].byteSize(); // 4 bytes
|
||||
int deletedEnd = tree.nodes[delIdx].offset + deletedSize;
|
||||
QVector<uint64_t> shiftedIds;
|
||||
QHash<uint64_t, QString> nameMap; // for debug messages
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].parentId == parentId && i != delIdx
|
||||
&& tree.nodes[i].offset >= deletedEnd) {
|
||||
shiftedIds.append(tree.nodes[i].id);
|
||||
nameMap[tree.nodes[i].id] = tree.nodes[i].name;
|
||||
}
|
||||
}
|
||||
QVERIFY2(!shiftedIds.isEmpty(), "Should have siblings after field_u32");
|
||||
|
||||
// Seed value history for shifted siblings (simulate accumulated heat)
|
||||
auto& history = const_cast<QHash<uint64_t, ValueHistory>&>(m_ctrl->valueHistory());
|
||||
for (uint64_t id : shiftedIds) {
|
||||
history[id].record("old_val_1");
|
||||
history[id].record("old_val_2");
|
||||
history[id].record("old_val_3");
|
||||
QVERIFY2(history[id].heatLevel() >= 2,
|
||||
qPrintable(QString("Pre-delete: %1 should have heat>=2")
|
||||
.arg(nameMap[id])));
|
||||
}
|
||||
|
||||
// Also seed the to-be-deleted node
|
||||
history[delId].record("del_1");
|
||||
history[delId].record("del_2");
|
||||
QVERIFY(history.contains(delId));
|
||||
|
||||
// Delete field_u32 — this shifts all subsequent siblings
|
||||
m_ctrl->removeNode(delIdx);
|
||||
QApplication::processEvents();
|
||||
|
||||
// The deleted node's history should be gone
|
||||
QVERIFY2(!m_ctrl->valueHistory().contains(delId),
|
||||
"Deleted node's value history should be cleared");
|
||||
|
||||
// All shifted siblings should have heat=0 after the delete.
|
||||
// With a live provider, refresh() inside removeNode re-records one new
|
||||
// value at the new offset → count=1 → heatLevel=0.
|
||||
for (uint64_t id : shiftedIds) {
|
||||
int heat = m_ctrl->valueHistory().contains(id)
|
||||
? m_ctrl->valueHistory()[id].heatLevel() : 0;
|
||||
QVERIFY2(heat == 0,
|
||||
qPrintable(QString("Shifted node '%1' (id=%2) should have heat=0, got %3")
|
||||
.arg(nameMap[id]).arg(id).arg(heat)));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test: value history records and cycles correctly ──
|
||||
void testValueHistoryRingBuffer() {
|
||||
ValueHistory vh;
|
||||
QCOMPARE(vh.count, 0);
|
||||
QCOMPARE(vh.heatLevel(), 0);
|
||||
|
||||
vh.record("10");
|
||||
QCOMPARE(vh.count, 1);
|
||||
QCOMPARE(vh.heatLevel(), 0); // 1 unique = static
|
||||
|
||||
// Duplicate should not increase count
|
||||
vh.record("10");
|
||||
QCOMPARE(vh.count, 1);
|
||||
|
||||
vh.record("20");
|
||||
QCOMPARE(vh.count, 2);
|
||||
QCOMPARE(vh.heatLevel(), 1); // cold
|
||||
|
||||
vh.record("30");
|
||||
QCOMPARE(vh.count, 3);
|
||||
QCOMPARE(vh.heatLevel(), 2); // warm
|
||||
|
||||
vh.record("40");
|
||||
vh.record("50");
|
||||
QCOMPARE(vh.count, 5);
|
||||
QCOMPARE(vh.heatLevel(), 3); // hot
|
||||
|
||||
QCOMPARE(vh.last(), QString("50"));
|
||||
|
||||
// Ring buffer: uniqueCount() caps at kCapacity
|
||||
for (int i = 0; i < 20; i++)
|
||||
vh.record(QString::number(100 + i));
|
||||
QCOMPARE(vh.uniqueCount(), ValueHistory::kCapacity);
|
||||
QVERIFY(vh.count > ValueHistory::kCapacity);
|
||||
|
||||
// forEach iterates oldest→newest within ring
|
||||
QStringList vals;
|
||||
vh.forEach([&](const QString& v) { vals.append(v); });
|
||||
QCOMPARE(vals.size(), ValueHistory::kCapacity);
|
||||
QCOMPARE(vals.last(), vh.last());
|
||||
}
|
||||
// ── Test: inline edit "int32_t[4]" on primitive converts to array ──
|
||||
void testInlineEditPrimitiveArray() {
|
||||
// Find a primitive field to convert
|
||||
int idx = -1;
|
||||
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
|
||||
if (m_doc->tree.nodes[i].name == "field_u32") { idx = i; break; }
|
||||
}
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32);
|
||||
uint64_t nodeId = m_doc->tree.nodes[idx].id;
|
||||
|
||||
// Emit inlineEditCommitted with array syntax
|
||||
emit m_editor->inlineEditCommitted(idx, 0, EditTarget::Type,
|
||||
QStringLiteral("int32_t[4]"));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Node should now be an Array with elementKind=Int32, arrayLen=4
|
||||
int newIdx = m_doc->tree.indexOfId(nodeId);
|
||||
QVERIFY(newIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::Array);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].elementKind, NodeKind::Int32);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].arrayLen, 4);
|
||||
|
||||
// Undo should restore to UInt32
|
||||
m_doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
newIdx = m_doc->tree.indexOfId(nodeId);
|
||||
QVERIFY(newIdx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::UInt32);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestController)
|
||||
|
||||
@@ -583,6 +583,94 @@ private slots:
|
||||
QCOMPARE(norm.size(), 1);
|
||||
QVERIFY(norm.contains(rootId));
|
||||
}
|
||||
|
||||
// ── ValueHistory tests ──
|
||||
|
||||
void testValueHistory_empty() {
|
||||
rcx::ValueHistory h;
|
||||
QCOMPARE(h.heatLevel(), 0);
|
||||
QCOMPARE(h.uniqueCount(), 0);
|
||||
QCOMPARE(h.last(), QString());
|
||||
}
|
||||
|
||||
void testValueHistory_singleValue() {
|
||||
rcx::ValueHistory h;
|
||||
h.record("42");
|
||||
QCOMPARE(h.heatLevel(), 0); // only 1 unique → static
|
||||
QCOMPARE(h.uniqueCount(), 1);
|
||||
QCOMPARE(h.last(), QString("42"));
|
||||
}
|
||||
|
||||
void testValueHistory_duplicateIgnored() {
|
||||
rcx::ValueHistory h;
|
||||
h.record("42");
|
||||
h.record("42");
|
||||
h.record("42");
|
||||
QCOMPARE(h.count, 1);
|
||||
QCOMPARE(h.heatLevel(), 0);
|
||||
}
|
||||
|
||||
void testValueHistory_heatLevels() {
|
||||
rcx::ValueHistory h;
|
||||
h.record("a");
|
||||
QCOMPARE(h.heatLevel(), 0); // 1 unique
|
||||
|
||||
h.record("b");
|
||||
QCOMPARE(h.heatLevel(), 1); // 2 unique → cold
|
||||
|
||||
h.record("c");
|
||||
QCOMPARE(h.heatLevel(), 2); // 3 unique → warm
|
||||
|
||||
h.record("d");
|
||||
QCOMPARE(h.heatLevel(), 2); // 4 unique → warm
|
||||
|
||||
h.record("e");
|
||||
QCOMPARE(h.heatLevel(), 3); // 5 unique → hot
|
||||
}
|
||||
|
||||
void testValueHistory_ringWrap() {
|
||||
rcx::ValueHistory h;
|
||||
// Fill beyond capacity
|
||||
for (int i = 0; i < 15; i++)
|
||||
h.record(QString::number(i));
|
||||
|
||||
QCOMPARE(h.count, 15);
|
||||
QCOMPARE(h.uniqueCount(), 10); // capped at kCapacity
|
||||
QCOMPARE(h.heatLevel(), 3); // hot
|
||||
QCOMPARE(h.last(), QString("14"));
|
||||
|
||||
// Verify oldest values were pushed out, newest 10 remain
|
||||
QStringList collected;
|
||||
h.forEach([&](const QString& v) { collected.append(v); });
|
||||
QCOMPARE(collected.size(), 10);
|
||||
QCOMPARE(collected.first(), QString("5")); // oldest surviving
|
||||
QCOMPARE(collected.last(), QString("14")); // newest
|
||||
}
|
||||
|
||||
void testValueHistory_forEach() {
|
||||
rcx::ValueHistory h;
|
||||
h.record("x");
|
||||
h.record("y");
|
||||
h.record("z");
|
||||
|
||||
QStringList items;
|
||||
h.forEach([&](const QString& v) { items.append(v); });
|
||||
QCOMPARE(items.size(), 3);
|
||||
QCOMPARE(items[0], QString("x"));
|
||||
QCOMPARE(items[1], QString("y"));
|
||||
QCOMPARE(items[2], QString("z"));
|
||||
}
|
||||
|
||||
void testValueHistory_oscillation() {
|
||||
// Values that oscillate (A → B → A → B) should still count each unique transition
|
||||
rcx::ValueHistory h;
|
||||
h.record("A");
|
||||
h.record("B");
|
||||
h.record("A");
|
||||
h.record("B");
|
||||
QCOMPARE(h.count, 4); // 4 transitions
|
||||
QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range)
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCore)
|
||||
|
||||
465
tests/test_disasm.cpp
Normal file
465
tests/test_disasm.cpp
Normal file
@@ -0,0 +1,465 @@
|
||||
#include <QtTest/QTest>
|
||||
#include "disasm.h"
|
||||
#include "core.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
// Helper: extract mnemonic portion from disassembly output (after "addr ")
|
||||
static QString mnemonic(const QString& line) {
|
||||
int sep = line.indexOf(" ");
|
||||
return sep >= 0 ? line.mid(sep + 2) : line;
|
||||
}
|
||||
|
||||
class TestDisasm : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
// ──────────────────────────────────────────────────
|
||||
// disassemble() unit tests – exact mnemonic match
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
void testDisasm64_pushMov() {
|
||||
QByteArray code("\x55\x48\x89\xe5", 4);
|
||||
QString result = disassemble(code, 0x401000, 64);
|
||||
QStringList lines = result.split('\n');
|
||||
QCOMPARE(lines.size(), 2);
|
||||
QVERIFY(lines[0].startsWith("0000000000401000"));
|
||||
QVERIFY(lines[1].startsWith("0000000000401001"));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
|
||||
}
|
||||
|
||||
void testDisasm64_ret() { QCOMPARE(mnemonic(disassemble(QByteArray("\xc3",1), 0x7FF000, 64)), QStringLiteral("ret")); }
|
||||
void testDisasm64_nop() { QCOMPARE(mnemonic(disassemble(QByteArray("\x90",1), 0, 64)), QStringLiteral("nop")); }
|
||||
void testDisasm64_xorEax() { QCOMPARE(mnemonic(disassemble(QByteArray("\x31\xc0",2), 0, 64)), QStringLiteral("xor eax, eax")); }
|
||||
void testDisasm64_subRsp() { QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x83\xec\x20",4), 0, 64)), QStringLiteral("sub rsp, 0x20")); }
|
||||
void testDisasm64_int3() { QCOMPARE(mnemonic(disassemble(QByteArray("\xcc",1), 0, 64)), QStringLiteral("int3")); }
|
||||
void testDisasm64_pushRdi() { QCOMPARE(mnemonic(disassemble(QByteArray("\x57",1), 0, 64)), QStringLiteral("push rdi")); }
|
||||
void testDisasm64_popRsi() { QCOMPARE(mnemonic(disassemble(QByteArray("\x5e",1), 0, 64)), QStringLiteral("pop rsi")); }
|
||||
void testDisasm64_testEax() { QCOMPARE(mnemonic(disassemble(QByteArray("\x85\xc0",2), 0, 64)), QStringLiteral("test eax, eax")); }
|
||||
|
||||
void testDisasm64_leaRipRel() {
|
||||
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x8d\x05\x10\x00\x00\x00",7), 0x1000, 64)),
|
||||
QStringLiteral("lea rax, [rip+0x10]"));
|
||||
}
|
||||
void testDisasm64_callRel() {
|
||||
// call target = 0x1000 + 5 + 0x100 = 0x1105
|
||||
QCOMPARE(mnemonic(disassemble(QByteArray("\xe8\x00\x01\x00\x00",5), 0x1000, 64)),
|
||||
QStringLiteral("call 0x1105"));
|
||||
}
|
||||
void testDisasm64_jmpRel() {
|
||||
// jmp target = 0x1000 + 2 + 0x10 = 0x1012
|
||||
QCOMPARE(mnemonic(disassemble(QByteArray("\xeb\x10",2), 0x1000, 64)),
|
||||
QStringLiteral("jmp 0x1012"));
|
||||
}
|
||||
void testDisasm64_movMemRead() {
|
||||
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x8b\x43\x10",4), 0, 64)),
|
||||
QStringLiteral("mov rax, qword ptr [rbx+0x10]"));
|
||||
}
|
||||
void testDisasm64_movMemWrite() {
|
||||
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x89\x4c\x24\x08",5), 0, 64)),
|
||||
QStringLiteral("mov qword ptr [rsp+0x8], rcx"));
|
||||
}
|
||||
|
||||
void testDisasm64_functionPrologue() {
|
||||
QByteArray code("\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
|
||||
QStringList lines = disassemble(code, 0x140001000ULL, 64).split('\n');
|
||||
QCOMPARE(lines.size(), 4);
|
||||
QVERIFY(lines[0].startsWith("0000000140001000"));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
|
||||
QCOMPARE(mnemonic(lines[2]), QStringLiteral("sub rsp, 0x20"));
|
||||
QCOMPARE(mnemonic(lines[3]), QStringLiteral("ret"));
|
||||
}
|
||||
|
||||
void testDisasm64_multipleNops() {
|
||||
QStringList lines = disassemble(QByteArray(5,'\x90'), 0x1000, 64).split('\n');
|
||||
QCOMPARE(lines.size(), 5);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
QCOMPARE(mnemonic(lines[i]), QStringLiteral("nop"));
|
||||
QVERIFY(lines[i].startsWith(QStringLiteral("%1").arg(0x1000+i, 16, 16, QLatin1Char('0'))));
|
||||
}
|
||||
}
|
||||
|
||||
void testDisasm32_pushMov() {
|
||||
QByteArray code("\x55\x89\xe5", 3);
|
||||
QStringList lines = disassemble(code, 0x401000, 32).split('\n');
|
||||
QCOMPARE(lines.size(), 2);
|
||||
QVERIFY(lines[0].startsWith("00401000"));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push ebp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov ebp, esp"));
|
||||
}
|
||||
|
||||
void testDisasm_empty() { QVERIFY(disassemble({}, 0, 64).isEmpty()); QVERIFY(disassemble({}, 0, 32).isEmpty()); }
|
||||
void testDisasm_invalidBitness() { QVERIFY(disassemble(QByteArray("\x90",1), 0, 16).isEmpty()); }
|
||||
void testDisasm_maxBytes() { QCOMPARE(disassemble(QByteArray(200,'\x90'), 0, 64, 128).count('\n') + 1, 128); }
|
||||
void testDisasm64_addrWidth() { QCOMPARE(disassemble(QByteArray("\x90",1), 0, 64).indexOf(" "), 16); }
|
||||
void testDisasm32_addrWidth() { QCOMPARE(disassemble(QByteArray("\x90",1), 0, 32).indexOf(" "), 8); }
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// hexDump() unit tests
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
void testHexDump_basic() {
|
||||
QByteArray data; for (int i=0;i<32;i++) data.append((char)i);
|
||||
QString r = hexDump(data, 0x1000, 128);
|
||||
QCOMPARE(r.count('\n')+1, 2);
|
||||
QVERIFY(r.startsWith("00001000"));
|
||||
}
|
||||
void testHexDump_ascii() {
|
||||
QVERIFY(hexDump(QByteArray("Hello, World!xx",15), 0, 128).contains("Hello"));
|
||||
}
|
||||
void testHexDump_nonPrintable() {
|
||||
QByteArray d(16,'\0'); d[0]='A'; d[15]='Z';
|
||||
QVERIFY(hexDump(d, 0, 128).contains("A..............Z"));
|
||||
}
|
||||
void testHexDump_empty() { QVERIFY(hexDump({}, 0).isEmpty()); }
|
||||
void testHexDump_maxBytes() { QCOMPARE(hexDump(QByteArray(200,'\xAA'), 0, 64).count('\n')+1, 4); }
|
||||
void testHexDump_wideAddr() { QVERIFY(hexDump(QByteArray(16,'\0'), 0x100000000ULL, 128).startsWith("0000000100000000")); }
|
||||
void testHexDump_hexValues() {
|
||||
QByteArray d; d.append('\xDE'); d.append('\xAD'); d.append('\xBE'); d.append('\xEF');
|
||||
while (d.size()<16) d.append('\0');
|
||||
QVERIFY(hexDump(d, 0, 128).contains("de ad be ef", Qt::CaseInsensitive));
|
||||
}
|
||||
void testHexDump_secondLineAddr() {
|
||||
QStringList lines = hexDump(QByteArray(32,'\x42'), 0x2000, 128).split('\n');
|
||||
QCOMPARE(lines.size(), 2);
|
||||
QVERIFY(lines[1].startsWith("00002010"));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// End-to-end: pointer-expanded VTable with FuncPtr64
|
||||
// Verifies we read from the COMPOSED address, not node.offset
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
void testVTableDisasm_composedAddress() {
|
||||
// Memory layout (absolute addresses, baseAddress = 0):
|
||||
//
|
||||
// [0x0000] Root "Obj" struct
|
||||
// +0x00: Pointer64 __vptr => points to 0x100 (vtable)
|
||||
//
|
||||
// [0x0100] VTable (expanded via pointer deref)
|
||||
// +0x00: func ptr 0 => value 0x200 (func0 code)
|
||||
// +0x08: func ptr 1 => value 0x300 (func1 code)
|
||||
//
|
||||
// [0x0200] func0 code: push rbp; ret
|
||||
// [0x0300] func1 code: xor eax, eax; ret
|
||||
//
|
||||
|
||||
// Build a 4KB buffer
|
||||
QByteArray mem(4096, '\0');
|
||||
auto w64 = [&](int off, uint64_t val) {
|
||||
memcpy(mem.data() + off, &val, 8);
|
||||
};
|
||||
|
||||
// Root object at offset 0: __vptr points to vtable at 0x100
|
||||
w64(0x00, 0x100);
|
||||
|
||||
// VTable at offset 0x100: two function pointers
|
||||
w64(0x100, 0x200); // slot 0 -> func0
|
||||
w64(0x108, 0x300); // slot 1 -> func1
|
||||
|
||||
// func0 at offset 0x200: push rbp; ret
|
||||
mem[0x200] = '\x55';
|
||||
mem[0x201] = '\xc3';
|
||||
|
||||
// func1 at offset 0x300: xor eax, eax; ret
|
||||
mem[0x300] = '\x31';
|
||||
mem[0x301] = '\xc0';
|
||||
mem[0x302] = '\xc3';
|
||||
|
||||
BufferProvider prov(mem);
|
||||
|
||||
// Build node tree
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
// Root struct "Obj"
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Obj";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// VTable struct definition (template)
|
||||
Node vtDef;
|
||||
vtDef.kind = NodeKind::Struct;
|
||||
vtDef.name = "VTable";
|
||||
vtDef.parentId = 0;
|
||||
vtDef.offset = 0x1000; // parked far away so it doesn't overlap
|
||||
int vti = tree.addNode(vtDef);
|
||||
uint64_t vtId = tree.nodes[vti].id;
|
||||
|
||||
// Two FuncPtr64 children inside VTable definition
|
||||
Node fp0;
|
||||
fp0.kind = NodeKind::FuncPtr64;
|
||||
fp0.name = "func0";
|
||||
fp0.parentId = vtId;
|
||||
fp0.offset = 0;
|
||||
tree.addNode(fp0);
|
||||
|
||||
Node fp1;
|
||||
fp1.kind = NodeKind::FuncPtr64;
|
||||
fp1.name = "func1";
|
||||
fp1.parentId = vtId;
|
||||
fp1.offset = 8;
|
||||
tree.addNode(fp1);
|
||||
|
||||
// Pointer64 "__vptr" in root, pointing to VTable via refId
|
||||
Node vptr;
|
||||
vptr.kind = NodeKind::Pointer64;
|
||||
vptr.name = "__vptr";
|
||||
vptr.parentId = rootId;
|
||||
vptr.offset = 0;
|
||||
vptr.refId = vtId;
|
||||
tree.addNode(vptr);
|
||||
|
||||
// Compose the tree
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// Find the FuncPtr64 lines in the composed output that are inside the
|
||||
// pointer-expanded VTable (near vtable address), not the standalone definition.
|
||||
struct FuncInfo { int line; uint64_t offsetAddr; NodeKind kind; QString name; };
|
||||
QVector<FuncInfo> funcPtrs;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
const LineMeta& lm = result.meta[i];
|
||||
if (lm.nodeKind == NodeKind::FuncPtr64 && lm.lineKind == LineKind::Field) {
|
||||
// Only include the pointer-expanded ones (near vtable at 0x100)
|
||||
if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
|
||||
int nodeIdx = lm.nodeIdx;
|
||||
funcPtrs.append({i, lm.offsetAddr, lm.nodeKind,
|
||||
nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QCOMPARE(funcPtrs.size(), 2);
|
||||
|
||||
// Verify composed addresses point to the vtable, NOT to the root struct
|
||||
// func0 should be at 0x100 (vtable + 0)
|
||||
QCOMPARE(funcPtrs[0].offsetAddr, (uint64_t)0x100);
|
||||
// func1 should be at 0x108 (vtable + 8)
|
||||
QCOMPARE(funcPtrs[1].offsetAddr, (uint64_t)0x108);
|
||||
|
||||
// Now simulate what the hover code should do:
|
||||
// Read the function pointer VALUE from the correct provider address
|
||||
for (const auto& fp : funcPtrs) {
|
||||
// Provider reads at absolute address directly
|
||||
uint64_t provAddr = fp.offsetAddr;
|
||||
|
||||
// Read the pointer value (the function address)
|
||||
uint64_t ptrVal = prov.readU64(provAddr);
|
||||
|
||||
// Verify we got the right pointer values
|
||||
if (fp.name == "func0") {
|
||||
QCOMPARE(ptrVal, (uint64_t)0x200);
|
||||
} else {
|
||||
QCOMPARE(ptrVal, (uint64_t)0x300);
|
||||
}
|
||||
|
||||
// Read code bytes at the pointer target (absolute address)
|
||||
uint64_t codeProvAddr = ptrVal;
|
||||
QByteArray codeBytes = prov.readBytes(codeProvAddr, 128);
|
||||
|
||||
// Disassemble and verify
|
||||
QString asm_ = disassemble(codeBytes, ptrVal, 64, 128);
|
||||
QVERIFY2(!asm_.isEmpty(), qPrintable("Empty disasm for " + fp.name));
|
||||
|
||||
QStringList lines = asm_.split('\n');
|
||||
if (fp.name == "func0") {
|
||||
// Should decode: push rbp; ret
|
||||
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func0, got %1: %2").arg(lines.size()).arg(asm_)));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
||||
// Verify address in output matches the real function address
|
||||
QVERIFY2(lines[0].contains("200"),
|
||||
qPrintable("func0 addr wrong: " + lines[0]));
|
||||
} else {
|
||||
// Should decode: xor eax, eax; ret
|
||||
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func1, got %1: %2").arg(lines.size()).arg(asm_)));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
||||
QVERIFY2(lines[0].contains("300"),
|
||||
qPrintable("func1 addr wrong: " + lines[0]));
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Verify that reading from node.offset (the WRONG way) gives
|
||||
// different/wrong results. node.offset for func0=0, func1=8, which are
|
||||
// inside the ROOT struct, not the vtable.
|
||||
uint64_t wrongVal0 = prov.readU64(0); // node.offset=0: reads __vptr value
|
||||
uint64_t wrongVal1 = prov.readU64(8); // node.offset=8: reads garbage after __vptr
|
||||
// wrongVal0 = 0x100 (the vptr itself, NOT a function address)
|
||||
QCOMPARE(wrongVal0, (uint64_t)0x100);
|
||||
// This is the vtable address, not a function — disassembling it would be wrong
|
||||
QVERIFY2(wrongVal0 != (uint64_t)0x200,
|
||||
"node.offset reads the vptr, not the function pointer");
|
||||
QVERIFY2(wrongVal1 != (uint64_t)0x300,
|
||||
"node.offset=8 reads past vptr, not the second function pointer");
|
||||
}
|
||||
|
||||
void testVTableDisasm_wrongAddressGivesWrongCode() {
|
||||
// Demonstrate that using node.offset instead of composed address
|
||||
// gives completely wrong disassembly results
|
||||
QByteArray mem(1024, '\0');
|
||||
auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); };
|
||||
|
||||
// Root at 0: vptr -> 0x80
|
||||
w64(0x00, (uint64_t)0x80);
|
||||
// VTable at 0x80: one func ptr -> 0x100
|
||||
w64(0x80, (uint64_t)0x100);
|
||||
// Code at 0x100: sub rsp, 0x28; nop; ret
|
||||
mem[0x100] = '\x48'; mem[0x101] = '\x83'; mem[0x102] = '\xec';
|
||||
mem[0x103] = '\x28'; mem[0x104] = '\x90'; mem[0x105] = '\xc3';
|
||||
|
||||
BufferProvider prov(mem);
|
||||
|
||||
// WRONG: read from node.offset=0 (root's vptr value, not the func ptr)
|
||||
uint64_t wrongPtrVal = prov.readU64(0);
|
||||
QCOMPARE(wrongPtrVal, (uint64_t)0x80); // This is the vtable addr, not a function!
|
||||
|
||||
// RIGHT: read from composed address (vtable + 0)
|
||||
uint64_t rightPtrVal = prov.readU64(0x80);
|
||||
QCOMPARE(rightPtrVal, (uint64_t)0x100); // This IS the function address
|
||||
|
||||
// Disassemble the RIGHT target
|
||||
QByteArray rightCode = prov.readBytes(0x100, 128);
|
||||
QString rightAsm = disassemble(rightCode, 0x100, 64, 128);
|
||||
QStringList rightLines = rightAsm.split('\n');
|
||||
QVERIFY(rightLines.size() >= 3);
|
||||
QCOMPARE(mnemonic(rightLines[0]), QStringLiteral("sub rsp, 0x28"));
|
||||
QCOMPARE(mnemonic(rightLines[1]), QStringLiteral("nop"));
|
||||
QCOMPARE(mnemonic(rightLines[2]), QStringLiteral("ret"));
|
||||
|
||||
// Disassemble the WRONG target (vtable data, not code!)
|
||||
QByteArray wrongCode = prov.readBytes(0x80, 128);
|
||||
QString wrongAsm = disassemble(wrongCode, 0x80, 64, 128);
|
||||
// The wrong bytes are the vtable entries (pointer values),
|
||||
// which decode as garbage instructions, not sub/nop/ret
|
||||
QVERIFY2(!wrongAsm.contains("sub rsp"),
|
||||
qPrintable("Wrong address should NOT produce sub rsp: " + wrongAsm));
|
||||
}
|
||||
|
||||
void testHoverFlow_fullSimulation() {
|
||||
// Full simulation of the hover flow as implemented in editor.cpp:
|
||||
//
|
||||
// 1. Compose the tree to get LineMeta with correct offsetAddr
|
||||
// 2. For each FuncPtr64 line, read pointer value from provider
|
||||
// using lm.offsetAddr (absolute address)
|
||||
// 3. Read code bytes from the REAL provider using ptrVal directly
|
||||
// (the real provider can read any process address; snapshot cannot)
|
||||
// 4. Disassemble the code bytes
|
||||
//
|
||||
// The key distinction: step 2 reads from composed tree addresses (in
|
||||
// the snapshot), step 3 reads from arbitrary code addresses (needs
|
||||
// the real provider, not snapshot).
|
||||
|
||||
QByteArray mem(8192, '\0');
|
||||
auto w64 = [&](int off, uint64_t val) {
|
||||
memcpy(mem.data() + off, &val, 8);
|
||||
};
|
||||
|
||||
// Layout:
|
||||
// [0x000] Root struct: __vptr -> vtable at 0x100
|
||||
// [0x100] VTable: func0 -> 0x1000, func1 -> 0x1800
|
||||
// [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret
|
||||
// [0x1800] func1 code: xor eax, eax; ret
|
||||
w64(0x000, (uint64_t)0x100); // __vptr
|
||||
w64(0x100, (uint64_t)0x1000); // vtable[0]
|
||||
w64(0x108, (uint64_t)0x1800); // vtable[1]
|
||||
// func0 code
|
||||
memcpy(mem.data() + 0x1000, "\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
|
||||
// func1 code
|
||||
memcpy(mem.data() + 0x1800, "\x31\xc0\xc3", 3);
|
||||
|
||||
// This provider represents the real process memory.
|
||||
BufferProvider realProv(mem);
|
||||
|
||||
// Build a snapshot that only contains tree-data pages (like the
|
||||
// async refresh does). The snapshot does NOT contain function code pages.
|
||||
// This simulates the real scenario where SnapshotProvider only has
|
||||
// pages for the root struct and pointer-expanded structs.
|
||||
QByteArray snapData(0x200, '\0'); // only pages for root + vtable
|
||||
memcpy(snapData.data(), mem.constData(), 0x200);
|
||||
BufferProvider snapProv(snapData);
|
||||
|
||||
// Build node tree
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root; root.kind = NodeKind::Struct; root.name = "Obj";
|
||||
root.parentId = 0; root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node vtDef; vtDef.kind = NodeKind::Struct; vtDef.name = "VTable";
|
||||
vtDef.parentId = 0; vtDef.offset = 0x2000;
|
||||
int vti = tree.addNode(vtDef);
|
||||
uint64_t vtId = tree.nodes[vti].id;
|
||||
|
||||
Node fp0; fp0.kind = NodeKind::FuncPtr64; fp0.name = "func0";
|
||||
fp0.parentId = vtId; fp0.offset = 0;
|
||||
tree.addNode(fp0);
|
||||
Node fp1; fp1.kind = NodeKind::FuncPtr64; fp1.name = "func1";
|
||||
fp1.parentId = vtId; fp1.offset = 8;
|
||||
tree.addNode(fp1);
|
||||
|
||||
Node vptr; vptr.kind = NodeKind::Pointer64; vptr.name = "__vptr";
|
||||
vptr.parentId = rootId; vptr.offset = 0; vptr.refId = vtId;
|
||||
tree.addNode(vptr);
|
||||
|
||||
// Compose with the snapshot (like production: compose uses snapshot)
|
||||
ComposeResult result = compose(tree, snapProv);
|
||||
|
||||
// Find expanded FuncPtr64 lines
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
const LineMeta& lm = result.meta[i];
|
||||
if (lm.nodeKind != NodeKind::FuncPtr64 || lm.lineKind != LineKind::Field)
|
||||
continue;
|
||||
if (lm.offsetAddr < 0x100 || lm.offsetAddr >= 0x200)
|
||||
continue; // skip standalone VTable definition entries
|
||||
|
||||
// --- Hover step 1: read pointer value from snapshot ---
|
||||
uint64_t provAddr = lm.offsetAddr;
|
||||
// The snapshot has this data (vtable pages are in it)
|
||||
QVERIFY2(snapProv.isReadable(provAddr, 8),
|
||||
qPrintable(QString("Snapshot should have vtable page at %1")
|
||||
.arg(provAddr, 0, 16)));
|
||||
uint64_t ptrVal = snapProv.readU64(provAddr);
|
||||
QVERIFY2(ptrVal != 0, "Function pointer should not be zero");
|
||||
|
||||
// --- Hover step 2: read code from REAL provider ---
|
||||
// The snapshot does NOT have the code pages:
|
||||
uint64_t codeAddr = ptrVal;
|
||||
QVERIFY2(!snapProv.isReadable(codeAddr, 1),
|
||||
"Snapshot should NOT have function code pages");
|
||||
// But the real provider does:
|
||||
QByteArray codeBytes(128, Qt::Uninitialized);
|
||||
bool readOk = realProv.read(codeAddr, codeBytes.data(), 128);
|
||||
QVERIFY2(readOk, "Real provider should be able to read code bytes");
|
||||
|
||||
// --- Hover step 3: disassemble ---
|
||||
QString asm_ = disassemble(codeBytes, ptrVal, 64, 128);
|
||||
QVERIFY2(!asm_.isEmpty(), qPrintable("Empty disasm for line " + QString::number(i)));
|
||||
|
||||
QStringList lines = asm_.split('\n');
|
||||
const Node& node = tree.nodes[lm.nodeIdx];
|
||||
if (node.name == "func0") {
|
||||
QVERIFY(lines.size() >= 4);
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
|
||||
QCOMPARE(mnemonic(lines[2]), QStringLiteral("sub rsp, 0x20"));
|
||||
QCOMPARE(mnemonic(lines[3]), QStringLiteral("ret"));
|
||||
} else if (node.name == "func1") {
|
||||
QVERIFY(lines.size() >= 2);
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestDisasm)
|
||||
#include "test_disasm.moc"
|
||||
@@ -152,7 +152,7 @@ static BufferProvider makeTestProvider() {
|
||||
// Build the full _PEB64 tree (0x7D0 bytes), unions mapped to first member
|
||||
static NodeTree makeTestTree() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0x000000D87B5E5000ULL;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
// Root struct
|
||||
Node root;
|
||||
@@ -170,9 +170,10 @@ static NodeTree makeTestTree() {
|
||||
n.parentId = rootId; n.offset = off;
|
||||
tree.addNode(n);
|
||||
};
|
||||
auto pad = [&](int off, int len, const char* name) {
|
||||
Node n; n.kind = NodeKind::Padding; n.name = name;
|
||||
n.parentId = rootId; n.offset = off; n.arrayLen = len;
|
||||
auto pad = [&](int off, int /*len*/, const char* name) {
|
||||
// 4-byte padding → Hex32 (all usages in this test pass len=4)
|
||||
Node n; n.kind = NodeKind::Hex32; n.name = name;
|
||||
n.parentId = rootId; n.offset = off;
|
||||
tree.addNode(n);
|
||||
};
|
||||
auto arr = [&](int off, NodeKind ek, int len, const char* name) {
|
||||
@@ -278,8 +279,8 @@ static NodeTree makeTestTree() {
|
||||
|
||||
n.kind = NodeKind::UInt16; n.name = "Length"; n.offset = 0; tree.addNode(n);
|
||||
n.kind = NodeKind::UInt16; n.name = "MaximumLength"; n.offset = 2; tree.addNode(n);
|
||||
n.kind = NodeKind::Padding; n.name = "Pad";
|
||||
n.offset = 4; n.arrayLen = 4; tree.addNode(n);
|
||||
n.kind = NodeKind::Hex32; n.name = "Pad";
|
||||
n.offset = 4; n.arrayLen = 1; tree.addNode(n);
|
||||
n.kind = NodeKind::Pointer64; n.name = "Buffer"; n.offset = 8; n.arrayLen = 1;
|
||||
tree.addNode(n);
|
||||
}
|
||||
@@ -341,6 +342,95 @@ static NodeTree makeTestTree() {
|
||||
return tree;
|
||||
}
|
||||
|
||||
// ── Pointer expansion demo data ──
|
||||
// Small tree with a working pointer that points within the buffer.
|
||||
// Root struct "Demo" has a UInt32 "id" and Pointer64 "pChild" → ChildData.
|
||||
// ChildData has UInt32 "x", UInt32 "y", Float "z".
|
||||
struct PtrDemo {
|
||||
NodeTree tree;
|
||||
BufferProvider prov{QByteArray()};
|
||||
uint64_t rootId = 0;
|
||||
uint64_t childStructId = 0;
|
||||
};
|
||||
|
||||
static PtrDemo makePtrDemo(bool collapsed = false, bool nullPtr = false) {
|
||||
PtrDemo d;
|
||||
d.tree.baseAddress = 0;
|
||||
|
||||
// Root struct
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.structTypeName = "Demo";
|
||||
root.name = "demo";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = d.tree.addNode(root);
|
||||
d.rootId = d.tree.nodes[ri].id;
|
||||
|
||||
// id field at offset 0
|
||||
{
|
||||
Node n;
|
||||
n.kind = NodeKind::UInt32;
|
||||
n.name = "id";
|
||||
n.parentId = d.rootId;
|
||||
n.offset = 0;
|
||||
d.tree.addNode(n);
|
||||
}
|
||||
|
||||
// ChildData struct definition (separate root)
|
||||
Node child;
|
||||
child.kind = NodeKind::Struct;
|
||||
child.structTypeName = "ChildData";
|
||||
child.name = "ChildData";
|
||||
child.parentId = 0;
|
||||
child.offset = 200; // standalone rendering offset
|
||||
int ci = d.tree.addNode(child);
|
||||
d.childStructId = d.tree.nodes[ci].id;
|
||||
|
||||
{
|
||||
Node n;
|
||||
n.kind = NodeKind::UInt32; n.name = "x";
|
||||
n.parentId = d.childStructId; n.offset = 0;
|
||||
d.tree.addNode(n);
|
||||
n.kind = NodeKind::UInt32; n.name = "y";
|
||||
n.offset = 4;
|
||||
d.tree.addNode(n);
|
||||
n.kind = NodeKind::Float; n.name = "z";
|
||||
n.offset = 8;
|
||||
d.tree.addNode(n);
|
||||
}
|
||||
|
||||
// Pointer at offset 8 → ChildData
|
||||
{
|
||||
Node ptr;
|
||||
ptr.kind = NodeKind::Pointer64;
|
||||
ptr.name = "pChild";
|
||||
ptr.parentId = d.rootId;
|
||||
ptr.offset = 8;
|
||||
ptr.refId = d.childStructId;
|
||||
ptr.collapsed = collapsed;
|
||||
d.tree.addNode(ptr);
|
||||
}
|
||||
|
||||
// Buffer: 128 bytes
|
||||
QByteArray data(128, '\0');
|
||||
uint32_t idVal = 42;
|
||||
memcpy(data.data() + 0, &idVal, 4);
|
||||
|
||||
if (!nullPtr) {
|
||||
uint64_t ptrVal = 64; // points to offset 64 in buffer
|
||||
memcpy(data.data() + 8, &ptrVal, 8);
|
||||
}
|
||||
|
||||
// Data at the pointer target (offset 64)
|
||||
uint32_t xVal = 100; memcpy(data.data() + 64, &xVal, 4);
|
||||
uint32_t yVal = 200; memcpy(data.data() + 68, &yVal, 4);
|
||||
float zVal = 3.14f; memcpy(data.data() + 72, &zVal, 4);
|
||||
|
||||
d.prov = BufferProvider(data, "ptr_demo");
|
||||
return d;
|
||||
}
|
||||
|
||||
class TestEditor : public QObject {
|
||||
Q_OBJECT
|
||||
private:
|
||||
@@ -751,70 +841,6 @@ private slots:
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: Padding line rejects value editing ──
|
||||
void testPaddingLineRejectsValueEdit() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Find a Padding line in the composed output
|
||||
int paddingLine = -1;
|
||||
for (int i = 0; i < m_result.meta.size(); i++) {
|
||||
if (m_result.meta[i].nodeKind == NodeKind::Padding &&
|
||||
m_result.meta[i].lineKind == LineKind::Field) {
|
||||
paddingLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(paddingLine >= 0, "Should have at least one Padding line in test tree");
|
||||
|
||||
const LineMeta* lm = m_editor->metaForLine(paddingLine);
|
||||
QVERIFY(lm);
|
||||
QCOMPARE(lm->nodeKind, NodeKind::Padding);
|
||||
|
||||
// Value edit on Padding MUST be rejected (the bug fix)
|
||||
QVERIFY2(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine),
|
||||
"Value edit should be rejected on Padding lines");
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
|
||||
// Name edit on Padding SHOULD succeed (ASCII preview column is editable)
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Name, paddingLine);
|
||||
QVERIFY2(ok, "Name edit should be allowed on Padding lines (ASCII preview)");
|
||||
QVERIFY(m_editor->isEditing());
|
||||
m_editor->cancelInlineEdit();
|
||||
|
||||
// Type edit on Padding SHOULD succeed (emits popup signal)
|
||||
QSignalSpy typeSpy(m_editor, &RcxEditor::typePickerRequested);
|
||||
ok = m_editor->beginInlineEdit(EditTarget::Type, paddingLine);
|
||||
QVERIFY2(ok, "Type edit should be allowed on Padding lines");
|
||||
QCOMPARE(typeSpy.count(), 1);
|
||||
}
|
||||
|
||||
// ── Test: resolvedSpanFor rejects Value on Padding (defense-in-depth) ──
|
||||
void testPaddingLineRejectsValueSpan() {
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Find a Padding line
|
||||
int paddingLine = -1;
|
||||
for (int i = 0; i < m_result.meta.size(); i++) {
|
||||
if (m_result.meta[i].nodeKind == NodeKind::Padding &&
|
||||
m_result.meta[i].lineKind == LineKind::Field) {
|
||||
paddingLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(paddingLine >= 0);
|
||||
|
||||
const LineMeta* lm = m_editor->metaForLine(paddingLine);
|
||||
QVERIFY(lm);
|
||||
|
||||
// valueSpanFor returns valid (shared with Hex via KF_HexPreview)
|
||||
ColumnSpan vs = RcxEditor::valueSpan(*lm, 200);
|
||||
QVERIFY2(vs.valid, "valueSpanFor should return valid for Padding (shared HexPreview flag)");
|
||||
|
||||
// But beginInlineEdit should still reject it
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, paddingLine));
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
}
|
||||
|
||||
// ── Test: value edit commit fires signal with typed text ──
|
||||
void testValueEditCommitUpdatesSignal() {
|
||||
m_editor->applyDocument(m_result);
|
||||
@@ -823,8 +849,6 @@ private slots:
|
||||
const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
|
||||
QVERIFY(lm);
|
||||
QCOMPARE(lm->lineKind, LineKind::Field);
|
||||
QVERIFY(lm->nodeKind != NodeKind::Padding);
|
||||
|
||||
// Begin value edit
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine);
|
||||
QVERIFY(ok);
|
||||
@@ -1006,19 +1030,13 @@ private slots:
|
||||
|
||||
// Set CommandRow text with root class (simulates controller.updateCommandRow)
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {"));
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"));
|
||||
|
||||
// RootClassName should be allowed on CommandRow (line 0)
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 0);
|
||||
QVERIFY2(ok, "RootClassName edit should be allowed on CommandRow");
|
||||
QVERIFY(m_editor->isEditing());
|
||||
m_editor->cancelInlineEdit();
|
||||
|
||||
// RootClassType should be allowed on CommandRow (line 0)
|
||||
ok = m_editor->beginInlineEdit(EditTarget::RootClassType, 0);
|
||||
QVERIFY2(ok, "RootClassType edit should be allowed on CommandRow");
|
||||
QVERIFY(m_editor->isEditing());
|
||||
m_editor->cancelInlineEdit();
|
||||
}
|
||||
|
||||
// ── Test: CommandRow root class name editable ──
|
||||
@@ -1027,7 +1045,7 @@ private slots:
|
||||
|
||||
// Set CommandRow with root class
|
||||
m_editor->setCommandRowText(
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {"));
|
||||
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"));
|
||||
|
||||
// Line 0 is CommandRow
|
||||
const LineMeta* lm = m_editor->metaForLine(0);
|
||||
@@ -1064,6 +1082,144 @@ private slots:
|
||||
"Root header should be suppressed from compose output");
|
||||
}
|
||||
|
||||
// ── Test: command row hover indicator survives refresh cycle ──
|
||||
void testCommandRowHoverSurvivesRefresh() {
|
||||
// IND_HOVER_SPAN = 11 (defined in editor.cpp, replicate for test)
|
||||
constexpr int IND_HOVER_SPAN = 11;
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
// Set command row text (simulates controller.updateCommandRow)
|
||||
QString cmdText = QStringLiteral(
|
||||
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
|
||||
m_editor->setCommandRowText(cmdText);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Parse the source span on line 0
|
||||
auto* sci = m_editor->scintilla();
|
||||
int len = (int)sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0);
|
||||
QVERIFY(len > 0);
|
||||
QByteArray buf(len + 1, '\0');
|
||||
sci->SendScintilla(QsciScintillaBase::SCI_GETLINE, (unsigned long)0,
|
||||
(void*)buf.data());
|
||||
QString lineText = QString::fromUtf8(buf.constData(), len);
|
||||
while (lineText.endsWith('\n') || lineText.endsWith('\r'))
|
||||
lineText.chop(1);
|
||||
|
||||
ColumnSpan srcSpan = commandRowSrcSpan(lineText);
|
||||
QVERIFY2(srcSpan.valid, "Source span should be valid on command row");
|
||||
|
||||
// Programmatically move mouse to the source span
|
||||
int hoverCol = srcSpan.start + 1;
|
||||
QPoint hoverPos = colToViewport(sci, 0, hoverCol);
|
||||
sendMouseMove(sci->viewport(), hoverPos);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Verify IND_HOVER_SPAN is set at the hover position
|
||||
long pos = sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
||||
(unsigned long)0, (long)hoverCol);
|
||||
sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT,
|
||||
(unsigned long)IND_HOVER_SPAN);
|
||||
int valBefore = (int)sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_INDICATORVALUEAT,
|
||||
(unsigned long)IND_HOVER_SPAN, pos);
|
||||
QVERIFY2(valBefore != 0,
|
||||
"IND_HOVER_SPAN should be set on source span after hover");
|
||||
|
||||
// Verify cursor is PointingHand (Source target = clickable)
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||
|
||||
// ── Simulate a full refresh cycle (same order as controller.refresh) ──
|
||||
ViewState vs = m_editor->saveViewState();
|
||||
m_editor->applyDocument(m_result);
|
||||
m_editor->restoreViewState(vs);
|
||||
|
||||
// Cursor must NOT have flipped to Arrow during applyDocument
|
||||
// (applyHoverCursor is not called prematurely on composed text)
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||
|
||||
// updateCommandRow() — replaces line 0 text
|
||||
m_editor->setCommandRowText(cmdText);
|
||||
|
||||
// applySelectionOverlays() — must run AFTER updateCommandRow
|
||||
m_editor->applySelectionOverlay(QSet<uint64_t>());
|
||||
QApplication::processEvents();
|
||||
|
||||
// Re-query the position (text was replaced, byte offset may have shifted)
|
||||
long posAfter = sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
||||
(unsigned long)0, (long)hoverCol);
|
||||
int valAfter = (int)sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_INDICATORVALUEAT,
|
||||
(unsigned long)IND_HOVER_SPAN, posAfter);
|
||||
QVERIFY2(valAfter != 0,
|
||||
"IND_HOVER_SPAN must survive refresh on command row "
|
||||
"(hover should not flicker)");
|
||||
|
||||
// Cursor must still be PointingHand after full refresh cycle
|
||||
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: command row hover survives multiple rapid refresh cycles ──
|
||||
void testCommandRowHoverSurvivesRepeatedRefresh() {
|
||||
constexpr int IND_HOVER_SPAN = 11;
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
|
||||
QString cmdText = QStringLiteral(
|
||||
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
|
||||
m_editor->setCommandRowText(cmdText);
|
||||
QApplication::processEvents();
|
||||
|
||||
auto* sci = m_editor->scintilla();
|
||||
int lineLen = (int)sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0);
|
||||
QByteArray buf(lineLen + 1, '\0');
|
||||
sci->SendScintilla(QsciScintillaBase::SCI_GETLINE, (unsigned long)0,
|
||||
(void*)buf.data());
|
||||
QString lineText = QString::fromUtf8(buf.constData(), lineLen);
|
||||
while (lineText.endsWith('\n') || lineText.endsWith('\r'))
|
||||
lineText.chop(1);
|
||||
|
||||
ColumnSpan srcSpan = commandRowSrcSpan(lineText);
|
||||
QVERIFY(srcSpan.valid);
|
||||
int hoverCol = srcSpan.start + 1;
|
||||
|
||||
// Move mouse into position
|
||||
QPoint hoverPos = colToViewport(sci, 0, hoverCol);
|
||||
sendMouseMove(sci->viewport(), hoverPos);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Simulate 5 rapid refresh cycles (like ~660ms timer x5)
|
||||
for (int cycle = 0; cycle < 5; cycle++) {
|
||||
ViewState vs = m_editor->saveViewState();
|
||||
m_editor->applyDocument(m_result);
|
||||
m_editor->restoreViewState(vs);
|
||||
m_editor->setCommandRowText(cmdText);
|
||||
m_editor->applySelectionOverlay(QSet<uint64_t>());
|
||||
|
||||
// Re-send mouse move each cycle (mouse is still there physically)
|
||||
sendMouseMove(sci->viewport(), hoverPos);
|
||||
QApplication::processEvents();
|
||||
|
||||
long pos = sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN,
|
||||
(unsigned long)0, (long)hoverCol);
|
||||
int val = (int)sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_INDICATORVALUEAT,
|
||||
(unsigned long)IND_HOVER_SPAN, pos);
|
||||
QVERIFY2(val != 0,
|
||||
qPrintable(QString(
|
||||
"IND_HOVER_SPAN lost on refresh cycle %1").arg(cycle)));
|
||||
QVERIFY2(viewportCursor(m_editor) == Qt::PointingHandCursor,
|
||||
qPrintable(QString(
|
||||
"Cursor flipped away from PointingHand on cycle %1").arg(cycle)));
|
||||
}
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: MenuBarStyle gives QMenu items generous click targets ──
|
||||
// ── Test: M_ACCENT marker appears on selected rows ──
|
||||
void testAccentMarkerOnSelectedRows() {
|
||||
@@ -1182,6 +1338,157 @@ private slots:
|
||||
.arg(styled.height()).arg(base.height())));
|
||||
}
|
||||
|
||||
// ── Test: non-hex nodes don't show false heat coloring after offset shift ──
|
||||
void testDeleteClearsHeatOnShiftedNodes() {
|
||||
// Heat indicator constants (replicated from editor.cpp)
|
||||
constexpr int IND_HEAT_COLD = 13;
|
||||
constexpr int IND_HEAT_WARM = 17;
|
||||
constexpr int IND_HEAT_HOT = 18;
|
||||
|
||||
// Build a small tree: root struct with mixed regular (non-hex) + hex fields
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.structTypeName = "SmallStruct";
|
||||
root.name = "s";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// field0: UInt32 at offset 0 (4 bytes) — will be deleted
|
||||
// field1: UInt32 at offset 4 (4 bytes) — regular type, will shift
|
||||
// field2: Float at offset 8 (4 bytes) — regular type, will shift
|
||||
// field3: Hex32 at offset 12 (4 bytes) — hex type, will shift
|
||||
struct FieldDef { int off; NodeKind kind; const char* name; };
|
||||
FieldDef defs[] = {
|
||||
{ 0, NodeKind::UInt32, "count"},
|
||||
{ 4, NodeKind::UInt32, "flags"},
|
||||
{ 8, NodeKind::Float, "speed"},
|
||||
{12, NodeKind::Hex32, "raw"},
|
||||
};
|
||||
QVector<uint64_t> fieldIds;
|
||||
for (auto& d : defs) {
|
||||
Node n;
|
||||
n.kind = d.kind;
|
||||
n.name = d.name;
|
||||
n.parentId = rootId;
|
||||
n.offset = d.off;
|
||||
int idx = tree.addNode(n);
|
||||
fieldIds.append(tree.nodes[idx].id);
|
||||
}
|
||||
|
||||
// Create a provider with 16 bytes of recognizable data
|
||||
QByteArray data(16, '\0');
|
||||
uint32_t v0 = 42; memcpy(data.data() + 0, &v0, 4); // count=42
|
||||
uint32_t v1 = 0xFF; memcpy(data.data() + 4, &v1, 4); // flags=255
|
||||
float v2 = 3.14f; memcpy(data.data() + 8, &v2, 4); // speed=3.14
|
||||
uint32_t v3 = 0xCAFE; memcpy(data.data() + 12, &v3, 4); // raw=0xCAFE
|
||||
BufferProvider prov(data);
|
||||
|
||||
// Compose the initial document
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// Inject heatLevel=2 (warm) on field1, field2, field3 — simulates
|
||||
// heat accumulated before the delete
|
||||
for (auto& lm : result.meta) {
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
if (lm.nodeId == fieldIds[i])
|
||||
lm.heatLevel = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to editor — heat indicators should appear
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
auto* sci = m_editor->scintilla();
|
||||
|
||||
// Helper: check if any heat indicator is set anywhere on a line
|
||||
auto hasHeatOnLine = [&](int line) -> bool {
|
||||
int lineLen = (int)sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)line);
|
||||
long lineStart = sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line);
|
||||
for (long pos = lineStart; pos < lineStart + lineLen; pos++) {
|
||||
for (int ind : { IND_HEAT_COLD, IND_HEAT_WARM, IND_HEAT_HOT }) {
|
||||
int val = (int)sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_INDICATORVALUEAT,
|
||||
(unsigned long)ind, pos);
|
||||
if (val != 0) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Find lines for each shifted field
|
||||
auto findFieldLine = [&](const ComposeResult& cr, uint64_t nodeId) -> int {
|
||||
for (int i = 0; i < cr.meta.size(); i++) {
|
||||
if (cr.meta[i].nodeId == nodeId && cr.meta[i].lineKind == LineKind::Field)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
int line1 = findFieldLine(result, fieldIds[1]);
|
||||
int line2 = findFieldLine(result, fieldIds[2]);
|
||||
int line3 = findFieldLine(result, fieldIds[3]);
|
||||
QVERIFY(line1 >= 0);
|
||||
QVERIFY(line2 >= 0);
|
||||
QVERIFY(line3 >= 0);
|
||||
|
||||
// Verify heat indicators ARE present (UInt32, Float, and Hex32)
|
||||
QVERIFY2(hasHeatOnLine(line1),
|
||||
"Heat should be present on UInt32 'flags' before delete");
|
||||
QVERIFY2(hasHeatOnLine(line2),
|
||||
"Heat should be present on Float 'speed' before delete");
|
||||
QVERIFY2(hasHeatOnLine(line3),
|
||||
"Heat should be present on Hex32 'raw' before delete");
|
||||
|
||||
// ── Simulate delete of field0 (UInt32 'count' at offset 0) ──
|
||||
int field0Idx = tree.indexOfId(fieldIds[0]);
|
||||
QVERIFY(field0Idx >= 0);
|
||||
tree.nodes.remove(field0Idx);
|
||||
tree.invalidateIdCache();
|
||||
|
||||
// Shift remaining fields' offsets down by 4
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
int fi = tree.indexOfId(fieldIds[i]);
|
||||
if (fi >= 0) tree.nodes[fi].offset -= 4;
|
||||
}
|
||||
|
||||
// Recompose — heatLevel defaults to 0 (simulates cleared history)
|
||||
ComposeResult afterResult = compose(tree, prov);
|
||||
|
||||
// Apply the post-delete document to the editor
|
||||
m_editor->applyDocument(afterResult);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find new line positions
|
||||
int newLine1 = findFieldLine(afterResult, fieldIds[1]);
|
||||
int newLine2 = findFieldLine(afterResult, fieldIds[2]);
|
||||
int newLine3 = findFieldLine(afterResult, fieldIds[3]);
|
||||
QVERIFY(newLine1 >= 0);
|
||||
QVERIFY(newLine2 >= 0);
|
||||
QVERIFY(newLine3 >= 0);
|
||||
|
||||
// After applying heatLevel=0, NO heat indicators should appear
|
||||
QVERIFY2(!hasHeatOnLine(newLine1),
|
||||
"UInt32 'flags' should NOT show heat after offset shift "
|
||||
"(old values are from wrong address)");
|
||||
QVERIFY2(!hasHeatOnLine(newLine2),
|
||||
"Float 'speed' should NOT show heat after offset shift "
|
||||
"(old values are from wrong address)");
|
||||
QVERIFY2(!hasHeatOnLine(newLine3),
|
||||
"Hex32 'raw' should NOT show heat after offset shift "
|
||||
"(old values are from wrong address)");
|
||||
|
||||
// Restore original document
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
void testMenuHoverRendersAmberText() {
|
||||
// Replicate MenuBarStyle with drawControl hover override
|
||||
class TestMenuStyle : public QProxyStyle {
|
||||
@@ -1304,6 +1611,440 @@ private slots:
|
||||
"found %1 / %2 total (see menu_hover_full.png, menu_hover_item.png)")
|
||||
.arg(amberPixels).arg(totalPixels)));
|
||||
}
|
||||
void testStructPreviewPopupOnCollapsedTypedPointer() {
|
||||
// Build a small tree: root struct with a typed Pointer64 → target struct
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.structTypeName = "TestRoot";
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Target struct with some fields
|
||||
Node target;
|
||||
target.kind = NodeKind::Struct;
|
||||
target.structTypeName = "TargetStruct";
|
||||
target.name = "TargetStruct";
|
||||
target.parentId = 0;
|
||||
target.offset = 0;
|
||||
int ti = tree.addNode(target);
|
||||
uint64_t targetId = tree.nodes[ti].id;
|
||||
|
||||
// Add fields to the target struct
|
||||
{
|
||||
Node f; f.parentId = targetId;
|
||||
f.kind = NodeKind::UInt64; f.name = "FieldA"; f.offset = 0;
|
||||
tree.addNode(f);
|
||||
f.kind = NodeKind::UInt64; f.name = "FieldB"; f.offset = 8;
|
||||
tree.addNode(f);
|
||||
f.kind = NodeKind::UInt32; f.name = "FieldC"; f.offset = 16;
|
||||
tree.addNode(f);
|
||||
}
|
||||
|
||||
// Add a Pointer64 node that references the target struct, collapsed
|
||||
Node ptr;
|
||||
ptr.kind = NodeKind::Pointer64;
|
||||
ptr.name = "pTarget";
|
||||
ptr.parentId = rootId;
|
||||
ptr.offset = 0;
|
||||
ptr.refId = targetId;
|
||||
ptr.collapsed = true;
|
||||
tree.addNode(ptr);
|
||||
|
||||
// Provider: 8 bytes at offset 0 holding a pointer value
|
||||
QByteArray data(64, '\0');
|
||||
uint64_t ptrVal = 0x00007FFE12340000ULL;
|
||||
memcpy(data.data(), &ptrVal, 8);
|
||||
BufferProvider prov(data, "test_struct_preview");
|
||||
|
||||
ComposeResult cr = compose(tree, prov);
|
||||
m_editor->applyDocument(cr);
|
||||
m_editor->setProviderRef(&prov, nullptr, &tree);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find the pointer line (should be a Pointer64 with foldCollapsed=true)
|
||||
int ptrLine = -1;
|
||||
for (int i = 0; i < cr.meta.size(); ++i) {
|
||||
if (cr.meta[i].nodeKind == NodeKind::Pointer64
|
||||
&& cr.meta[i].foldCollapsed) {
|
||||
ptrLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(ptrLine >= 0, "Could not find collapsed Pointer64 line in compose output");
|
||||
|
||||
// Simulate hover over the value column of the pointer line
|
||||
const LineMeta& lm = cr.meta[ptrLine];
|
||||
QString lineText;
|
||||
{
|
||||
long len = m_editor->scintilla()->SendScintilla(
|
||||
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)ptrLine);
|
||||
QByteArray buf(len + 1, '\0');
|
||||
m_editor->scintilla()->SendScintilla(
|
||||
QsciScintillaBase::SCI_GETLINE, (uintptr_t)ptrLine, static_cast<const char*>(buf.data()));
|
||||
lineText = QString::fromUtf8(buf.left(len));
|
||||
}
|
||||
ColumnSpan vs = m_editor->valueSpan(lm, lineText.size(),
|
||||
lm.effectiveTypeW, lm.effectiveNameW);
|
||||
QVERIFY2(vs.valid, "Value span for pointer line is not valid");
|
||||
|
||||
int hoverCol = (vs.start + vs.end) / 2; // middle of value span
|
||||
QPoint vp = colToViewport(m_editor->scintilla(), ptrLine, hoverCol);
|
||||
sendMouseMove(m_editor->scintilla()->viewport(), vp);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Verify struct preview popup is shown
|
||||
QVERIFY2(m_editor->structPreviewPopup() != nullptr,
|
||||
"Struct preview popup was not created");
|
||||
QVERIFY2(m_editor->structPreviewPopup()->isVisible(),
|
||||
"Struct preview popup is not visible");
|
||||
|
||||
// Restore original document for other tests
|
||||
m_editor->setProviderRef(nullptr, nullptr, nullptr);
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
void testStructPreviewPopupNotShownWhenExpanded() {
|
||||
// Same tree but pointer is NOT collapsed — popup should not show
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.structTypeName = "TestRoot";
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node target;
|
||||
target.kind = NodeKind::Struct;
|
||||
target.structTypeName = "TargetStruct";
|
||||
target.name = "TargetStruct";
|
||||
target.parentId = 0;
|
||||
target.offset = 0;
|
||||
int ti = tree.addNode(target);
|
||||
uint64_t targetId = tree.nodes[ti].id;
|
||||
|
||||
{
|
||||
Node f; f.parentId = targetId;
|
||||
f.kind = NodeKind::UInt64; f.name = "FieldA"; f.offset = 0;
|
||||
tree.addNode(f);
|
||||
f.kind = NodeKind::UInt64; f.name = "FieldB"; f.offset = 8;
|
||||
tree.addNode(f);
|
||||
}
|
||||
|
||||
Node ptr;
|
||||
ptr.kind = NodeKind::Pointer64;
|
||||
ptr.name = "pTarget";
|
||||
ptr.parentId = rootId;
|
||||
ptr.offset = 0;
|
||||
ptr.refId = targetId;
|
||||
ptr.collapsed = false; // expanded
|
||||
tree.addNode(ptr);
|
||||
|
||||
QByteArray data(64, '\0');
|
||||
uint64_t ptrVal = 0x00007FFE12340000ULL;
|
||||
memcpy(data.data(), &ptrVal, 8);
|
||||
BufferProvider prov(data, "test_struct_preview_expanded");
|
||||
|
||||
ComposeResult cr = compose(tree, prov);
|
||||
m_editor->applyDocument(cr);
|
||||
m_editor->setProviderRef(&prov, nullptr, &tree);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find the pointer line (should be Pointer64 and NOT collapsed)
|
||||
int ptrLine = -1;
|
||||
for (int i = 0; i < cr.meta.size(); ++i) {
|
||||
if (cr.meta[i].nodeKind == NodeKind::Pointer64) {
|
||||
ptrLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(ptrLine >= 0, "Could not find Pointer64 line in compose output");
|
||||
|
||||
// Hover at a middle column on the pointer line — expanded pointer header
|
||||
// may not have a standard value span, but we just need to verify no popup
|
||||
int hoverCol = 40; // somewhere in the middle of the line
|
||||
QPoint vp = colToViewport(m_editor->scintilla(), ptrLine, hoverCol);
|
||||
sendMouseMove(m_editor->scintilla()->viewport(), vp);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Struct preview popup should NOT be visible (pointer is expanded)
|
||||
bool popupVisible = m_editor->structPreviewPopup()
|
||||
&& m_editor->structPreviewPopup()->isVisible();
|
||||
QVERIFY2(!popupVisible,
|
||||
"Struct preview popup should not appear for expanded pointer");
|
||||
|
||||
// Restore
|
||||
m_editor->setProviderRef(nullptr, nullptr, nullptr);
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: expanded pointer renders child fields from buffer ──
|
||||
void testPointerExpansionRendersChildren() {
|
||||
PtrDemo d = makePtrDemo(/*collapsed=*/false);
|
||||
ComposeResult cr = compose(d.tree, d.prov);
|
||||
m_editor->applyDocument(cr);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find the pointer header line
|
||||
int ptrHeaderLine = -1;
|
||||
for (int i = 0; i < cr.meta.size(); ++i) {
|
||||
if (cr.meta[i].nodeKind == NodeKind::Pointer64
|
||||
&& cr.meta[i].foldHead && !cr.meta[i].foldCollapsed) {
|
||||
ptrHeaderLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(ptrHeaderLine >= 0, "Should have an expanded Pointer64 header");
|
||||
QCOMPARE(cr.meta[ptrHeaderLine].lineKind, LineKind::Header);
|
||||
|
||||
// Find expanded child fields (x, y, z at depth = header depth + 1)
|
||||
int headerDepth = cr.meta[ptrHeaderLine].depth;
|
||||
int childFieldCount = 0;
|
||||
for (int i = ptrHeaderLine + 1; i < cr.meta.size(); ++i) {
|
||||
const LineMeta& lm = cr.meta[i];
|
||||
if (lm.depth == headerDepth + 1 && lm.lineKind == LineKind::Field)
|
||||
childFieldCount++;
|
||||
if (lm.lineKind == LineKind::Footer && lm.nodeKind == NodeKind::Pointer64)
|
||||
break; // reached pointer footer
|
||||
}
|
||||
QCOMPARE(childFieldCount, 3); // x, y, z
|
||||
|
||||
// Find the pointer footer line
|
||||
int ptrFooterLine = -1;
|
||||
for (int i = ptrHeaderLine + 1; i < cr.meta.size(); ++i) {
|
||||
if (cr.meta[i].lineKind == LineKind::Footer
|
||||
&& cr.meta[i].nodeKind == NodeKind::Pointer64) {
|
||||
ptrFooterLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(ptrFooterLine > ptrHeaderLine, "Should have a pointer footer after header");
|
||||
|
||||
// Verify the composed text contains the child field values
|
||||
// UInt32 displays as hex (e.g. 100 → "0x00000064"), Float as decimal
|
||||
QStringList lines = cr.text.split('\n');
|
||||
bool foundX = false, foundY = false, foundZ = false;
|
||||
for (const QString& line : lines) {
|
||||
if (line.contains("0x64") && line.contains("x")) foundX = true; // 100 = 0x64
|
||||
if (line.contains("0xc8") && line.contains("y")) foundY = true; // 200 = 0xc8
|
||||
if (line.contains("3.14") && line.contains("z")) foundZ = true;
|
||||
}
|
||||
QVERIFY2(foundX, "Child field 'x' with value 0x64 should appear in output");
|
||||
QVERIFY2(foundY, "Child field 'y' with value 0xc8 should appear in output");
|
||||
QVERIFY2(foundZ, "Child field 'z' with value 3.14 should appear in output");
|
||||
|
||||
// Verify the pointer type name appears
|
||||
QVERIFY2(cr.text.contains("ChildData*"),
|
||||
"Pointer type 'ChildData*' should appear in output");
|
||||
|
||||
// Editor should have rendered all lines
|
||||
int editorLineCount = m_editor->scintilla()->lines();
|
||||
QVERIFY2(editorLineCount >= cr.meta.size(),
|
||||
qPrintable(QString("Editor has %1 lines but compose has %2 meta entries")
|
||||
.arg(editorLineCount).arg(cr.meta.size())));
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: collapsed pointer hides child fields ──
|
||||
void testPointerCollapsedHidesChildren() {
|
||||
PtrDemo expanded = makePtrDemo(/*collapsed=*/false);
|
||||
ComposeResult crExpanded = compose(expanded.tree, expanded.prov);
|
||||
|
||||
PtrDemo collapsed = makePtrDemo(/*collapsed=*/true);
|
||||
ComposeResult crCollapsed = compose(collapsed.tree, collapsed.prov);
|
||||
|
||||
// Collapsed should have fewer lines (no child fields, no pointer footer)
|
||||
QVERIFY2(crCollapsed.meta.size() < crExpanded.meta.size(),
|
||||
qPrintable(QString("Collapsed (%1 lines) should be smaller than expanded (%2)")
|
||||
.arg(crCollapsed.meta.size()).arg(crExpanded.meta.size())));
|
||||
|
||||
// The pointer line should be a Field (not Header) with foldCollapsed=true
|
||||
bool foundCollapsedPtr = false;
|
||||
for (const LineMeta& lm : crCollapsed.meta) {
|
||||
if (lm.nodeKind == NodeKind::Pointer64 && lm.foldHead) {
|
||||
QVERIFY(lm.foldCollapsed);
|
||||
QCOMPARE(lm.lineKind, LineKind::Field);
|
||||
foundCollapsedPtr = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundCollapsedPtr, "Should have a collapsed Pointer64 fold head");
|
||||
|
||||
// No child fields from ChildData should appear in the main struct section
|
||||
bool foundChildField = false;
|
||||
for (const LineMeta& lm : crCollapsed.meta) {
|
||||
if (lm.lineKind == LineKind::Footer && lm.nodeKind == NodeKind::Pointer64) {
|
||||
foundChildField = true; // pointer footer exists = children visible
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(!foundChildField,
|
||||
"Collapsed pointer should not have a pointer footer (no children)");
|
||||
|
||||
// Apply collapsed to editor
|
||||
m_editor->applyDocument(crCollapsed);
|
||||
QApplication::processEvents();
|
||||
|
||||
int collapsedLines = m_editor->scintilla()->lines();
|
||||
m_editor->applyDocument(crExpanded);
|
||||
QApplication::processEvents();
|
||||
int expandedLines = m_editor->scintilla()->lines();
|
||||
|
||||
QVERIFY2(collapsedLines < expandedLines,
|
||||
qPrintable(QString("Collapsed (%1 editor lines) should be fewer than expanded (%2)")
|
||||
.arg(collapsedLines).arg(expandedLines)));
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: null pointer still shows template fields (via NullProvider) ──
|
||||
void testPointerNullShowsTemplate() {
|
||||
PtrDemo d = makePtrDemo(/*collapsed=*/false, /*nullPtr=*/true);
|
||||
ComposeResult cr = compose(d.tree, d.prov);
|
||||
m_editor->applyDocument(cr);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Even with null pointer, expanded pointer should show template children
|
||||
int ptrHeaderLine = -1;
|
||||
for (int i = 0; i < cr.meta.size(); ++i) {
|
||||
if (cr.meta[i].nodeKind == NodeKind::Pointer64
|
||||
&& cr.meta[i].foldHead && !cr.meta[i].foldCollapsed) {
|
||||
ptrHeaderLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(ptrHeaderLine >= 0,
|
||||
"Null pointer should still produce an expanded header");
|
||||
|
||||
// Should have child field lines (template from NullProvider shows zeros)
|
||||
int headerDepth = cr.meta[ptrHeaderLine].depth;
|
||||
int childFieldCount = 0;
|
||||
for (int i = ptrHeaderLine + 1; i < cr.meta.size(); ++i) {
|
||||
const LineMeta& lm = cr.meta[i];
|
||||
if (lm.depth == headerDepth + 1 && lm.lineKind == LineKind::Field)
|
||||
childFieldCount++;
|
||||
if (lm.lineKind == LineKind::Footer && lm.nodeKind == NodeKind::Pointer64)
|
||||
break;
|
||||
}
|
||||
QCOMPARE(childFieldCount, 3); // x, y, z template still rendered
|
||||
|
||||
// Verify ChildData* appears in output
|
||||
QVERIFY2(cr.text.contains("ChildData*"),
|
||||
"Null pointer should still show 'ChildData*' type");
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
|
||||
// ── Test: nested pointer chain renders multiple expansion levels ──
|
||||
void testPointerChainExpansion() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
// Root struct
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.structTypeName = "Chain";
|
||||
root.name = "chain";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Inner struct (innermost target)
|
||||
Node inner;
|
||||
inner.kind = NodeKind::Struct;
|
||||
inner.structTypeName = "Inner";
|
||||
inner.name = "Inner";
|
||||
inner.parentId = 0;
|
||||
inner.offset = 300;
|
||||
int ii = tree.addNode(inner);
|
||||
uint64_t innerId = tree.nodes[ii].id;
|
||||
{
|
||||
Node f;
|
||||
f.kind = NodeKind::UInt32; f.name = "value";
|
||||
f.parentId = innerId; f.offset = 0;
|
||||
tree.addNode(f);
|
||||
}
|
||||
|
||||
// Outer struct (contains pointer to Inner)
|
||||
Node outer;
|
||||
outer.kind = NodeKind::Struct;
|
||||
outer.structTypeName = "Outer";
|
||||
outer.name = "Outer";
|
||||
outer.parentId = 0;
|
||||
outer.offset = 200;
|
||||
int oi = tree.addNode(outer);
|
||||
uint64_t outerId = tree.nodes[oi].id;
|
||||
{
|
||||
Node f;
|
||||
f.kind = NodeKind::UInt32; f.name = "tag";
|
||||
f.parentId = outerId; f.offset = 0;
|
||||
tree.addNode(f);
|
||||
|
||||
Node p;
|
||||
p.kind = NodeKind::Pointer64; p.name = "pInner";
|
||||
p.parentId = outerId; p.offset = 8;
|
||||
p.refId = innerId;
|
||||
tree.addNode(p);
|
||||
}
|
||||
|
||||
// Root pointer to Outer
|
||||
{
|
||||
Node p;
|
||||
p.kind = NodeKind::Pointer64; p.name = "pOuter";
|
||||
p.parentId = rootId; p.offset = 0;
|
||||
p.refId = outerId;
|
||||
tree.addNode(p);
|
||||
}
|
||||
|
||||
// Buffer: pOuter at 0 → 32, pInner at 32+8=40 → 64, value at 64 = 999
|
||||
QByteArray data(128, '\0');
|
||||
uint64_t pOuter = 32; memcpy(data.data() + 0, &pOuter, 8);
|
||||
uint64_t pInner = 64; memcpy(data.data() + 40, &pInner, 8);
|
||||
uint32_t tag = 0xAB; memcpy(data.data() + 32, &tag, 4);
|
||||
uint32_t val = 999; memcpy(data.data() + 64, &val, 4);
|
||||
BufferProvider prov(data, "chain_demo");
|
||||
|
||||
ComposeResult cr = compose(tree, prov);
|
||||
m_editor->applyDocument(cr);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Both Outer* and Inner* should appear
|
||||
QVERIFY2(cr.text.contains("Outer*"), "Should display 'Outer*' pointer type");
|
||||
QVERIFY2(cr.text.contains("Inner*"), "Should display 'Inner*' pointer type");
|
||||
|
||||
// Count pointer fold heads — should have at least 2 (pOuter + pInner)
|
||||
int ptrFoldHeads = 0;
|
||||
int maxDepth = 0;
|
||||
for (const LineMeta& lm : cr.meta) {
|
||||
if (lm.foldHead && lm.nodeKind == NodeKind::Pointer64)
|
||||
ptrFoldHeads++;
|
||||
if (lm.depth > maxDepth) maxDepth = lm.depth;
|
||||
}
|
||||
QVERIFY2(ptrFoldHeads >= 2,
|
||||
qPrintable(QString("Expected >=2 pointer fold heads, got %1")
|
||||
.arg(ptrFoldHeads)));
|
||||
|
||||
// Depth should reach at least 3 (root=0, pOuter children=1..2, pInner children=2..3)
|
||||
QVERIFY2(maxDepth >= 3,
|
||||
qPrintable(QString("Expected max depth >= 3 for chain, got %1")
|
||||
.arg(maxDepth)));
|
||||
|
||||
// Verify innermost value (999 = 0x3e7) appears in the output
|
||||
QVERIFY2(cr.text.contains("0x3e7"),
|
||||
"Innermost field 'value = 0x3e7' should appear in chain expansion");
|
||||
|
||||
m_editor->applyDocument(m_result);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestEditor)
|
||||
|
||||
360
tests/test_export_xml.cpp
Normal file
360
tests/test_export_xml.cpp
Normal file
@@ -0,0 +1,360 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include <QTemporaryFile>
|
||||
#include "core.h"
|
||||
#include "export_reclass_xml.h"
|
||||
#include "import_reclass_xml.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestExportXml : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void exportEmptyTree();
|
||||
void exportSingleStruct();
|
||||
void exportPointerRef();
|
||||
void exportEmbeddedStruct();
|
||||
void exportArray();
|
||||
void exportTextNodes();
|
||||
void exportVectors();
|
||||
void exportHexCollapse();
|
||||
void exportMultiClass();
|
||||
void roundTripImportExport();
|
||||
};
|
||||
|
||||
static int countRoots(const NodeTree& tree) {
|
||||
int n = 0;
|
||||
for (const auto& node : tree.nodes)
|
||||
if (node.parentId == 0 && node.kind == NodeKind::Struct) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
static QVector<int> childrenOf(const NodeTree& tree, uint64_t parentId) {
|
||||
QVector<int> result;
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
if (tree.nodes[i].parentId == parentId) result.append(i);
|
||||
return result;
|
||||
}
|
||||
|
||||
static QString exportToString(const NodeTree& tree) {
|
||||
QTemporaryFile tmp;
|
||||
tmp.setAutoRemove(true);
|
||||
if (!tmp.open()) return {};
|
||||
QString path = tmp.fileName();
|
||||
tmp.close();
|
||||
|
||||
QString err;
|
||||
if (!exportReclassXml(tree, path, &err)) return {};
|
||||
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
|
||||
return QString::fromUtf8(f.readAll());
|
||||
}
|
||||
|
||||
static NodeTree roundTrip(const NodeTree& tree) {
|
||||
QTemporaryFile tmp;
|
||||
tmp.setAutoRemove(true);
|
||||
if (!tmp.open()) return {};
|
||||
QString path = tmp.fileName();
|
||||
tmp.close();
|
||||
|
||||
QString err;
|
||||
if (!exportReclassXml(tree, path, &err)) return {};
|
||||
return importReclassXml(path, &err);
|
||||
}
|
||||
|
||||
// ── Tests ──
|
||||
|
||||
void TestExportXml::exportEmptyTree() {
|
||||
NodeTree tree;
|
||||
QString err;
|
||||
QVERIFY(!exportReclassXml(tree, "dummy.xml", &err));
|
||||
QVERIFY(!err.isEmpty());
|
||||
}
|
||||
|
||||
void TestExportXml::exportSingleStruct() {
|
||||
NodeTree tree;
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Player");
|
||||
s.structTypeName = QStringLiteral("Player"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
Node f1; f1.kind = NodeKind::Int32; f1.name = QStringLiteral("health");
|
||||
f1.parentId = sid; f1.offset = 0; tree.addNode(f1);
|
||||
|
||||
Node f2; f2.kind = NodeKind::Float; f2.name = QStringLiteral("speed");
|
||||
f2.parentId = sid; f2.offset = 4; tree.addNode(f2);
|
||||
|
||||
Node f3; f3.kind = NodeKind::UInt64; f3.name = QStringLiteral("id");
|
||||
f3.parentId = sid; f3.offset = 8; tree.addNode(f3);
|
||||
|
||||
QString xml = exportToString(tree);
|
||||
QVERIFY(!xml.isEmpty());
|
||||
QVERIFY(xml.contains(QStringLiteral("Player")));
|
||||
QVERIFY(xml.contains(QStringLiteral("health")));
|
||||
QVERIFY(xml.contains(QStringLiteral("speed")));
|
||||
QVERIFY(xml.contains(QStringLiteral("ReClassEx")));
|
||||
|
||||
// Round-trip
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 1);
|
||||
QCOMPARE(rt.nodes[0].name, QStringLiteral("Player"));
|
||||
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::Int32);
|
||||
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::Float);
|
||||
QCOMPARE(rt.nodes[kids[2]].kind, NodeKind::UInt64);
|
||||
}
|
||||
|
||||
void TestExportXml::exportPointerRef() {
|
||||
NodeTree tree;
|
||||
Node s1; s1.kind = NodeKind::Struct; s1.name = QStringLiteral("Target");
|
||||
s1.structTypeName = QStringLiteral("Target"); s1.parentId = 0;
|
||||
int s1i = tree.addNode(s1);
|
||||
uint64_t s1id = tree.nodes[s1i].id;
|
||||
|
||||
Node f; f.kind = NodeKind::Int32; f.name = QStringLiteral("val");
|
||||
f.parentId = s1id; f.offset = 0; tree.addNode(f);
|
||||
|
||||
Node s2; s2.kind = NodeKind::Struct; s2.name = QStringLiteral("HasPtr");
|
||||
s2.structTypeName = QStringLiteral("HasPtr"); s2.parentId = 0;
|
||||
int s2i = tree.addNode(s2);
|
||||
uint64_t s2id = tree.nodes[s2i].id;
|
||||
|
||||
Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QStringLiteral("pTarget");
|
||||
ptr.parentId = s2id; ptr.offset = 0; ptr.refId = s1id;
|
||||
tree.addNode(ptr);
|
||||
|
||||
QString xml = exportToString(tree);
|
||||
QVERIFY(xml.contains(QStringLiteral("Pointer=\"Target\"")));
|
||||
|
||||
// Round-trip: pointer should resolve
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 2);
|
||||
bool foundPtr = false;
|
||||
for (const auto& n : rt.nodes) {
|
||||
if (n.kind == NodeKind::Pointer64 && n.name == QStringLiteral("pTarget")) {
|
||||
QVERIFY(n.refId != 0);
|
||||
foundPtr = true;
|
||||
}
|
||||
}
|
||||
QVERIFY(foundPtr);
|
||||
}
|
||||
|
||||
void TestExportXml::exportEmbeddedStruct() {
|
||||
NodeTree tree;
|
||||
Node inner; inner.kind = NodeKind::Struct; inner.name = QStringLiteral("Inner");
|
||||
inner.structTypeName = QStringLiteral("Inner"); inner.parentId = 0;
|
||||
int ii = tree.addNode(inner);
|
||||
uint64_t iid = tree.nodes[ii].id;
|
||||
|
||||
Node iv; iv.kind = NodeKind::Int32; iv.name = QStringLiteral("x");
|
||||
iv.parentId = iid; iv.offset = 0; tree.addNode(iv);
|
||||
|
||||
Node outer; outer.kind = NodeKind::Struct; outer.name = QStringLiteral("Outer");
|
||||
outer.structTypeName = QStringLiteral("Outer"); outer.parentId = 0;
|
||||
int oi = tree.addNode(outer);
|
||||
uint64_t oid = tree.nodes[oi].id;
|
||||
|
||||
Node embed; embed.kind = NodeKind::Struct; embed.name = QStringLiteral("embedded");
|
||||
embed.structTypeName = QStringLiteral("Inner"); embed.parentId = oid;
|
||||
embed.offset = 0; embed.refId = iid;
|
||||
tree.addNode(embed);
|
||||
|
||||
QString xml = exportToString(tree);
|
||||
QVERIFY(xml.contains(QStringLiteral("Instance=\"Inner\"")));
|
||||
}
|
||||
|
||||
void TestExportXml::exportArray() {
|
||||
NodeTree tree;
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Container");
|
||||
s.structTypeName = QStringLiteral("Container"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
Node arr; arr.kind = NodeKind::Array; arr.name = QStringLiteral("items");
|
||||
arr.parentId = sid; arr.offset = 0; arr.arrayLen = 10;
|
||||
arr.elementKind = NodeKind::Int32;
|
||||
tree.addNode(arr);
|
||||
|
||||
QString xml = exportToString(tree);
|
||||
QVERIFY(xml.contains(QStringLiteral("Total=\"10\"")));
|
||||
QVERIFY(xml.contains(QStringLiteral("<Array")));
|
||||
}
|
||||
|
||||
void TestExportXml::exportTextNodes() {
|
||||
NodeTree tree;
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("TextStruct");
|
||||
s.structTypeName = QStringLiteral("TextStruct"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
Node u8; u8.kind = NodeKind::UTF8; u8.name = QStringLiteral("name");
|
||||
u8.parentId = sid; u8.offset = 0; u8.strLen = 32; tree.addNode(u8);
|
||||
|
||||
Node u16; u16.kind = NodeKind::UTF16; u16.name = QStringLiteral("wname");
|
||||
u16.parentId = sid; u16.offset = 32; u16.strLen = 16; tree.addNode(u16);
|
||||
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 1);
|
||||
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::UTF8);
|
||||
QCOMPARE(rt.nodes[kids[0]].strLen, 32);
|
||||
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::UTF16);
|
||||
QCOMPARE(rt.nodes[kids[1]].strLen, 16);
|
||||
}
|
||||
|
||||
void TestExportXml::exportVectors() {
|
||||
NodeTree tree;
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Vectors");
|
||||
s.structTypeName = QStringLiteral("Vectors"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
Node v2; v2.kind = NodeKind::Vec2; v2.name = QStringLiteral("pos2");
|
||||
v2.parentId = sid; v2.offset = 0; tree.addNode(v2);
|
||||
|
||||
Node v3; v3.kind = NodeKind::Vec3; v3.name = QStringLiteral("pos3");
|
||||
v3.parentId = sid; v3.offset = 8; tree.addNode(v3);
|
||||
|
||||
Node v4; v4.kind = NodeKind::Vec4; v4.name = QStringLiteral("rot");
|
||||
v4.parentId = sid; v4.offset = 20; tree.addNode(v4);
|
||||
|
||||
Node m; m.kind = NodeKind::Mat4x4; m.name = QStringLiteral("matrix");
|
||||
m.parentId = sid; m.offset = 36; tree.addNode(m);
|
||||
|
||||
NodeTree rt = roundTrip(tree);
|
||||
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 4);
|
||||
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::Vec2);
|
||||
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::Vec3);
|
||||
QCOMPARE(rt.nodes[kids[2]].kind, NodeKind::Vec4);
|
||||
QCOMPARE(rt.nodes[kids[3]].kind, NodeKind::Mat4x4);
|
||||
}
|
||||
|
||||
void TestExportXml::exportHexCollapse() {
|
||||
NodeTree tree;
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("HexTest");
|
||||
s.structTypeName = QStringLiteral("HexTest"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
// 4 consecutive Hex8 nodes should collapse to one Custom node
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Node h; h.kind = NodeKind::Hex8; h.parentId = sid; h.offset = i;
|
||||
tree.addNode(h);
|
||||
}
|
||||
// Followed by a real field
|
||||
Node f; f.kind = NodeKind::Int32; f.name = QStringLiteral("val");
|
||||
f.parentId = sid; f.offset = 4; tree.addNode(f);
|
||||
|
||||
QString xml = exportToString(tree);
|
||||
// Should have Type="21" (Custom) for the collapsed hex
|
||||
QVERIFY(xml.contains(QStringLiteral("Type=\"21\"")));
|
||||
// Size should be 4
|
||||
QVERIFY(xml.contains(QStringLiteral("Size=\"4\"")));
|
||||
|
||||
// Round-trip: custom expands back to hex nodes
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 1);
|
||||
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||
// Import expands Custom(4 bytes) to best-fit hex: Hex32 (1 node) + Int32 = 2
|
||||
QVERIFY(kids.size() >= 2);
|
||||
// Last child should be Int32
|
||||
QCOMPARE(rt.nodes[kids.last()].kind, NodeKind::Int32);
|
||||
}
|
||||
|
||||
void TestExportXml::exportMultiClass() {
|
||||
NodeTree tree;
|
||||
for (int c = 0; c < 5; c++) {
|
||||
Node s; s.kind = NodeKind::Struct;
|
||||
s.name = QStringLiteral("Class%1").arg(c);
|
||||
s.structTypeName = s.name; s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
Node f; f.kind = NodeKind::Int32;
|
||||
f.name = QStringLiteral("field%1").arg(c);
|
||||
f.parentId = sid; f.offset = 0; tree.addNode(f);
|
||||
}
|
||||
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 5);
|
||||
|
||||
// All class names preserved
|
||||
QSet<QString> names;
|
||||
for (const auto& n : rt.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) names.insert(n.name);
|
||||
for (int c = 0; c < 5; c++)
|
||||
QVERIFY(names.contains(QStringLiteral("Class%1").arg(c)));
|
||||
}
|
||||
|
||||
void TestExportXml::roundTripImportExport() {
|
||||
// Build a comprehensive tree and verify it survives export->import
|
||||
NodeTree tree;
|
||||
|
||||
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("FullTest");
|
||||
s.structTypeName = QStringLiteral("FullTest"); s.parentId = 0;
|
||||
int si = tree.addNode(s);
|
||||
uint64_t sid = tree.nodes[si].id;
|
||||
|
||||
int offset = 0;
|
||||
auto addField = [&](NodeKind kind, const QString& name) {
|
||||
Node n; n.kind = kind; n.name = name; n.parentId = sid; n.offset = offset;
|
||||
tree.addNode(n);
|
||||
offset += sizeForKind(kind);
|
||||
};
|
||||
|
||||
addField(NodeKind::Int8, QStringLiteral("a"));
|
||||
addField(NodeKind::Int16, QStringLiteral("b"));
|
||||
addField(NodeKind::Int32, QStringLiteral("c"));
|
||||
addField(NodeKind::Int64, QStringLiteral("d"));
|
||||
addField(NodeKind::UInt8, QStringLiteral("e"));
|
||||
addField(NodeKind::UInt16, QStringLiteral("f"));
|
||||
addField(NodeKind::UInt32, QStringLiteral("g"));
|
||||
addField(NodeKind::UInt64, QStringLiteral("h"));
|
||||
addField(NodeKind::Float, QStringLiteral("i"));
|
||||
addField(NodeKind::Double, QStringLiteral("j"));
|
||||
addField(NodeKind::Vec2, QStringLiteral("k"));
|
||||
addField(NodeKind::Vec3, QStringLiteral("l"));
|
||||
addField(NodeKind::Vec4, QStringLiteral("m"));
|
||||
|
||||
// Self-pointer
|
||||
Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QStringLiteral("self");
|
||||
ptr.parentId = sid; ptr.offset = offset; ptr.refId = sid;
|
||||
tree.addNode(ptr);
|
||||
offset += 8;
|
||||
|
||||
// UTF8
|
||||
Node u8; u8.kind = NodeKind::UTF8; u8.name = QStringLiteral("str");
|
||||
u8.parentId = sid; u8.offset = offset; u8.strLen = 64;
|
||||
tree.addNode(u8);
|
||||
|
||||
NodeTree rt = roundTrip(tree);
|
||||
QCOMPARE(countRoots(rt), 1);
|
||||
QCOMPARE(rt.nodes[0].name, QStringLiteral("FullTest"));
|
||||
|
||||
auto origKids = childrenOf(tree, sid);
|
||||
auto rtKids = childrenOf(rt, rt.nodes[0].id);
|
||||
QCOMPARE(rtKids.size(), origKids.size());
|
||||
|
||||
// Verify each field kind matches
|
||||
for (int i = 0; i < origKids.size(); i++) {
|
||||
QCOMPARE(rt.nodes[rtKids[i]].kind, tree.nodes[origKids[i]].kind);
|
||||
QCOMPARE(rt.nodes[rtKids[i]].name, tree.nodes[origKids[i]].name);
|
||||
}
|
||||
|
||||
// Verify self-pointer resolved
|
||||
bool foundSelf = false;
|
||||
for (const auto& n : rt.nodes) {
|
||||
if (n.name == QStringLiteral("self") && n.kind == NodeKind::Pointer64) {
|
||||
QVERIFY(n.refId != 0);
|
||||
QCOMPARE(n.refId, rt.nodes[0].id);
|
||||
foundSelf = true;
|
||||
}
|
||||
}
|
||||
QVERIFY(foundSelf);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestExportXml)
|
||||
#include "test_export_xml.moc"
|
||||
@@ -418,30 +418,6 @@ private slots:
|
||||
QVERIFY(result.contains("wchar_t wname[32];"));
|
||||
}
|
||||
|
||||
// ── Padding node ──
|
||||
|
||||
void testPaddingNode() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "PadTest";
|
||||
root.structTypeName = "PadTest";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node pad;
|
||||
pad.kind = rcx::NodeKind::Padding;
|
||||
pad.name = "reserved";
|
||||
pad.parentId = rootId;
|
||||
pad.offset = 0;
|
||||
pad.arrayLen = 16;
|
||||
tree.addNode(pad);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
QVERIFY(result.contains("uint8_t reserved[16];"));
|
||||
}
|
||||
|
||||
// ── Full SDK export (multiple root structs) ──
|
||||
|
||||
void testFullSdkExport() {
|
||||
|
||||
846
tests/test_import_source.cpp
Normal file
846
tests/test_import_source.cpp
Normal file
@@ -0,0 +1,846 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include "core.h"
|
||||
#include "import_source.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestImportSource : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
// Basic type tests
|
||||
void emptyInput();
|
||||
void noStructs();
|
||||
void singleEmptyStruct();
|
||||
void stdintTypes();
|
||||
void windowsTypes();
|
||||
void platformPointerTypes();
|
||||
void standardCTypes();
|
||||
void multiWordTypes();
|
||||
void floatDouble();
|
||||
void boolType();
|
||||
|
||||
// Pointer tests
|
||||
void voidPointer();
|
||||
void typedPointer();
|
||||
void selfReferencingPointer();
|
||||
void doublePointer();
|
||||
|
||||
// Array tests
|
||||
void primitiveArray();
|
||||
void charArrayToUtf8();
|
||||
void wcharArrayToUtf16();
|
||||
void floatArrayToVec2();
|
||||
void floatArrayToVec3();
|
||||
void floatArrayToVec4();
|
||||
void floatArray4x4ToMat4x4();
|
||||
void genericFloatArray();
|
||||
void structArray();
|
||||
|
||||
// Comment offset tests
|
||||
void commentOffsets();
|
||||
void computedOffsets();
|
||||
void mixedOffsetsAutoDetect();
|
||||
|
||||
// Multi-struct tests
|
||||
void multiStruct();
|
||||
void pointerCrossRef();
|
||||
|
||||
// Forward declarations
|
||||
void forwardDeclaration();
|
||||
|
||||
// Union handling
|
||||
void unionPickFirst();
|
||||
|
||||
// Padding fields
|
||||
void paddingFieldExpansion();
|
||||
|
||||
// static_assert
|
||||
void staticAssertTailPadding();
|
||||
|
||||
// Embedded struct
|
||||
void embeddedStruct();
|
||||
|
||||
// Typedef
|
||||
void typedefBasic();
|
||||
|
||||
// Qualifiers
|
||||
void constVolatileQualifiers();
|
||||
void structPrefixOnType();
|
||||
|
||||
// Edge cases
|
||||
void bitfieldSkipped();
|
||||
void hexArraySizes();
|
||||
void windowsStylePEB();
|
||||
void classKeyword();
|
||||
void inheritanceSkipped();
|
||||
|
||||
// Round-trip test (requires generator.h)
|
||||
void basicRoundTrip();
|
||||
};
|
||||
|
||||
// ── Helper ──
|
||||
|
||||
static int countRoots(const NodeTree& tree) {
|
||||
int n = 0;
|
||||
for (const auto& node : tree.nodes)
|
||||
if (node.parentId == 0 && node.kind == NodeKind::Struct) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
static QVector<int> childrenOf(const NodeTree& tree, uint64_t parentId) {
|
||||
QVector<int> result;
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
if (tree.nodes[i].parentId == parentId) result.append(i);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Tests ──
|
||||
|
||||
void TestImportSource::emptyInput() {
|
||||
QString err;
|
||||
NodeTree tree = importFromSource(QString(), &err);
|
||||
QVERIFY(tree.nodes.isEmpty());
|
||||
QVERIFY(!err.isEmpty());
|
||||
}
|
||||
|
||||
void TestImportSource::noStructs() {
|
||||
QString err;
|
||||
NodeTree tree = importFromSource(QStringLiteral("int x = 42;"), &err);
|
||||
QVERIFY(tree.nodes.isEmpty());
|
||||
QVERIFY(!err.isEmpty());
|
||||
}
|
||||
|
||||
void TestImportSource::singleEmptyStruct() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Empty {};\n"
|
||||
));
|
||||
QCOMPARE(countRoots(tree), 1);
|
||||
QCOMPARE(tree.nodes[0].name, QStringLiteral("Empty"));
|
||||
QCOMPARE(tree.nodes[0].kind, NodeKind::Struct);
|
||||
}
|
||||
|
||||
void TestImportSource::stdintTypes() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Test {\n"
|
||||
" uint8_t a;\n"
|
||||
" int8_t b;\n"
|
||||
" uint16_t c;\n"
|
||||
" int16_t d;\n"
|
||||
" uint32_t e;\n"
|
||||
" int32_t f;\n"
|
||||
" uint64_t g;\n"
|
||||
" int64_t h;\n"
|
||||
"};\n"
|
||||
));
|
||||
QCOMPARE(countRoots(tree), 1);
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 8);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int8);
|
||||
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt16);
|
||||
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::Int16);
|
||||
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt32);
|
||||
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Int32);
|
||||
QCOMPARE(tree.nodes[kids[6]].kind, NodeKind::UInt64);
|
||||
QCOMPARE(tree.nodes[kids[7]].kind, NodeKind::Int64);
|
||||
}
|
||||
|
||||
void TestImportSource::windowsTypes() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct WinTypes {\n"
|
||||
" BYTE a;\n"
|
||||
" WORD b;\n"
|
||||
" DWORD c;\n"
|
||||
" QWORD d;\n"
|
||||
" ULONG e;\n"
|
||||
" LONG f;\n"
|
||||
" USHORT g;\n"
|
||||
" UCHAR h;\n"
|
||||
" BOOLEAN i;\n"
|
||||
" BOOL j;\n"
|
||||
" CHAR k;\n"
|
||||
" WCHAR l;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 12);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8); // BYTE
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::UInt16); // WORD
|
||||
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt32); // DWORD
|
||||
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt64); // QWORD
|
||||
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt32); // ULONG
|
||||
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Int32); // LONG
|
||||
QCOMPARE(tree.nodes[kids[6]].kind, NodeKind::UInt16); // USHORT
|
||||
QCOMPARE(tree.nodes[kids[7]].kind, NodeKind::UInt8); // UCHAR
|
||||
QCOMPARE(tree.nodes[kids[8]].kind, NodeKind::UInt8); // BOOLEAN
|
||||
QCOMPARE(tree.nodes[kids[9]].kind, NodeKind::Int32); // BOOL
|
||||
QCOMPARE(tree.nodes[kids[10]].kind, NodeKind::Int8); // CHAR
|
||||
QCOMPARE(tree.nodes[kids[11]].kind, NodeKind::UInt16); // WCHAR
|
||||
}
|
||||
|
||||
void TestImportSource::platformPointerTypes() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct PtrTypes {\n"
|
||||
" PVOID a;\n"
|
||||
" HANDLE b;\n"
|
||||
" SIZE_T c;\n"
|
||||
" ULONG_PTR d;\n"
|
||||
" uintptr_t e;\n"
|
||||
" size_t f;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 6);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt64);
|
||||
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt64);
|
||||
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt64);
|
||||
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::UInt64);
|
||||
}
|
||||
|
||||
void TestImportSource::standardCTypes() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct CTypes {\n"
|
||||
" char a;\n"
|
||||
" short b;\n"
|
||||
" int c;\n"
|
||||
" long d;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 4);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Int8); // char
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int16); // short
|
||||
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::Int32); // int
|
||||
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::Int32); // long
|
||||
}
|
||||
|
||||
void TestImportSource::multiWordTypes() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct MultiWord {\n"
|
||||
" unsigned char a;\n"
|
||||
" unsigned short b;\n"
|
||||
" unsigned int c;\n"
|
||||
" unsigned long d;\n"
|
||||
" long long e;\n"
|
||||
" unsigned long long f;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 6);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::UInt16);
|
||||
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt32);
|
||||
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt32);
|
||||
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::Int64);
|
||||
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::UInt64);
|
||||
}
|
||||
|
||||
void TestImportSource::floatDouble() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct FD {\n"
|
||||
" float a;\n"
|
||||
" double b;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Double);
|
||||
}
|
||||
|
||||
void TestImportSource::boolType() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct B {\n"
|
||||
" bool a;\n"
|
||||
" _Bool b;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Bool);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Bool);
|
||||
}
|
||||
|
||||
void TestImportSource::voidPointer() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct VP {\n"
|
||||
" void* ptr;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("ptr"));
|
||||
QCOMPARE(tree.nodes[kids[0]].refId, uint64_t(0)); // void* has no target
|
||||
}
|
||||
|
||||
void TestImportSource::typedPointer() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Target {\n"
|
||||
" int x;\n"
|
||||
"};\n"
|
||||
"struct HasPtr {\n"
|
||||
" Target* pTarget;\n"
|
||||
"};\n"
|
||||
));
|
||||
QCOMPARE(countRoots(tree), 2);
|
||||
// Find HasPtr
|
||||
int hasPtrIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == QStringLiteral("HasPtr") && tree.nodes[i].parentId == 0) {
|
||||
hasPtrIdx = i; break;
|
||||
}
|
||||
}
|
||||
QVERIFY(hasPtrIdx >= 0);
|
||||
auto kids = childrenOf(tree, tree.nodes[hasPtrIdx].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||
// refId should point to Target struct
|
||||
int targetIdx = tree.indexOfId(tree.nodes[kids[0]].refId);
|
||||
QVERIFY(targetIdx >= 0);
|
||||
QCOMPARE(tree.nodes[targetIdx].name, QStringLiteral("Target"));
|
||||
}
|
||||
|
||||
void TestImportSource::selfReferencingPointer() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Node {\n"
|
||||
" int value;\n"
|
||||
" Node* next;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[kids[1]].refId, tree.nodes[0].id);
|
||||
}
|
||||
|
||||
void TestImportSource::doublePointer() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct DP {\n"
|
||||
" void** ppData;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||
}
|
||||
|
||||
void TestImportSource::primitiveArray() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct PA {\n"
|
||||
" int32_t values[10];\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||
QCOMPARE(tree.nodes[kids[0]].arrayLen, 10);
|
||||
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Int32);
|
||||
}
|
||||
|
||||
void TestImportSource::charArrayToUtf8() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct CA {\n"
|
||||
" char name[64];\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UTF8);
|
||||
QCOMPARE(tree.nodes[kids[0]].strLen, 64);
|
||||
}
|
||||
|
||||
void TestImportSource::wcharArrayToUtf16() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct WC {\n"
|
||||
" wchar_t name[32];\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UTF16);
|
||||
QCOMPARE(tree.nodes[kids[0]].strLen, 32);
|
||||
}
|
||||
|
||||
void TestImportSource::floatArrayToVec2() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct V {\n"
|
||||
" float pos[2];\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec2);
|
||||
}
|
||||
|
||||
void TestImportSource::floatArrayToVec3() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct V {\n"
|
||||
" float pos[3];\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec3);
|
||||
}
|
||||
|
||||
void TestImportSource::floatArrayToVec4() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct V {\n"
|
||||
" float rot[4];\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec4);
|
||||
}
|
||||
|
||||
void TestImportSource::floatArray4x4ToMat4x4() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct M {\n"
|
||||
" float matrix[4][4];\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Mat4x4);
|
||||
}
|
||||
|
||||
void TestImportSource::genericFloatArray() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct GF {\n"
|
||||
" float values[8];\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||
QCOMPARE(tree.nodes[kids[0]].arrayLen, 8);
|
||||
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Float);
|
||||
}
|
||||
|
||||
void TestImportSource::structArray() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Item {\n"
|
||||
" int id;\n"
|
||||
"};\n"
|
||||
"struct Container {\n"
|
||||
" Item items[5];\n"
|
||||
"};\n"
|
||||
));
|
||||
QCOMPARE(countRoots(tree), 2);
|
||||
// Find Container
|
||||
int contIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == QStringLiteral("Container") && tree.nodes[i].parentId == 0) {
|
||||
contIdx = i; break;
|
||||
}
|
||||
}
|
||||
QVERIFY(contIdx >= 0);
|
||||
auto kids = childrenOf(tree, tree.nodes[contIdx].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||
QCOMPARE(tree.nodes[kids[0]].arrayLen, 5);
|
||||
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Struct);
|
||||
}
|
||||
|
||||
void TestImportSource::commentOffsets() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Offsets {\n"
|
||||
" uint64_t vtable; // 0x0\n"
|
||||
" float health; // 0x8\n"
|
||||
" uint8_t _pad000C[0x4]; // 0xC\n"
|
||||
" double score; // 0x10\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// vtable at 0x0
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt64);
|
||||
// health at 0x8
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 8);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Float);
|
||||
// _pad at 0xC -> hex nodes
|
||||
// score at 0x10
|
||||
// Find the double
|
||||
bool foundDouble = false;
|
||||
for (int k : kids) {
|
||||
if (tree.nodes[k].kind == NodeKind::Double) {
|
||||
QCOMPARE(tree.nodes[k].offset, 0x10);
|
||||
foundDouble = true;
|
||||
}
|
||||
}
|
||||
QVERIFY(foundDouble);
|
||||
}
|
||||
|
||||
void TestImportSource::computedOffsets() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Computed {\n"
|
||||
" uint8_t a;\n"
|
||||
" uint16_t b;\n"
|
||||
" uint32_t c;\n"
|
||||
" uint64_t d;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 4);
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0); // uint8_t at 0
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 1); // uint16_t at 1
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 3); // uint32_t at 3
|
||||
QCOMPARE(tree.nodes[kids[3]].offset, 7); // uint64_t at 7
|
||||
}
|
||||
|
||||
void TestImportSource::mixedOffsetsAutoDetect() {
|
||||
// If any field has a comment offset, all should use comment mode
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Mixed {\n"
|
||||
" uint32_t a; // 0x0\n"
|
||||
" uint32_t b;\n"
|
||||
" uint32_t c; // 0x10\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
// b has no comment offset, in comment mode it gets computed offset 4
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
// c has comment offset 0x10
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 0x10);
|
||||
}
|
||||
|
||||
void TestImportSource::multiStruct() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct A {\n"
|
||||
" int x;\n"
|
||||
"};\n"
|
||||
"struct B {\n"
|
||||
" float y;\n"
|
||||
"};\n"
|
||||
"struct C {\n"
|
||||
" double z;\n"
|
||||
"};\n"
|
||||
));
|
||||
QCOMPARE(countRoots(tree), 3);
|
||||
}
|
||||
|
||||
void TestImportSource::pointerCrossRef() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct A {\n"
|
||||
" int value;\n"
|
||||
"};\n"
|
||||
"struct B {\n"
|
||||
" A* ref;\n"
|
||||
"};\n"
|
||||
));
|
||||
// Find B's pointer field
|
||||
int bIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == QStringLiteral("B") && tree.nodes[i].parentId == 0) {
|
||||
bIdx = i; break;
|
||||
}
|
||||
}
|
||||
QVERIFY(bIdx >= 0);
|
||||
auto kids = childrenOf(tree, tree.nodes[bIdx].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||
// Should point to A
|
||||
int aIdx = tree.indexOfId(tree.nodes[kids[0]].refId);
|
||||
QVERIFY(aIdx >= 0);
|
||||
QCOMPARE(tree.nodes[aIdx].name, QStringLiteral("A"));
|
||||
}
|
||||
|
||||
void TestImportSource::forwardDeclaration() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Bar;\n"
|
||||
"struct Foo {\n"
|
||||
" Bar* pBar;\n"
|
||||
"};\n"
|
||||
"struct Bar {\n"
|
||||
" int val;\n"
|
||||
"};\n"
|
||||
));
|
||||
QCOMPARE(countRoots(tree), 2);
|
||||
// Foo's pBar should resolve to Bar
|
||||
int fooIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == QStringLiteral("Foo") && tree.nodes[i].parentId == 0) {
|
||||
fooIdx = i; break;
|
||||
}
|
||||
}
|
||||
QVERIFY(fooIdx >= 0);
|
||||
auto kids = childrenOf(tree, tree.nodes[fooIdx].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||
}
|
||||
|
||||
void TestImportSource::unionPickFirst() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct WithUnion {\n"
|
||||
" union {\n"
|
||||
" float asFloat;\n"
|
||||
" uint32_t asInt;\n"
|
||||
" };\n"
|
||||
" int after;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// Should have 2 fields: asFloat (first union member) + after
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("asFloat"));
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
|
||||
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||
}
|
||||
|
||||
void TestImportSource::paddingFieldExpansion() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Padded {\n"
|
||||
" uint8_t _pad0000[0x10];\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// 0x10 = 16 bytes, should be 2x Hex64 (best fit)
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Hex64);
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex64);
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 8);
|
||||
}
|
||||
|
||||
void TestImportSource::staticAssertTailPadding() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Sized {\n"
|
||||
" uint32_t x;\n"
|
||||
"};\n"
|
||||
"static_assert(sizeof(Sized) == 0x10, \"Size check\");\n"
|
||||
));
|
||||
// x is 4 bytes, static_assert says 0x10 = 16
|
||||
// Should have tail padding from offset 4 to 16 (12 bytes)
|
||||
int span = tree.structSpan(tree.nodes[0].id);
|
||||
QCOMPARE(span, 0x10);
|
||||
}
|
||||
|
||||
void TestImportSource::embeddedStruct() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Inner {\n"
|
||||
" int a;\n"
|
||||
"};\n"
|
||||
"struct Outer {\n"
|
||||
" Inner embedded;\n"
|
||||
" float after;\n"
|
||||
"};\n"
|
||||
));
|
||||
int outerIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == QStringLiteral("Outer") && tree.nodes[i].parentId == 0) {
|
||||
outerIdx = i; break;
|
||||
}
|
||||
}
|
||||
QVERIFY(outerIdx >= 0);
|
||||
auto kids = childrenOf(tree, tree.nodes[outerIdx].id);
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[kids[0]].structTypeName, QStringLiteral("Inner"));
|
||||
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Float);
|
||||
}
|
||||
|
||||
void TestImportSource::typedefBasic() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"typedef uint32_t MyInt;\n"
|
||||
"struct TD {\n"
|
||||
" MyInt value;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
|
||||
}
|
||||
|
||||
void TestImportSource::constVolatileQualifiers() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Quals {\n"
|
||||
" const uint32_t a;\n"
|
||||
" volatile int32_t b;\n"
|
||||
" const volatile uint8_t c;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
|
||||
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt8);
|
||||
}
|
||||
|
||||
void TestImportSource::structPrefixOnType() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Inner {\n"
|
||||
" int val;\n"
|
||||
"};\n"
|
||||
"struct Outer {\n"
|
||||
" struct Inner member;\n"
|
||||
"};\n"
|
||||
));
|
||||
int outerIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == QStringLiteral("Outer") && tree.nodes[i].parentId == 0) {
|
||||
outerIdx = i; break;
|
||||
}
|
||||
}
|
||||
QVERIFY(outerIdx >= 0);
|
||||
auto kids = childrenOf(tree, tree.nodes[outerIdx].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[kids[0]].structTypeName, QStringLiteral("Inner"));
|
||||
}
|
||||
|
||||
void TestImportSource::bitfieldSkipped() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct BF {\n"
|
||||
" uint32_t normal;\n"
|
||||
" uint32_t bitA : 4;\n"
|
||||
" uint32_t bitB : 12;\n"
|
||||
" uint32_t after;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// Bitfields should be skipped, only normal + after
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||
}
|
||||
|
||||
void TestImportSource::hexArraySizes() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct HexArr {\n"
|
||||
" uint8_t data[0x20];\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||
QCOMPARE(tree.nodes[kids[0]].arrayLen, 0x20);
|
||||
}
|
||||
|
||||
void TestImportSource::windowsStylePEB() {
|
||||
// Test with Windows PEB-style struct (no comment offsets)
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct PEB64 {\n"
|
||||
" BOOLEAN InheritedAddressSpace;\n"
|
||||
" BOOLEAN ReadImageFileExecOptions;\n"
|
||||
" BOOLEAN BeingDebugged;\n"
|
||||
" BOOLEAN BitField;\n"
|
||||
" PVOID Mutant;\n"
|
||||
" PVOID ImageBaseAddress;\n"
|
||||
"};\n"
|
||||
));
|
||||
QCOMPARE(countRoots(tree), 1);
|
||||
QCOMPARE(tree.nodes[0].name, QStringLiteral("PEB64"));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 6);
|
||||
// First 4 are BOOLEAN (UInt8)
|
||||
for (int i = 0; i < 4; i++)
|
||||
QCOMPARE(tree.nodes[kids[i]].kind, NodeKind::UInt8);
|
||||
// Last 2 are PVOID (Pointer64)
|
||||
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Pointer64);
|
||||
}
|
||||
|
||||
void TestImportSource::classKeyword() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"class MyClass {\n"
|
||||
" int value;\n"
|
||||
"};\n"
|
||||
));
|
||||
QCOMPARE(countRoots(tree), 1);
|
||||
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("class"));
|
||||
}
|
||||
|
||||
void TestImportSource::inheritanceSkipped() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct Base {\n"
|
||||
" int a;\n"
|
||||
"};\n"
|
||||
"struct Derived : public Base {\n"
|
||||
" float b;\n"
|
||||
"};\n"
|
||||
));
|
||||
QCOMPARE(countRoots(tree), 2);
|
||||
int derivedIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == QStringLiteral("Derived") && tree.nodes[i].parentId == 0) {
|
||||
derivedIdx = i; break;
|
||||
}
|
||||
}
|
||||
QVERIFY(derivedIdx >= 0);
|
||||
auto kids = childrenOf(tree, tree.nodes[derivedIdx].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||
}
|
||||
|
||||
void TestImportSource::basicRoundTrip() {
|
||||
// Build a simple tree manually, export it, then re-import and compare
|
||||
NodeTree original;
|
||||
{
|
||||
Node s;
|
||||
s.kind = NodeKind::Struct;
|
||||
s.name = QStringLiteral("RoundTrip");
|
||||
s.structTypeName = QStringLiteral("RoundTrip");
|
||||
s.parentId = 0;
|
||||
s.offset = 0;
|
||||
int sIdx = original.addNode(s);
|
||||
uint64_t sId = original.nodes[sIdx].id;
|
||||
|
||||
Node f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = QStringLiteral("field_a");
|
||||
f1.parentId = sId;
|
||||
f1.offset = 0;
|
||||
original.addNode(f1);
|
||||
|
||||
Node f2;
|
||||
f2.kind = NodeKind::Float;
|
||||
f2.name = QStringLiteral("field_b");
|
||||
f2.parentId = sId;
|
||||
f2.offset = 4;
|
||||
original.addNode(f2);
|
||||
|
||||
Node f3;
|
||||
f3.kind = NodeKind::UInt64;
|
||||
f3.name = QStringLiteral("field_c");
|
||||
f3.parentId = sId;
|
||||
f3.offset = 8;
|
||||
original.addNode(f3);
|
||||
}
|
||||
|
||||
// Create source text that matches what generator would produce
|
||||
QString source = QStringLiteral(
|
||||
"struct RoundTrip {\n"
|
||||
" uint32_t field_a; // 0x0\n"
|
||||
" float field_b; // 0x4\n"
|
||||
" uint64_t field_c; // 0x8\n"
|
||||
"};\n"
|
||||
"static_assert(sizeof(RoundTrip) == 0x10, \"Size mismatch\");\n"
|
||||
);
|
||||
|
||||
NodeTree reimported = importFromSource(source);
|
||||
QCOMPARE(countRoots(reimported), 1);
|
||||
QCOMPARE(reimported.nodes[0].name, QStringLiteral("RoundTrip"));
|
||||
|
||||
auto origKids = childrenOf(original, original.nodes[0].id);
|
||||
auto reimpKids = childrenOf(reimported, reimported.nodes[0].id);
|
||||
|
||||
// Compare field count (reimported may have extra padding nodes from static_assert)
|
||||
// Check that the first 3 fields match
|
||||
QVERIFY(reimpKids.size() >= 3);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
QCOMPARE(reimported.nodes[reimpKids[i]].kind, original.nodes[origKids[i]].kind);
|
||||
QCOMPARE(reimported.nodes[reimpKids[i]].name, original.nodes[origKids[i]].name);
|
||||
QCOMPARE(reimported.nodes[reimpKids[i]].offset, original.nodes[origKids[i]].offset);
|
||||
}
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestImportSource)
|
||||
#include "test_import_source.moc"
|
||||
70
tests/test_import_xml.cpp
Normal file
70
tests/test_import_xml.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include <QtTest/QtTest>
|
||||
#include "core.h"
|
||||
#include "import_reclass_xml.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestImportXml : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void importSmallXml();
|
||||
};
|
||||
|
||||
void TestImportXml::importSmallXml() {
|
||||
// Create a minimal XML in a temp file and test parsing
|
||||
QTemporaryFile tmp;
|
||||
tmp.setAutoRemove(true);
|
||||
QVERIFY(tmp.open());
|
||||
tmp.write(R"(<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ReClass>
|
||||
<!--ReClassEx-->
|
||||
<Class Name="TestClass" Type="28" Comment="" Offset="0" strOffset="0" Code="">
|
||||
<Node Name="vtable" Type="9" Size="8" bHidden="false" Comment=""/>
|
||||
<Node Name="health" Type="13" Size="4" bHidden="false" Comment=""/>
|
||||
<Node Name="name" Type="18" Size="32" bHidden="false" Comment=""/>
|
||||
<Node Name="position" Type="23" Size="12" bHidden="false" Comment=""/>
|
||||
<Node Name="pNext" Type="8" Size="8" bHidden="false" Comment="" Pointer="TestClass"/>
|
||||
</Class>
|
||||
</ReClass>
|
||||
)");
|
||||
tmp.flush();
|
||||
|
||||
QString error;
|
||||
NodeTree tree = importReclassXml(tmp.fileName(), &error);
|
||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
||||
|
||||
// Should have 1 root struct + 5 children = 6 nodes
|
||||
QCOMPARE(tree.nodes.size(), 6);
|
||||
|
||||
// Root struct
|
||||
QCOMPARE(tree.nodes[0].kind, NodeKind::Struct);
|
||||
QCOMPARE(tree.nodes[0].name, QStringLiteral("TestClass"));
|
||||
|
||||
// vtable = Int64
|
||||
QCOMPARE(tree.nodes[1].kind, NodeKind::Int64);
|
||||
QCOMPARE(tree.nodes[1].name, QStringLiteral("vtable"));
|
||||
QCOMPARE(tree.nodes[1].offset, 0);
|
||||
|
||||
// health = Float
|
||||
QCOMPARE(tree.nodes[2].kind, NodeKind::Float);
|
||||
QCOMPARE(tree.nodes[2].name, QStringLiteral("health"));
|
||||
QCOMPARE(tree.nodes[2].offset, 8);
|
||||
|
||||
// name = UTF8 with strLen=32
|
||||
QCOMPARE(tree.nodes[3].kind, NodeKind::UTF8);
|
||||
QCOMPARE(tree.nodes[3].strLen, 32);
|
||||
QCOMPARE(tree.nodes[3].offset, 12);
|
||||
|
||||
// position = Vec3
|
||||
QCOMPARE(tree.nodes[4].kind, NodeKind::Vec3);
|
||||
QCOMPARE(tree.nodes[4].offset, 44);
|
||||
|
||||
// pNext = Pointer64 with resolved refId
|
||||
QCOMPARE(tree.nodes[5].kind, NodeKind::Pointer64);
|
||||
QCOMPARE(tree.nodes[5].name, QStringLiteral("pNext"));
|
||||
QVERIFY(tree.nodes[5].refId != 0);
|
||||
QCOMPARE(tree.nodes[5].refId, tree.nodes[0].id); // points to TestClass
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestImportXml)
|
||||
#include "test_import_xml.moc"
|
||||
@@ -304,39 +304,6 @@ private slots:
|
||||
QVERIFY(result.contains("float speed;"));
|
||||
}
|
||||
|
||||
void testGenerator_typeAliases_padding() {
|
||||
// Padding gap and tail padding should use aliased uint8_t
|
||||
NodeTree tree;
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "PadTest";
|
||||
root.structTypeName = "PadTest";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node f1;
|
||||
f1.kind = NodeKind::UInt32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
Node f2;
|
||||
f2.kind = NodeKind::UInt32;
|
||||
f2.name = "b";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 8; // gap of 4 bytes at offset 4
|
||||
tree.addNode(f2);
|
||||
|
||||
QHash<NodeKind, QString> aliases;
|
||||
aliases[NodeKind::Padding] = "BYTE";
|
||||
|
||||
QString result = renderCpp(tree, rootId, &aliases);
|
||||
// Padding gap should use the alias
|
||||
QVERIFY(result.contains("BYTE _pad"));
|
||||
}
|
||||
|
||||
void testGenerator_typeAliases_array() {
|
||||
// Array element type should use alias
|
||||
NodeTree tree;
|
||||
@@ -547,134 +514,92 @@ private slots:
|
||||
void testWorkspace_simpleTree() {
|
||||
auto tree = makeSimpleTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "TestProject.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "TestProject.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
// 1 top-level item (the project)
|
||||
// Single "Project" root
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QStandardItem* project = model.item(0);
|
||||
QCOMPARE(project->text(), QString("TestProject.rcx"));
|
||||
QCOMPARE(project->text(), QString("Project"));
|
||||
|
||||
// Project has 1 child: the Player struct
|
||||
// 1 type directly under Project: Player (no member fields)
|
||||
QCOMPARE(project->rowCount(), 1);
|
||||
QStandardItem* player = project->child(0);
|
||||
QVERIFY(player->text().contains("Player"));
|
||||
QVERIFY(player->text().contains("struct"));
|
||||
|
||||
// Player struct has 2 children: health, speed
|
||||
QCOMPARE(player->rowCount(), 2);
|
||||
QVERIFY(player->child(0)->text().contains("health"));
|
||||
QVERIFY(player->child(1)->text().contains("speed"));
|
||||
QVERIFY(project->child(0)->text().contains("Player"));
|
||||
QVERIFY(project->child(0)->text().contains("struct"));
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_twoRootTree() {
|
||||
auto tree = makeTwoRootTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "TwoRoot.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "TwoRoot.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QStandardItem* project = model.item(0);
|
||||
|
||||
// 2 root struct children: Alpha and Bravo
|
||||
// 2 types sorted alphabetically: Alpha, Bravo (no field children)
|
||||
QCOMPARE(project->rowCount(), 2);
|
||||
QVERIFY(project->child(0)->text().contains("Alpha"));
|
||||
QVERIFY(project->child(1)->text().contains("Bravo"));
|
||||
|
||||
// Each has 1 field child
|
||||
QCOMPARE(project->child(0)->rowCount(), 1);
|
||||
QVERIFY(project->child(0)->child(0)->text().contains("flagsA"));
|
||||
QCOMPARE(project->child(1)->rowCount(), 1);
|
||||
QVERIFY(project->child(1)->child(0)->text().contains("flagsB"));
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_rootCount() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
QCOMPARE(project->rowCount(), 3); // Pet, Cat, Ball
|
||||
QCOMPARE(project->rowCount(), 3); // Ball, Cat, Pet (sorted)
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_petChildren() {
|
||||
void testWorkspace_richTree_sorted() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* pet = model.item(0)->child(0);
|
||||
QVERIFY(pet->text().contains("Pet"));
|
||||
// Pet has 2 non-hex children: name (UTF8), owner (Pointer64)
|
||||
QCOMPARE(pet->rowCount(), 2);
|
||||
QVERIFY(pet->child(0)->text().contains("name"));
|
||||
QVERIFY(pet->child(1)->text().contains("owner"));
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_catNesting() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
|
||||
QStandardItem* cat = model.item(0)->child(1);
|
||||
QVERIFY(cat->text().contains("Cat"));
|
||||
|
||||
// Find the nested "Pet" struct child (base)
|
||||
QStandardItem* base = nullptr;
|
||||
for (int i = 0; i < cat->rowCount(); i++) {
|
||||
if (cat->child(i)->text().contains("Pet") &&
|
||||
cat->child(i)->text().contains("struct")) {
|
||||
base = cat->child(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(base != nullptr, "Cat should have a nested Pet struct child");
|
||||
|
||||
// base has structId set
|
||||
QVERIFY(base->data(Qt::UserRole + 1).isValid());
|
||||
|
||||
// base should have its own children (name + owner)
|
||||
QCOMPARE(base->rowCount(), 2);
|
||||
}
|
||||
|
||||
void testWorkspace_richTree_ballChildren() {
|
||||
auto tree = makeRichTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Rich.rcx");
|
||||
|
||||
QStandardItem* ball = model.item(0)->child(2);
|
||||
QVERIFY(ball->text().contains("Ball"));
|
||||
|
||||
// Ball has 3 non-hex children: speed, position, color
|
||||
QCOMPARE(ball->rowCount(), 3);
|
||||
QVERIFY(ball->child(0)->text().contains("speed"));
|
||||
QVERIFY(ball->child(1)->text().contains("position"));
|
||||
QVERIFY(ball->child(2)->text().contains("color"));
|
||||
QStandardItem* project = model.item(0);
|
||||
// Sorted alphabetically: Ball, Cat, Pet
|
||||
QVERIFY(project->child(0)->text().contains("Ball"));
|
||||
QVERIFY(project->child(1)->text().contains("Cat"));
|
||||
QVERIFY(project->child(2)->text().contains("Pet"));
|
||||
// No member fields under type nodes
|
||||
QCOMPARE(project->child(0)->rowCount(), 0);
|
||||
QCOMPARE(project->child(1)->rowCount(), 0);
|
||||
QCOMPARE(project->child(2)->rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_emptyTree() {
|
||||
NodeTree tree;
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Empty.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Empty.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
// Still has the "Project" root, just no children
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
QCOMPARE(model.item(0)->text(), QString("Project"));
|
||||
QCOMPARE(model.item(0)->rowCount(), 0);
|
||||
}
|
||||
|
||||
void testWorkspace_structIdRole() {
|
||||
auto tree = makeSimpleTree();
|
||||
QStandardItemModel model;
|
||||
buildWorkspaceModel(&model, tree, "Test.rcx");
|
||||
QVector<TabInfo> tabs = {{ &tree, "Test.rcx", nullptr }};
|
||||
buildProjectExplorer(&model, tabs);
|
||||
|
||||
QStandardItem* project = model.item(0);
|
||||
// Project item should NOT have structId
|
||||
QVERIFY(!project->data(Qt::UserRole + 1).isValid());
|
||||
// Project root has kGroupSentinel
|
||||
QCOMPARE(project->data(Qt::UserRole + 1).toULongLong(), kGroupSentinel);
|
||||
|
||||
// Player struct should have structId
|
||||
// Player type item should have structId
|
||||
QStandardItem* player = project->child(0);
|
||||
QVERIFY(player->data(Qt::UserRole + 1).isValid());
|
||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
|
||||
|
||||
// health field should NOT have structId
|
||||
QStandardItem* health = player->child(0);
|
||||
QVERIFY(!health->data(Qt::UserRole + 1).isValid());
|
||||
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() != kGroupSentinel);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
|
||||
291
tests/test_options_dialog.cpp
Normal file
291
tests/test_options_dialog.cpp
Normal file
@@ -0,0 +1,291 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
#include <QTreeWidget>
|
||||
#include <QStackedWidget>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QPushButton>
|
||||
#include <QGroupBox>
|
||||
#include <QLineEdit>
|
||||
#include <QSpinBox>
|
||||
#include <QLabel>
|
||||
#include "optionsdialog.h"
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
// Helper: apply the global palette the same way main.cpp does
|
||||
static void applyGlobalTheme(const Theme& theme) {
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.background);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
pal.setColor(QPalette::Base, theme.background);
|
||||
pal.setColor(QPalette::AlternateBase, theme.surface);
|
||||
pal.setColor(QPalette::Text, theme.text);
|
||||
pal.setColor(QPalette::Button, theme.button);
|
||||
pal.setColor(QPalette::ButtonText, theme.text);
|
||||
pal.setColor(QPalette::Highlight, theme.selection);
|
||||
pal.setColor(QPalette::HighlightedText, theme.text);
|
||||
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::ToolTipText, theme.text);
|
||||
pal.setColor(QPalette::Mid, theme.border);
|
||||
pal.setColor(QPalette::Dark, theme.background);
|
||||
pal.setColor(QPalette::Light, theme.textFaint);
|
||||
pal.setColor(QPalette::Link, theme.indHoverSpan);
|
||||
|
||||
pal.setColor(QPalette::Disabled, QPalette::WindowText, theme.textMuted);
|
||||
pal.setColor(QPalette::Disabled, QPalette::Text, theme.textMuted);
|
||||
pal.setColor(QPalette::Disabled, QPalette::ButtonText, theme.textMuted);
|
||||
pal.setColor(QPalette::Disabled, QPalette::HighlightedText, theme.textMuted);
|
||||
pal.setColor(QPalette::Disabled, QPalette::Light, theme.background);
|
||||
|
||||
qApp->setPalette(pal);
|
||||
qApp->setStyleSheet(QString());
|
||||
}
|
||||
|
||||
class TestOptionsDialog : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
|
||||
void initTestCase() {
|
||||
// Apply theme palette so dialog inherits real colors
|
||||
auto& tm = ThemeManager::instance();
|
||||
applyGlobalTheme(tm.current());
|
||||
}
|
||||
|
||||
void dialogCreatesAllWidgets() {
|
||||
OptionsResult defaults;
|
||||
defaults.themeIndex = 0;
|
||||
defaults.fontName = "JetBrains Mono";
|
||||
defaults.menuBarTitleCase = true;
|
||||
defaults.safeMode = false;
|
||||
defaults.autoStartMcp = false;
|
||||
|
||||
OptionsDialog dlg(defaults);
|
||||
|
||||
// Core widgets exist
|
||||
auto* tree = dlg.findChild<QTreeWidget*>();
|
||||
QVERIFY(tree);
|
||||
auto* pages = dlg.findChild<QStackedWidget*>();
|
||||
QVERIFY(pages);
|
||||
QCOMPARE(pages->count(), 3);
|
||||
|
||||
auto* themeCombo = dlg.findChild<QComboBox*>("themeCombo");
|
||||
QVERIFY(themeCombo);
|
||||
QVERIFY(themeCombo->count() >= 3);
|
||||
|
||||
auto* fontCombo = dlg.findChild<QComboBox*>("fontCombo");
|
||||
QVERIFY(fontCombo);
|
||||
QCOMPARE(fontCombo->count(), 2);
|
||||
|
||||
auto* showIconCheck = dlg.findChild<QCheckBox*>();
|
||||
QVERIFY(showIconCheck);
|
||||
|
||||
auto* buttons = dlg.findChild<QDialogButtonBox*>();
|
||||
QVERIFY(buttons);
|
||||
QVERIFY(buttons->button(QDialogButtonBox::Ok));
|
||||
QVERIFY(buttons->button(QDialogButtonBox::Cancel));
|
||||
}
|
||||
|
||||
void resultReflectsInput() {
|
||||
OptionsResult input;
|
||||
input.themeIndex = 1;
|
||||
input.fontName = "Consolas";
|
||||
input.menuBarTitleCase = false;
|
||||
input.safeMode = true;
|
||||
input.autoStartMcp = true;
|
||||
|
||||
OptionsDialog dlg(input);
|
||||
auto r = dlg.result();
|
||||
|
||||
QCOMPARE(r.themeIndex, 1);
|
||||
QCOMPARE(r.fontName, QString("Consolas"));
|
||||
QCOMPARE(r.menuBarTitleCase, false);
|
||||
QCOMPARE(r.safeMode, true);
|
||||
QCOMPARE(r.autoStartMcp, true);
|
||||
}
|
||||
|
||||
void noStyleSheetOnDialog() {
|
||||
OptionsResult defaults;
|
||||
OptionsDialog dlg(defaults);
|
||||
|
||||
// Dialog itself must have no stylesheet override
|
||||
QVERIFY(dlg.styleSheet().isEmpty());
|
||||
|
||||
// Combo boxes must have no stylesheet override
|
||||
auto* themeCombo = dlg.findChild<QComboBox*>("themeCombo");
|
||||
QVERIFY(themeCombo->styleSheet().isEmpty());
|
||||
auto* fontCombo = dlg.findChild<QComboBox*>("fontCombo");
|
||||
QVERIFY(fontCombo->styleSheet().isEmpty());
|
||||
|
||||
// No child widget should have a stylesheet set
|
||||
for (auto* child : dlg.findChildren<QWidget*>()) {
|
||||
QVERIFY2(child->styleSheet().isEmpty(),
|
||||
qPrintable(QString("Widget %1 (%2) has unexpected stylesheet: %3")
|
||||
.arg(child->objectName(),
|
||||
child->metaObject()->className(),
|
||||
child->styleSheet())));
|
||||
}
|
||||
}
|
||||
|
||||
void highlightColorDiffersFromBackground() {
|
||||
// Verify the palette Highlight is distinguishable from Window background
|
||||
// This is the root cause of broken hover: if they're the same, hover is invisible
|
||||
auto& tm = ThemeManager::instance();
|
||||
const auto themes = tm.themes();
|
||||
for (const auto& theme : themes) {
|
||||
QVERIFY2(theme.selection != theme.background,
|
||||
qPrintable(QString("Theme '%1': selection == background (%2)")
|
||||
.arg(theme.name, theme.background.name())));
|
||||
}
|
||||
}
|
||||
|
||||
void paletteHighlightIsSelection() {
|
||||
// After applying theme, QPalette::Highlight must be theme.selection (not theme.hover)
|
||||
auto& tm = ThemeManager::instance();
|
||||
const auto& theme = tm.current();
|
||||
applyGlobalTheme(theme);
|
||||
|
||||
QPalette pal = qApp->palette();
|
||||
QCOMPARE(pal.color(QPalette::Highlight), theme.selection);
|
||||
}
|
||||
|
||||
void treePageSwitching() {
|
||||
OptionsResult defaults;
|
||||
OptionsDialog dlg(defaults);
|
||||
|
||||
auto* tree = dlg.findChild<QTreeWidget*>();
|
||||
auto* pages = dlg.findChild<QStackedWidget*>();
|
||||
QVERIFY(tree && pages);
|
||||
|
||||
// General is selected by default -> page 0
|
||||
QCOMPARE(pages->currentIndex(), 0);
|
||||
|
||||
// Find "AI Features" item and select it
|
||||
auto* envItem = tree->topLevelItem(0);
|
||||
QVERIFY(envItem);
|
||||
QTreeWidgetItem* aiItem = nullptr;
|
||||
for (int i = 0; i < envItem->childCount(); ++i) {
|
||||
if (envItem->child(i)->text(0) == "AI Features") {
|
||||
aiItem = envItem->child(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(aiItem);
|
||||
tree->setCurrentItem(aiItem);
|
||||
QCOMPARE(pages->currentIndex(), 1);
|
||||
|
||||
// Switch back to General
|
||||
QTreeWidgetItem* generalItem = nullptr;
|
||||
for (int i = 0; i < envItem->childCount(); ++i) {
|
||||
if (envItem->child(i)->text(0) == "General") {
|
||||
generalItem = envItem->child(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(generalItem);
|
||||
tree->setCurrentItem(generalItem);
|
||||
QCOMPARE(pages->currentIndex(), 0);
|
||||
}
|
||||
|
||||
void searchFilterHidesItems() {
|
||||
OptionsResult defaults;
|
||||
OptionsDialog dlg(defaults);
|
||||
|
||||
auto* search = dlg.findChild<QLineEdit*>();
|
||||
auto* tree = dlg.findChild<QTreeWidget*>();
|
||||
QVERIFY(search && tree);
|
||||
|
||||
auto* envItem = tree->topLevelItem(0);
|
||||
QVERIFY(envItem);
|
||||
|
||||
// All children visible initially
|
||||
for (int i = 0; i < envItem->childCount(); ++i)
|
||||
QVERIFY(!envItem->child(i)->isHidden());
|
||||
|
||||
// Search for "MCP" - should hide General, show AI Features
|
||||
search->setText("MCP");
|
||||
QTreeWidgetItem* generalItem = nullptr;
|
||||
QTreeWidgetItem* aiItem = nullptr;
|
||||
for (int i = 0; i < envItem->childCount(); ++i) {
|
||||
auto* child = envItem->child(i);
|
||||
if (child->text(0) == "General") generalItem = child;
|
||||
if (child->text(0) == "AI Features") aiItem = child;
|
||||
}
|
||||
QVERIFY(generalItem && aiItem);
|
||||
QVERIFY(generalItem->isHidden());
|
||||
QVERIFY(!aiItem->isHidden());
|
||||
|
||||
// Clear search - all visible again
|
||||
search->setText("");
|
||||
QVERIFY(!generalItem->isHidden());
|
||||
QVERIFY(!aiItem->isHidden());
|
||||
}
|
||||
|
||||
void refreshRateSpinBoxExists() {
|
||||
OptionsResult defaults;
|
||||
defaults.refreshMs = 660;
|
||||
OptionsDialog dlg(defaults);
|
||||
|
||||
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
|
||||
QVERIFY(spin);
|
||||
QCOMPARE(spin->value(), 660);
|
||||
QCOMPARE(spin->minimum(), 1);
|
||||
QCOMPARE(spin->maximum(), 60000);
|
||||
}
|
||||
|
||||
void refreshRateResultReflectsInput() {
|
||||
OptionsResult input;
|
||||
input.refreshMs = 200;
|
||||
OptionsDialog dlg(input);
|
||||
|
||||
auto r = dlg.result();
|
||||
QCOMPARE(r.refreshMs, 200);
|
||||
|
||||
// Change via spin box
|
||||
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
|
||||
QVERIFY(spin);
|
||||
spin->setValue(100);
|
||||
r = dlg.result();
|
||||
QCOMPARE(r.refreshMs, 100);
|
||||
}
|
||||
|
||||
void refreshRateClampsMin() {
|
||||
OptionsResult input;
|
||||
input.refreshMs = 0; // below minimum
|
||||
OptionsDialog dlg(input);
|
||||
|
||||
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
|
||||
QVERIFY(spin);
|
||||
// QSpinBox clamps to minimum
|
||||
QCOMPARE(spin->value(), 1);
|
||||
}
|
||||
|
||||
void dialogInheritsPalette() {
|
||||
auto& tm = ThemeManager::instance();
|
||||
const auto& theme = tm.current();
|
||||
applyGlobalTheme(theme);
|
||||
|
||||
OptionsResult defaults;
|
||||
OptionsDialog dlg(defaults);
|
||||
dlg.show();
|
||||
QTest::qWaitForWindowExposed(&dlg);
|
||||
|
||||
// Dialog's effective palette should match the app palette
|
||||
QPalette dlgPal = dlg.palette();
|
||||
QPalette appPal = qApp->palette();
|
||||
|
||||
QCOMPARE(dlgPal.color(QPalette::Window), appPal.color(QPalette::Window));
|
||||
QCOMPARE(dlgPal.color(QPalette::WindowText), appPal.color(QPalette::WindowText));
|
||||
QCOMPARE(dlgPal.color(QPalette::Highlight), appPal.color(QPalette::Highlight));
|
||||
QCOMPARE(dlgPal.color(QPalette::Button), appPal.color(QPalette::Button));
|
||||
QCOMPARE(dlgPal.color(QPalette::ButtonText), appPal.color(QPalette::ButtonText));
|
||||
|
||||
// Highlight must be visible against background
|
||||
QVERIFY(dlgPal.color(QPalette::Highlight) != dlgPal.color(QPalette::Window));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestOptionsDialog)
|
||||
#include "test_options_dialog.moc"
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <QLineEdit>
|
||||
#include <QListView>
|
||||
#include <QStringListModel>
|
||||
#include <QLabel>
|
||||
#include <QFrame>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include "controller.h"
|
||||
#include "typeselectorpopup.h"
|
||||
@@ -19,7 +21,7 @@ Q_DECLARE_METATYPE(rcx::TypeEntry)
|
||||
using namespace rcx;
|
||||
|
||||
static void buildTwoRootTree(NodeTree& tree) {
|
||||
tree.baseAddress = 0x1000;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node a;
|
||||
a.kind = NodeKind::Struct;
|
||||
@@ -60,7 +62,7 @@ private slots:
|
||||
// ── Chevron span detection ──
|
||||
|
||||
void testChevronSpanDetected() {
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {");
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {");
|
||||
ColumnSpan span = commandRowChevronSpan(text);
|
||||
QVERIFY(span.valid);
|
||||
QCOMPARE(span.start, 0);
|
||||
@@ -77,7 +79,7 @@ private slots:
|
||||
// ── Existing spans unbroken by chevron prefix ──
|
||||
|
||||
void testSpansWithPrefix() {
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {");
|
||||
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {");
|
||||
|
||||
ColumnSpan src = commandRowSrcSpan(text);
|
||||
QVERIFY(src.valid);
|
||||
@@ -198,6 +200,127 @@ private slots:
|
||||
}
|
||||
}
|
||||
|
||||
// ── Isolate first-show cost with different window flags ──
|
||||
|
||||
void benchmarkFirstShow() {
|
||||
auto ms = [](qint64 ns) { return QString::number(ns / 1000000.0, 'f', 2); };
|
||||
|
||||
struct FlagTest {
|
||||
const char* name;
|
||||
Qt::WindowFlags flags;
|
||||
};
|
||||
FlagTest tests[] = {
|
||||
{"Qt::Popup|Frameless", Qt::Popup | Qt::FramelessWindowHint},
|
||||
{"Qt::Tool|Frameless", Qt::Tool | Qt::FramelessWindowHint},
|
||||
{"Qt::ToolTip", Qt::ToolTip},
|
||||
{"Qt::Window|Frameless", Qt::Window | Qt::FramelessWindowHint},
|
||||
{"Qt::Popup|Frameless (2nd)", Qt::Popup | Qt::FramelessWindowHint},
|
||||
};
|
||||
|
||||
for (const auto& test : tests) {
|
||||
auto* f = new QFrame(nullptr, test.flags);
|
||||
f->resize(300, 400);
|
||||
|
||||
QElapsedTimer t; t.start();
|
||||
f->show();
|
||||
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t2 = t.nsecsElapsed();
|
||||
f->hide();
|
||||
QApplication::processEvents();
|
||||
|
||||
t.restart();
|
||||
f->show();
|
||||
qint64 t3 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t4 = t.nsecsElapsed();
|
||||
f->hide();
|
||||
QApplication::processEvents();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug().noquote() << QString("=== %1 ===").arg(test.name);
|
||||
qDebug().noquote() << QString(" 1st: show=%1ms events=%2ms | 2nd: show=%3ms events=%4ms")
|
||||
.arg(ms(t1)).arg(ms(t2)).arg(ms(t3)).arg(ms(t4));
|
||||
delete f;
|
||||
}
|
||||
|
||||
// TypeSelectorPopup: cold vs after warmUp
|
||||
{
|
||||
auto* popup = new TypeSelectorPopup();
|
||||
TypeEntry dummy;
|
||||
dummy.entryKind = TypeEntry::Primitive;
|
||||
dummy.primitiveKind = NodeKind::Hex8;
|
||||
dummy.displayName = "test";
|
||||
popup->setTypes({dummy});
|
||||
|
||||
QElapsedTimer t; t.start();
|
||||
popup->show();
|
||||
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t2 = t.nsecsElapsed();
|
||||
popup->hide();
|
||||
QApplication::processEvents();
|
||||
|
||||
t.restart();
|
||||
popup->show();
|
||||
qint64 t3 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t4 = t.nsecsElapsed();
|
||||
popup->hide();
|
||||
QApplication::processEvents();
|
||||
|
||||
qDebug() << "";
|
||||
qDebug().noquote() << QString("=== TypeSelectorPopup (cold, Qt::Popup) ===");
|
||||
qDebug().noquote() << QString(" 1st: show=%1ms events=%2ms | 2nd: show=%3ms events=%4ms")
|
||||
.arg(ms(t1)).arg(ms(t2)).arg(ms(t3)).arg(ms(t4));
|
||||
delete popup;
|
||||
}
|
||||
|
||||
// Clean order test: dummy popup with children FIRST, then TypeSelectorPopup
|
||||
qDebug() << "";
|
||||
qDebug() << "=== CLEAN: dummy popup first, then TypeSelectorPopup ===";
|
||||
{
|
||||
auto* dummy = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint);
|
||||
dummy->resize(300, 400);
|
||||
auto* dLay = new QVBoxLayout(dummy);
|
||||
dLay->addWidget(new QLabel("dummy"));
|
||||
dLay->addWidget(new QLineEdit);
|
||||
auto* dModel = new QStringListModel(dummy);
|
||||
QStringList dItems; for (int i = 0; i < 10; i++) dItems << "x";
|
||||
dModel->setStringList(dItems);
|
||||
auto* dLv = new QListView; dLv->setModel(dModel);
|
||||
dLay->addWidget(dLv);
|
||||
|
||||
QElapsedTimer t; t.start();
|
||||
dummy->show();
|
||||
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t2 = t.nsecsElapsed();
|
||||
dummy->hide();
|
||||
QApplication::processEvents();
|
||||
qDebug().noquote() << QString(" Dummy popup: show=%1ms events=%2ms").arg(ms(t1)).arg(ms(t2));
|
||||
delete dummy;
|
||||
}
|
||||
{
|
||||
auto* popup = new TypeSelectorPopup();
|
||||
TypeEntry e;
|
||||
e.entryKind = TypeEntry::Primitive;
|
||||
e.primitiveKind = NodeKind::Hex8;
|
||||
e.displayName = "test";
|
||||
popup->setTypes({e});
|
||||
popup->resize(300, 400);
|
||||
QElapsedTimer t; t.start();
|
||||
popup->show();
|
||||
qint64 t1 = t.nsecsElapsed(); t.restart();
|
||||
QApplication::processEvents();
|
||||
qint64 t2 = t.nsecsElapsed();
|
||||
popup->hide();
|
||||
QApplication::processEvents();
|
||||
qDebug().noquote() << QString(" TypeSelectorPopup (after dummy): show=%1ms events=%2ms").arg(ms(t1)).arg(ms(t2));
|
||||
delete popup;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Popup data model ──
|
||||
|
||||
void testPopupListsRootStructs() {
|
||||
@@ -613,6 +736,230 @@ private slots:
|
||||
QVERIFY(listView);
|
||||
QVERIFY(listView->model()->rowCount() > 2);
|
||||
}
|
||||
// ── FieldType popup: primitive with [n] creates an array ──
|
||||
|
||||
void testFieldTypePrimitiveArrayCreation() {
|
||||
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);
|
||||
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32);
|
||||
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
|
||||
|
||||
// Simulate the primitive-array path of applyTypePopupResult:
|
||||
// beginMacro → changeNodeKind(Array) → ChangeArrayMeta → endMacro
|
||||
doc->undoStack.beginMacro(QStringLiteral("Change to primitive array"));
|
||||
ctrl->changeNodeKind(xIdx, NodeKind::Array);
|
||||
xIdx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(xIdx >= 0);
|
||||
doc->undoStack.push(new RcxCommand(ctrl,
|
||||
cmd::ChangeArrayMeta{xNodeId, doc->tree.nodes[xIdx].elementKind,
|
||||
NodeKind::Int32,
|
||||
doc->tree.nodes[xIdx].arrayLen, 4}));
|
||||
doc->undoStack.endMacro();
|
||||
QApplication::processEvents();
|
||||
|
||||
// Node should now be an Array
|
||||
xIdx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(xIdx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Array);
|
||||
QCOMPARE(doc->tree.nodes[xIdx].elementKind, NodeKind::Int32);
|
||||
QCOMPARE(doc->tree.nodes[xIdx].arrayLen, 4);
|
||||
|
||||
// Single undo reverses the entire macro
|
||||
doc->undoStack.undo();
|
||||
QApplication::processEvents();
|
||||
xIdx = doc->tree.indexOfId(xNodeId);
|
||||
QVERIFY(xIdx >= 0);
|
||||
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32);
|
||||
|
||||
delete ctrl;
|
||||
delete splitter;
|
||||
delete doc;
|
||||
}
|
||||
// ── Test: SVG icon and gutter scale with font size ──
|
||||
|
||||
void testDelegateIconScalesWithFont() {
|
||||
// Create a popup and set two different font sizes.
|
||||
// The delegate sizeHint row height should scale with font.
|
||||
TypeSelectorPopup popup;
|
||||
|
||||
TypeEntry prim;
|
||||
prim.entryKind = TypeEntry::Primitive;
|
||||
prim.primitiveKind = NodeKind::Int32;
|
||||
prim.displayName = QStringLiteral("int32_t");
|
||||
|
||||
TypeEntry comp;
|
||||
comp.entryKind = TypeEntry::Composite;
|
||||
comp.structId = 100;
|
||||
comp.displayName = QStringLiteral("TestStruct");
|
||||
comp.classKeyword = QStringLiteral("struct");
|
||||
|
||||
// Small font
|
||||
QFont small(QStringLiteral("Consolas"), 9);
|
||||
popup.setFont(small);
|
||||
popup.setTypes({prim, comp});
|
||||
popup.popup(QPoint(-9999, -9999)); // offscreen
|
||||
QApplication::processEvents();
|
||||
|
||||
auto* listView = popup.findChild<QListView*>();
|
||||
QVERIFY(listView);
|
||||
auto* delegate = listView->itemDelegate();
|
||||
QVERIFY(delegate);
|
||||
|
||||
// Find first non-section row for consistent measurement
|
||||
int dataRow = -1;
|
||||
for (int i = 0; i < listView->model()->rowCount(); i++) {
|
||||
QSize h = delegate->sizeHint(QStyleOptionViewItem(), listView->model()->index(i, 0));
|
||||
// Non-section rows are taller (font.height + 8 vs + 2)
|
||||
if (h.height() > QFontMetrics(small).height() + 4) { dataRow = i; break; }
|
||||
}
|
||||
QVERIFY2(dataRow >= 0, "Should find a non-section row");
|
||||
|
||||
QSize smallHint = delegate->sizeHint(QStyleOptionViewItem(), listView->model()->index(dataRow, 0));
|
||||
popup.hide();
|
||||
|
||||
// Large font (simulates zoomed editor)
|
||||
QFont large(QStringLiteral("Consolas"), 18);
|
||||
popup.setFont(large);
|
||||
popup.setTypes({prim, comp});
|
||||
popup.popup(QPoint(-9999, -9999));
|
||||
QApplication::processEvents();
|
||||
|
||||
QSize largeHint = delegate->sizeHint(QStyleOptionViewItem(), listView->model()->index(dataRow, 0));
|
||||
popup.hide();
|
||||
|
||||
// Large font should produce taller rows than small font
|
||||
QVERIFY2(largeHint.height() > smallHint.height(),
|
||||
qPrintable(QString("Large hint %1 should be > small hint %2")
|
||||
.arg(largeHint.height()).arg(smallHint.height())));
|
||||
|
||||
// The ratio should roughly match the font size ratio (18/9 = 2x)
|
||||
double ratio = double(largeHint.height()) / double(smallHint.height());
|
||||
QVERIFY2(ratio > 1.4, qPrintable(QString("Row height ratio %1 should be > 1.4").arg(ratio)));
|
||||
}
|
||||
|
||||
void testPopupWidthScalesWithFont() {
|
||||
TypeSelectorPopup popup;
|
||||
|
||||
TypeEntry comp;
|
||||
comp.entryKind = TypeEntry::Composite;
|
||||
comp.structId = 100;
|
||||
comp.displayName = QStringLiteral("MyLongStructName");
|
||||
comp.classKeyword = QStringLiteral("struct");
|
||||
popup.setTypes({comp});
|
||||
|
||||
// Small font
|
||||
QFont small(QStringLiteral("Consolas"), 9);
|
||||
popup.setFont(small);
|
||||
popup.popup(QPoint(-9999, -9999));
|
||||
QApplication::processEvents();
|
||||
int smallW = popup.width();
|
||||
popup.hide();
|
||||
|
||||
// Large font
|
||||
QFont large(QStringLiteral("Consolas"), 18);
|
||||
popup.setFont(large);
|
||||
popup.setTypes({comp});
|
||||
popup.popup(QPoint(-9999, -9999));
|
||||
QApplication::processEvents();
|
||||
int largeW = popup.width();
|
||||
popup.hide();
|
||||
|
||||
// Popup with larger font should be wider
|
||||
QVERIFY2(largeW > smallW,
|
||||
qPrintable(QString("Large popup width %1 should be > small %2")
|
||||
.arg(largeW).arg(smallW)));
|
||||
}
|
||||
// ── Test: popup updates colors when theme changes ──
|
||||
|
||||
void testPopupUpdatesOnThemeChange() {
|
||||
auto& tm = ThemeManager::instance();
|
||||
int origIdx = tm.currentIndex();
|
||||
|
||||
// Ensure at least two themes exist
|
||||
QVERIFY2(tm.themes().size() >= 2,
|
||||
"Need at least 2 themes to test theme switching");
|
||||
|
||||
// Create popup with current theme
|
||||
TypeSelectorPopup popup;
|
||||
TypeEntry prim;
|
||||
prim.entryKind = TypeEntry::Primitive;
|
||||
prim.primitiveKind = NodeKind::Int32;
|
||||
prim.displayName = QStringLiteral("int32_t");
|
||||
popup.setTypes({prim});
|
||||
|
||||
QColor bgBefore = popup.palette().color(QPalette::Window);
|
||||
|
||||
// Switch to a different theme
|
||||
int otherIdx = (origIdx == 0) ? 1 : 0;
|
||||
tm.setCurrent(otherIdx);
|
||||
QApplication::processEvents();
|
||||
|
||||
// The popup should have applyTheme connected to themeChanged
|
||||
popup.applyTheme(tm.current());
|
||||
QColor bgAfter = popup.palette().color(QPalette::Window);
|
||||
|
||||
// If the two themes have different background colors, verify the change
|
||||
// (some themes may coincidentally share colors, so we just verify the
|
||||
// method doesn't crash and the palette is set to the new theme's color)
|
||||
QCOMPARE(bgAfter, tm.current().backgroundAlt);
|
||||
|
||||
// Also verify child widgets got updated
|
||||
auto* filterEdit = popup.findChild<QLineEdit*>();
|
||||
QVERIFY(filterEdit);
|
||||
QCOMPARE(filterEdit->palette().color(QPalette::Base),
|
||||
tm.current().background);
|
||||
|
||||
auto* listView = popup.findChild<QListView*>();
|
||||
QVERIFY(listView);
|
||||
QCOMPARE(listView->palette().color(QPalette::Base),
|
||||
tm.current().background);
|
||||
|
||||
// Restore original theme
|
||||
tm.setCurrent(origIdx);
|
||||
}
|
||||
|
||||
void testPopupAutoConnectsThemeChange() {
|
||||
auto& tm = ThemeManager::instance();
|
||||
int origIdx = tm.currentIndex();
|
||||
QVERIFY2(tm.themes().size() >= 2, "Need >= 2 themes");
|
||||
|
||||
TypeSelectorPopup popup;
|
||||
|
||||
// applyTheme is a public slot — verify it can be connected
|
||||
connect(&tm, &ThemeManager::themeChanged,
|
||||
&popup, &TypeSelectorPopup::applyTheme);
|
||||
|
||||
QColor bgBefore = popup.palette().color(QPalette::Window);
|
||||
|
||||
int otherIdx = (origIdx == 0) ? 1 : 0;
|
||||
tm.setCurrent(otherIdx);
|
||||
QApplication::processEvents();
|
||||
|
||||
// After theme change + signal, popup palette should match new theme
|
||||
QCOMPARE(popup.palette().color(QPalette::Window),
|
||||
tm.current().backgroundAlt);
|
||||
|
||||
// Restore
|
||||
tm.setCurrent(origIdx);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestTypeSelector)
|
||||
|
||||
@@ -16,7 +16,7 @@ using namespace rcx;
|
||||
// ── Fixture: small tree with diverse field types ──
|
||||
|
||||
static void buildValidationTree(NodeTree& tree) {
|
||||
tree.baseAddress = 0x1000;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
@@ -57,8 +57,8 @@ static void buildValidationTree(NodeTree& tree) {
|
||||
field(46, NodeKind::Hex32, "field_h32");
|
||||
field(50, NodeKind::Hex64, "field_h64");
|
||||
field(58, NodeKind::Pointer64, "field_ptr");
|
||||
field(66, NodeKind::Padding, "pad0");
|
||||
tree.nodes.last().arrayLen = 6;
|
||||
field(66, NodeKind::Hex32, "pad0");
|
||||
field(70, NodeKind::Hex16, "pad1");
|
||||
fieldArr(72, NodeKind::UInt32, 4, "field_arr");
|
||||
}
|
||||
|
||||
@@ -725,9 +725,9 @@ private slots:
|
||||
QCOMPARE(m_doc->undoStack.count(), 0);
|
||||
}
|
||||
|
||||
// ── changeNodeKind size transitions: shrink inserts padding ──
|
||||
// ── changeNodeKind size transitions: shrink inserts hex nodes ──
|
||||
|
||||
void testChangeKindShrinkInsertsPadding() {
|
||||
void testChangeKindShrinkInsertsHexNodes() {
|
||||
int idx = findNode(m_doc->tree, "field_u32");
|
||||
QVERIFY(idx >= 0);
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); // 4 bytes
|
||||
@@ -737,7 +737,7 @@ private slots:
|
||||
QApplication::processEvents();
|
||||
|
||||
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt8);
|
||||
// Should have inserted padding nodes (Hex16 + Hex8 = 3 bytes, or similar)
|
||||
// Should have inserted hex nodes (Hex16 + Hex8 = 3 bytes, or similar)
|
||||
QVERIFY(m_doc->tree.nodes.size() > origCount);
|
||||
|
||||
// Undo restores everything
|
||||
@@ -985,37 +985,6 @@ private slots:
|
||||
QVERIFY(!m_editor->isEditing());
|
||||
}
|
||||
|
||||
// ── Editor: padding value edit blocked, name/type still work ──
|
||||
|
||||
void testPaddingEditRestrictions() {
|
||||
m_ctrl->refresh();
|
||||
QApplication::processEvents();
|
||||
|
||||
ComposeResult result = m_doc->compose();
|
||||
m_editor->applyDocument(result);
|
||||
QApplication::processEvents();
|
||||
|
||||
// Find padding line
|
||||
int padLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].nodeKind == NodeKind::Padding &&
|
||||
result.meta[i].lineKind == LineKind::Field) {
|
||||
padLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(padLine >= 0);
|
||||
|
||||
// Value edit rejected
|
||||
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, padLine));
|
||||
|
||||
// Type edit accepted
|
||||
bool ok = m_editor->beginInlineEdit(EditTarget::Type, padLine);
|
||||
QVERIFY(ok);
|
||||
m_editor->cancelInlineEdit();
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
// ── Editor: struct header rejects value edit ──
|
||||
|
||||
void testStructHeaderRejectsValueEdit() {
|
||||
|
||||
@@ -260,17 +260,6 @@ private slots:
|
||||
qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16);
|
||||
}
|
||||
|
||||
void provider_setBase()
|
||||
{
|
||||
WinDbgMemoryProvider prov(m_connString);
|
||||
QVERIFY(prov.isValid());
|
||||
uint64_t orig = prov.base();
|
||||
prov.setBase(0x1000);
|
||||
QCOMPARE(prov.base(), (uint64_t)0x1000);
|
||||
prov.setBase(orig);
|
||||
QCOMPARE(prov.base(), orig);
|
||||
}
|
||||
|
||||
// ── Read: MZ header on main thread ──
|
||||
|
||||
void provider_read_mz_mainThread()
|
||||
@@ -325,7 +314,7 @@ private slots:
|
||||
// Verify it's not all zeros (the old failure mode)
|
||||
bool allZero = true;
|
||||
for (int i = 0; i < data.size(); ++i) {
|
||||
if (data[i] != 0) { allZero = false; break; }
|
||||
if (data[i] != '\0') { allZero = false; break; }
|
||||
}
|
||||
QVERIFY2(!allZero, "Data is all zeros — background thread read failed");
|
||||
}
|
||||
|
||||
16
third_party/fadec/.build.yml
vendored
Normal file
16
third_party/fadec/.build.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
image: alpine/edge
|
||||
sources:
|
||||
- https://git.sr.ht/~aengelke/fadec
|
||||
packages:
|
||||
- meson
|
||||
tasks:
|
||||
- build: |
|
||||
mkdir fadec-build1
|
||||
meson fadec-build1 fadec
|
||||
ninja -C fadec-build1
|
||||
ninja -C fadec-build1 test
|
||||
# Complete test with encode2 API.
|
||||
mkdir fadec-build2
|
||||
meson fadec-build2 fadec -Dwith_encode2=true
|
||||
ninja -C fadec-build2
|
||||
ninja -C fadec-build2 test
|
||||
51
third_party/fadec/.github/workflows/ci.yml
vendored
Normal file
51
third_party/fadec/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: sudo apt install -y ninja-build meson
|
||||
- name: Configure
|
||||
run: mkdir build; CC=clang CXX=clang++ meson -Dbuildtype=debugoptimized -Dwith_encode2=true build
|
||||
- name: Build
|
||||
run: ninja -v -C build
|
||||
- name: Test
|
||||
run: meson test -v -C build
|
||||
build-linux-cmake:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: sudo apt install -y ninja-build cmake
|
||||
- name: Configure
|
||||
run: CC=clang CXX=clang++ cmake -B build -G Ninja -DFADEC_ENCODE2=ON
|
||||
- name: Build
|
||||
run: cmake --build build -v
|
||||
- name: Test
|
||||
run: ctest --test-dir build -V
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: pip install ninja meson
|
||||
- name: Configure
|
||||
run: mkdir build; meson setup --vsenv -Dbuildtype=debugoptimized -Dwith_encode2=true build
|
||||
- name: Build
|
||||
run: meson compile -v -C build
|
||||
- name: Test
|
||||
run: meson test -v -C build
|
||||
build-windows-cmake:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Configure
|
||||
run: cmake -B build -DFADEC_ENCODE2=ON
|
||||
- name: Build
|
||||
run: cmake --build build -v
|
||||
- name: Test
|
||||
run: ctest --test-dir build -V -C Debug
|
||||
1
third_party/fadec/.gitignore
vendored
Normal file
1
third_party/fadec/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build/
|
||||
109
third_party/fadec/CMakeLists.txt
vendored
Normal file
109
third_party/fadec/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
cmake_minimum_required(VERSION 3.23)
|
||||
|
||||
project(fadec LANGUAGES C)
|
||||
enable_testing()
|
||||
|
||||
# TODO: make this actually optional
|
||||
enable_language(CXX OPTIONAL)
|
||||
|
||||
# Options
|
||||
set(FADEC_ARCHMODE "both" CACHE STRING "Support only 32-bit x86, 64-bit x86 or both")
|
||||
set_property(CACHE FADEC_ARCHMODE PROPERTY STRINGS both only32 only64)
|
||||
|
||||
option(FADEC_UNDOC "Include undocumented instructions" FALSE)
|
||||
option(FADEC_DECODE "Include support for decoding" TRUE)
|
||||
option(FADEC_ENCODE "Include support for encoding" TRUE)
|
||||
option(FADEC_ENCODE2 "Include support for new encoding API" FALSE)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
|
||||
if (MSVC)
|
||||
add_compile_options(/W4 -D_CRT_SECURE_NO_WARNINGS /wd4018 /wd4146 /wd4244 /wd4245 /wd4267 /wd4310)
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-Zc:preprocessor>)
|
||||
else()
|
||||
add_compile_options(-Wall -Wextra -Wpedantic -Wno-overlength-strings)
|
||||
endif()
|
||||
|
||||
find_package(Python3 3.9 REQUIRED)
|
||||
|
||||
add_library(fadec)
|
||||
add_library(fadec::fadec ALIAS fadec)
|
||||
set_target_properties(fadec PROPERTIES
|
||||
LINKER_LANGUAGE C
|
||||
)
|
||||
|
||||
set(GEN_ARGS "")
|
||||
if (NOT FADEC_ARCHMODE STREQUAL "only64")
|
||||
list(APPEND GEN_ARGS "--32")
|
||||
endif ()
|
||||
if (NOT FADEC_ARCHMODE STREQUAL "only32")
|
||||
list(APPEND GEN_ARGS "--64")
|
||||
endif ()
|
||||
if (FADEC_UNDOC)
|
||||
list(APPEND GEN_ARGS "--with-undoc")
|
||||
endif ()
|
||||
|
||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include")
|
||||
|
||||
function(fadec_component)
|
||||
cmake_parse_arguments(ARG "" "NAME" "HEADERS;SOURCES" ${ARGN})
|
||||
|
||||
set(PRIV_INC ${CMAKE_CURRENT_BINARY_DIR}/include/fadec-${ARG_NAME}-private.inc)
|
||||
set(PUB_INC ${CMAKE_CURRENT_BINARY_DIR}/include/fadec-${ARG_NAME}-public.inc)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${PRIV_INC} ${PUB_INC}
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/parseinstrs.py ${ARG_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/instrs.txt
|
||||
${PUB_INC} ${PRIV_INC} ${GEN_ARGS}
|
||||
DEPENDS instrs.txt parseinstrs.py
|
||||
COMMENT "Building table for ${ARG_NAME}"
|
||||
)
|
||||
|
||||
list(APPEND FADEC_HEADERS ${PUB_INC})
|
||||
target_sources(fadec PRIVATE
|
||||
${ARG_SOURCES}
|
||||
|
||||
PUBLIC
|
||||
FILE_SET HEADERS
|
||||
BASE_DIRS .
|
||||
FILES
|
||||
${ARG_HEADERS}
|
||||
|
||||
PUBLIC
|
||||
FILE_SET generated_public TYPE HEADERS
|
||||
BASE_DIRS ${CMAKE_CURRENT_BINARY_DIR}/include
|
||||
FILES
|
||||
${PUB_INC}
|
||||
|
||||
PRIVATE
|
||||
FILE_SET generated_private TYPE HEADERS
|
||||
BASE_DIRS ${CMAKE_CURRENT_BINARY_DIR}/include
|
||||
FILES
|
||||
${PRIV_INC}
|
||||
)
|
||||
|
||||
add_executable(fadec-${ARG_NAME}-test ${ARG_NAME}-test.c)
|
||||
target_link_libraries(fadec-${ARG_NAME}-test PRIVATE fadec)
|
||||
add_test(NAME ${ARG_NAME} COMMAND fadec-${ARG_NAME}-test)
|
||||
|
||||
if (CMAKE_CXX_COMPILER AND ${ARG_NAME} STREQUAL "encode2")
|
||||
add_executable(fadec-${ARG_NAME}-test-cpp ${ARG_NAME}-test.cc)
|
||||
target_link_libraries(fadec-${ARG_NAME}-test-cpp PRIVATE fadec)
|
||||
add_test(NAME ${ARG_NAME}-cpp COMMAND fadec-${ARG_NAME}-test-cpp)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if (FADEC_DECODE)
|
||||
fadec_component(NAME decode SOURCES decode.c format.c HEADERS fadec.h)
|
||||
endif ()
|
||||
if (FADEC_ENCODE)
|
||||
fadec_component(NAME encode SOURCES encode.c HEADERS fadec-enc.h)
|
||||
endif ()
|
||||
if (FADEC_ENCODE2)
|
||||
fadec_component(NAME encode2 SOURCES encode2.c HEADERS fadec-enc2.h)
|
||||
endif ()
|
||||
|
||||
install(TARGETS fadec EXPORT fadec
|
||||
LIBRARY
|
||||
ARCHIVE
|
||||
FILE_SET HEADERS FILE_SET generated_public)
|
||||
28
third_party/fadec/LICENSE
vendored
Normal file
28
third_party/fadec/LICENSE
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
Copyright (c) 2018, Alexis Engelke
|
||||
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.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
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.
|
||||
184
third_party/fadec/README.md
vendored
Normal file
184
third_party/fadec/README.md
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
# Fadec — Fast Decoder for x86-32 and x86-64 and Encoder for x86-64
|
||||
|
||||
Fadec is a fast and lightweight decoder for x86-32 and x86-64. To meet the goal of speed, lookup tables are used to map the opcode the (internal) description of the instruction encoding. This table currently has a size of roughly 37 kiB (for 32/64-bit combined).
|
||||
|
||||
Fadec-Enc (or Faenc) is a small, lightweight and easy-to-use encoder, currently for x86-64 only.
|
||||
|
||||
## Key features
|
||||
|
||||
> **Q: Why not just use any other decoding/encoding library available out there?**
|
||||
>
|
||||
> A: I needed to embed a small and fast decoder in a project for a freestanding environment (i.e., no libc). Further, only very few plain encoding libraries are available for x86-64; and most of them are large or make heavy use of external dependencies.
|
||||
|
||||
- **Small size:** the entire library with the x86-64/32 decoder and the x86-64 encoder are only 95 kiB; for specific use cases, the size can be reduced even further (e.g., by dropping AVX-512). The main decode/encode routines are only a few hundreds lines of code.
|
||||
- **Performance:** Fadec is significantly faster than libopcodes, Capstone, or Zydis due to the absence of high-level abstractions and the small lookup table.
|
||||
- **Zero dependencies:** the entire library has no dependencies, even on the standard library, making it suitable for freestanding environments without a full libc or `malloc`-style memory allocation.
|
||||
- **Correctness:** even corner cases should be handled correctly (if not, that's a bug), e.g., the order of prefixes, immediate sizes of jump instructions, the presence of the `lock` prefix, or properly handling VEX.W in 32-bit mode.
|
||||
|
||||
All components of this library target the Intel 64 implementations of x86. While AMD64 is _mostly similar_, there are some minor differences (e.g. operand sizes for jump instructions, more instructions, `cr8` can be accessed with `lock` prefix, `f34190` is `xchg`, not `pause`) which are currently not handled.
|
||||
|
||||
## Decoder Usage
|
||||
|
||||
### Example
|
||||
```c
|
||||
uint8_t buffer[] = {0x49, 0x90};
|
||||
FdInstr instr;
|
||||
// Decode from buffer into instr in 64-bit mode.
|
||||
int ret = fd_decode(buffer, sizeof(buffer), 64, 0, &instr);
|
||||
// ret<0 indicates an error, ret>0 the number of decoded bytes
|
||||
// Relevant properties of instructions can now be queried using the FD_* macros.
|
||||
// Or, we can format the instruction to a string buffer:
|
||||
char fmtbuf[64];
|
||||
fd_format(&instr, fmtbuf, sizeof(fmtbuf));
|
||||
// fmtbuf now reads: "xchg r8, rax"
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
The API consists of two functions to decode and format instructions, as well as several accessor macros. A full documentation can be found in [fadec.h](fadec.h). Direct access of any structure fields is not recommended.
|
||||
|
||||
- `int fd_decode(const uint8_t* buf, size_t len, int mode, uintptr_t address, FdInstr* out_instr)`
|
||||
- Decode a single instruction. For internal performance reasons, note that:
|
||||
- The decoded operand sizes are not always exact. However, the exact size can be reconstructed in all cases.
|
||||
- An implicit `fwait` in FPU instructions is decoded as a separate instruction (matching the opcode layout in machine code). For example, `finit` is decoded as `FD_FWAIT` + `FD_FINIT`
|
||||
- Return value: number of bytes used, or a negative value in case of an error.
|
||||
- `buf`/`len`: buffer containing instruction bytes. At most 15 bytes will be read. If the instruction is longer than `len`, an error value is returned.
|
||||
- `mode`: architecture mode, either `32` or `64`.
|
||||
- `address`: set to `0`. (Obsolete use: virtual address of the decoded instruction.)
|
||||
- `out_instr`: Pointer to the instruction buffer, might get written partially in case of an error.
|
||||
- `void fd_format(const FdInstr* instr, char* buf, size_t len)`
|
||||
- Format a single instruction to a human-readable format.
|
||||
- `instr`: decoded instruction.
|
||||
- `buf`/`len`: buffer for formatted instruction string
|
||||
- Various accessor macros: see [fadec.h](fadec.h).
|
||||
|
||||
## Encoder Usage
|
||||
|
||||
The encoder has two API variants: "v1" has a single entry point (`fe_enc64`) and the instruction is specified as integer parameter. "v2" has one entry point per instruction. v2 is currently about 3x faster than v1, but also has much larger code size (v1: <10 kiB; v2: ~3 MiB) and takes much longer to compile. It is therefore off by default and can be enabled by passing `-Dwith_encode2=true` to Meson. Both variants are supported.
|
||||
|
||||
### Example (API v1)
|
||||
|
||||
```c
|
||||
int failed = 0;
|
||||
uint8_t buf[64];
|
||||
uint8_t* cur = buf;
|
||||
|
||||
// xor eax, eax
|
||||
failed |= fe_enc64(&cur, FE_XOR32rr, FE_AX, FE_AX);
|
||||
// movzx ecx, byte ptr [rdi + 1*rax + 0]
|
||||
failed |= fe_enc64(&cur, FE_MOVZXr32m8, FE_CX, FE_MEM(FE_DI, 1, FE_AX, 0));
|
||||
// test ecx, ecx
|
||||
failed |= fe_enc64(&cur, FE_TEST32rr, FE_CX, FE_CX);
|
||||
// jnz $
|
||||
// This will be replaced later; FE_JMPL enforces use of longest offset
|
||||
uint8_t* fwd_jmp = cur;
|
||||
failed |= fe_enc64(&cur, FE_JNZ|FE_JMPL, (intptr_t) cur);
|
||||
uint8_t* loop_tgt = cur;
|
||||
// add rax, rcx
|
||||
failed |= fe_enc64(&cur, FE_ADD64rr, FE_AX, FE_CX);
|
||||
// sub ecx, 1
|
||||
failed |= fe_enc64(&cur, FE_SUB32ri, FE_CX, 1);
|
||||
// jnz loop_tgt
|
||||
failed |= fe_enc64(&cur, FE_JNZ, (intptr_t) loop_tgt);
|
||||
// (alternatively: fe_enc64(&cur, FE_Jcc|FE_CC_NZ, (intptr_t) loop_tgt).)
|
||||
// Update previous jump to jump here. Note that we _must_ specify FE_JMPL too.
|
||||
failed |= fe_enc64(&fwd_jmp, FE_JNZ|FE_JMPL, (intptr_t) cur);
|
||||
// ret
|
||||
failed |= fe_enc64(&cur, FE_RET);
|
||||
// cur now points to the end of the buffer, failed indicates any failures.
|
||||
```
|
||||
|
||||
### Example (API v2)
|
||||
|
||||
```c
|
||||
uint8_t buf[64];
|
||||
uint8_t* cur = buf;
|
||||
|
||||
// xor eax, eax
|
||||
cur += fe64_XOR32rr(cur, 0, FE_AX, FE_AX);
|
||||
// movzx ecx, byte ptr [rdi + 1*rax + 0]
|
||||
cur += fe64_MOVZXr32m8(cur, 0, FE_CX, FE_MEM(FE_DI, 1, FE_AX, 0));
|
||||
// test ecx, ecx
|
||||
cur += fe64_TEST32rr(cur, 0, FE_CX, FE_CX);
|
||||
// jnz $
|
||||
// This will be replaced later; FE_JMPL enforces use of longest offset
|
||||
uint8_t* fwd_jmp = cur;
|
||||
cur += fe64_JNZ(cur, FE_JMPL, cur);
|
||||
uint8_t* loop_tgt = cur;
|
||||
// add rax, rcx
|
||||
cur += fe64_ADD64rr(cur, 0, FE_AX, FE_CX);
|
||||
// sub ecx, 1
|
||||
cur += fe64_SUB32ri(cur, 0, FE_CX, 1);
|
||||
// jnz loop_tgt
|
||||
cur += fe64_JNZ(cur, 0, loop_tgt);
|
||||
// (alternatively: fe64_Jcc(cur, FE_CC_NZ, loop_tgt).)
|
||||
// Update previous jump to jump here. Note that we _must_ specify FE_JMPL too.
|
||||
fe64_JNZ(fwd_jmp, FE_JMPL, cur);
|
||||
// ret
|
||||
cur += fe64_RET(cur, 0);
|
||||
// cur now points to the end of the buffer
|
||||
// errors are ignored, this example should not cause any :-)
|
||||
```
|
||||
|
||||
### API v1
|
||||
|
||||
The API consists of one function to handle encode requests, as well as some macros. More information can be found in [fadec-enc.h](fadec-enc.h). Usage of internals like enum values is not recommended.
|
||||
|
||||
- `int fe_enc64(uint8_t** buf, uint64_t mnem, int64_t operands...)`
|
||||
- Encodes an instruction for x86-64 into `*buf`. EVEX-encoded instructions will transparently encode with the shorter VEX prefix where permitted.
|
||||
- Return value: `0` on success, a negative value in error cases.
|
||||
- `buf`: Pointer to the pointer to the instruction buffer. The pointer (`*buf`) will be advanced by the number of bytes written. The instruction buffer must have at least 15 bytes left.
|
||||
- `mnem`: Instruction mnemonic to encode combined with extra flags:
|
||||
- `FE_SEG(segreg)`: override segment to specified segment register.
|
||||
- `FE_ADDR32`: override address size to 32-bit.
|
||||
- `FE_JMPL`: use longest possible offset encoding, useful when jump target is not known.
|
||||
- `FE_MASK(maskreg)`: specify non-zero mask register (1--7) for instructions that support masking (suffixed with `_mask` or `_maskz`) or require a mask (AVX-512 gather/scatter).
|
||||
- `FE_RC_RN/RD/RU/RZ`: set rounding mode for instructions with static rounding control (suffixed `_er`).
|
||||
- `FE_CC_O/NO/E/NE/...`: set condition code for instructions with unspecified condition code (`Jcc`, `SETcc`, `CMOVcc`, `CMPccXADD`).
|
||||
- `operands...`: Up to 4 instruction operands. The operand kinds must match the requirements of the mnemonic.
|
||||
- For register operands (`r`=non-mask register, `k`=mask register), use the register: `FE_AX`, `FE_AH`, `FE_XMM12`.
|
||||
- For immediate operands (`i`=regular, `a`=absolute address), use the constant: `12`, `-0xbeef`.
|
||||
- For memory operands (`m`=regular or `b`=broadcast), use: `FE_MEM(basereg,scale,indexreg,offset)`. Use `0` to specify _no register_. For RIP-relative addressing, the size of the instruction is added automatically.
|
||||
- For offset operands (`o`), specify the target address.
|
||||
|
||||
### API v2
|
||||
|
||||
The API consists of one function per instruction, as well as some macros. The API provides type safety for different register types as well as for memory operands (regular vs. VSIB). Besides a few details listed here, the usage is very similar to API v1. More information can be found in [fadec-enc2.h](fadec-enc2.h). Usage of internals like enum values is not recommended.
|
||||
|
||||
- `int fe64_<mnemonic>(uint8_t* buf, int flags, <operands...>)`
|
||||
- Encodes the specified instruction for x86-64 into `buf`. EVEX-encoded instructions will transparently encode with the shorter VEX prefix where permitted.
|
||||
- Return value: `0` on failure, otherwise the instruction length.
|
||||
- `buf`: Pointer to the instruction buffer. The instruction buffer must have at least 15 bytes left. Bytes beyond the returned instruction length can be overwritten.
|
||||
- `flags`: combination of extra flags, default to `0`:
|
||||
- `FE_SEG(segreg)`: override segment to specified segment register.
|
||||
- `FE_ADDR32`: override address size to 32-bit.
|
||||
- `FE_JMPL`: use longest possible offset encoding, useful when jump target is not known.
|
||||
- `FE_RC_RN/RD/RU/RZ`: set rounding mode for instructions with static rounding control (suffixed `_er`).
|
||||
- `FE_CC_O/NO/E/NE/...`: set condition code for instructions with unspecified condition code (`Jcc`, `SETcc`, `CMOVcc`, `CMPccXADD`).
|
||||
- `FeRegMASK opmask` (instructions with opmask only): specify non-zero mask register (1--7) for instructions suffixed with `_mask`/`_maskz` and AVX-512 gather/scatter.
|
||||
- `operands...`: up to four instruction operands.
|
||||
- Registers have types `FeRegGP`/`FeRegXMM`/`FeRegMASK`/etc.; byte registers accepting high-byte operands also accept `FeRegGPH`.
|
||||
- Immediate operands have an appropriately sized integer type.
|
||||
- Memory operands use a `FeMem` (VSIB: `FeMemV`) structure, use the macro `FE_MEM(basereg,scale,indexreg,offset)` (VSIB: `FE_MEMV(...)`). Use `FE_NOREG` to specify _no register_. For RIP-relative addressing, the size of the instruction is added automatically.
|
||||
- For offset operands (`o`), specify the target address relative to `buf`.
|
||||
- `int fe64_NOP(uint8_t* buf, unsigned size)`
|
||||
- Encode a series of `nop`s of `size` bytes, but at least emit one byte. This will use larger the `nop` encodings to reduce the number of instructions and is intended for filling padding.
|
||||
|
||||
## Known issues
|
||||
- Decoder/Encoder: register uniqueness constraints are not enforced. This affects:
|
||||
- VSIB-encoded instructions: no vector register may be used more than once
|
||||
- AMX instructions: no tile register may be used more than once
|
||||
- AVX-512 complex FP16 multiplication: destination must be not be equal to a source register
|
||||
- Prefixes for indirect jumps and calls are not properly decoded, e.g. `notrack`, `bnd`.
|
||||
- Low test coverage. (Help needed.)
|
||||
- No Python API.
|
||||
|
||||
Some ISA extensions are not supported, often because they are deprecated or unsupported by recent hardware. These are unlikely to be implemented in the near future:
|
||||
|
||||
- (Intel) MPX: Intel lists MPX as deprecated.
|
||||
- (Intel) HLE prefixes `xacquire`/`xrelease`: Intel lists HLE as deprecated. The formatter for decoded instructions is able to reconstruct these in most cases, though.
|
||||
- (Intel) Xeon Phi (KNC/KNL/KNM) extensions, including the MVEX prefix: the hardware is discontinued/no longer available.
|
||||
- (AMD) XOP: unsupported by newer hardware.
|
||||
- (AMD) FMA4: unsupported by newer hardware.
|
||||
|
||||
If you find any other issues, please report a bug. Or, even better, send a patch fixing the issue.
|
||||
3248
third_party/fadec/decode-test.c
vendored
Normal file
3248
third_party/fadec/decode-test.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
791
third_party/fadec/decode.c
vendored
Normal file
791
third_party/fadec/decode.c
vendored
Normal file
@@ -0,0 +1,791 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <fadec.h>
|
||||
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define LIKELY(x) __builtin_expect((x), 1)
|
||||
#define UNLIKELY(x) __builtin_expect((x), 0)
|
||||
#define ASSUME(x) do { if (!(x)) __builtin_unreachable(); } while (0)
|
||||
#else
|
||||
#define LIKELY(x) (x)
|
||||
#define UNLIKELY(x) (x)
|
||||
#define ASSUME(x) ((void) 0)
|
||||
#endif
|
||||
|
||||
// Defines FD_TABLE_OFFSET_32 and FD_TABLE_OFFSET_64, if available
|
||||
#define FD_DECODE_TABLE_DEFINES
|
||||
#include <fadec-decode-private.inc>
|
||||
#undef FD_DECODE_TABLE_DEFINES
|
||||
|
||||
enum DecodeMode {
|
||||
DECODE_64 = 0,
|
||||
DECODE_32 = 1,
|
||||
};
|
||||
|
||||
typedef enum DecodeMode DecodeMode;
|
||||
|
||||
#define ENTRY_NONE 0
|
||||
#define ENTRY_INSTR 1
|
||||
#define ENTRY_TABLE256 2
|
||||
#define ENTRY_TABLE16 3
|
||||
#define ENTRY_TABLE8E 4
|
||||
#define ENTRY_TABLE_PREFIX 5
|
||||
#define ENTRY_TABLE_VEX 6
|
||||
#define ENTRY_TABLE_ROOT 8
|
||||
#define ENTRY_MASK 7
|
||||
|
||||
static uint16_t
|
||||
table_lookup(unsigned cur_idx, unsigned entry_idx) {
|
||||
static _Alignas(16) const uint16_t _decode_table[] = {
|
||||
#define FD_DECODE_TABLE_DATA
|
||||
#include <fadec-decode-private.inc>
|
||||
#undef FD_DECODE_TABLE_DATA
|
||||
};
|
||||
return _decode_table[cur_idx + entry_idx];
|
||||
}
|
||||
|
||||
static unsigned
|
||||
table_walk(unsigned table_entry, unsigned entry_idx) {
|
||||
return table_lookup(table_entry & ~0x3, entry_idx);
|
||||
}
|
||||
|
||||
#define LOAD_LE_1(buf) ((uint64_t) *(const uint8_t*) (buf))
|
||||
#define LOAD_LE_2(buf) (LOAD_LE_1(buf) | LOAD_LE_1((const uint8_t*) (buf) + 1)<<8)
|
||||
#define LOAD_LE_3(buf) (LOAD_LE_2(buf) | LOAD_LE_1((const uint8_t*) (buf) + 2)<<16)
|
||||
#define LOAD_LE_4(buf) (LOAD_LE_2(buf) | LOAD_LE_2((const uint8_t*) (buf) + 2)<<16)
|
||||
#define LOAD_LE_8(buf) (LOAD_LE_4(buf) | LOAD_LE_4((const uint8_t*) (buf) + 4)<<32)
|
||||
|
||||
enum
|
||||
{
|
||||
PREFIX_REXB = 0x01,
|
||||
PREFIX_REXX = 0x02,
|
||||
PREFIX_REXR = 0x04,
|
||||
PREFIX_REXW = 0x08,
|
||||
PREFIX_REX = 0x40,
|
||||
PREFIX_REXRR = 0x10,
|
||||
PREFIX_VEX = 0x20,
|
||||
};
|
||||
|
||||
struct InstrDesc
|
||||
{
|
||||
uint16_t type;
|
||||
uint16_t operand_indices;
|
||||
uint16_t operand_sizes;
|
||||
uint16_t reg_types;
|
||||
};
|
||||
|
||||
#define DESC_HAS_MODRM(desc) (((desc)->operand_indices & (3 << 0)) != 0)
|
||||
#define DESC_MODRM_IDX(desc) ((((desc)->operand_indices >> 0) & 3) ^ 3)
|
||||
#define DESC_HAS_MODREG(desc) (((desc)->operand_indices & (3 << 2)) != 0)
|
||||
#define DESC_MODREG_IDX(desc) ((((desc)->operand_indices >> 2) & 3) ^ 3)
|
||||
#define DESC_HAS_VEXREG(desc) (((desc)->operand_indices & (3 << 4)) != 0)
|
||||
#define DESC_VEXREG_IDX(desc) ((((desc)->operand_indices >> 4) & 3) ^ 3)
|
||||
#define DESC_IMM_CONTROL(desc) (((desc)->operand_indices >> 12) & 0x7)
|
||||
#define DESC_IMM_IDX(desc) ((((desc)->operand_indices >> 6) & 3) ^ 3)
|
||||
#define DESC_EVEX_BCST(desc) (((desc)->operand_indices >> 8) & 1)
|
||||
#define DESC_EVEX_MASK(desc) (((desc)->operand_indices >> 9) & 1)
|
||||
#define DESC_ZEROREG_VAL(desc) (((desc)->operand_indices >> 10) & 1)
|
||||
#define DESC_LOCK(desc) (((desc)->operand_indices >> 11) & 1)
|
||||
#define DESC_VSIB(desc) (((desc)->operand_indices >> 15) & 1)
|
||||
#define DESC_OPSIZE(desc) (((desc)->reg_types >> 11) & 7)
|
||||
#define DESC_MODRM_SIZE(desc) (((desc)->operand_sizes >> 0) & 3)
|
||||
#define DESC_MODREG_SIZE(desc) (((desc)->operand_sizes >> 2) & 3)
|
||||
#define DESC_VEXREG_SIZE(desc) (((desc)->operand_sizes >> 4) & 3)
|
||||
#define DESC_IMM_SIZE(desc) (((desc)->operand_sizes >> 6) & 3)
|
||||
#define DESC_LEGACY(desc) (((desc)->operand_sizes >> 8) & 1)
|
||||
#define DESC_SIZE_FIX1(desc) (((desc)->operand_sizes >> 10) & 7)
|
||||
#define DESC_SIZE_FIX2(desc) (((desc)->operand_sizes >> 13) & 3)
|
||||
#define DESC_INSTR_WIDTH(desc) (((desc)->operand_sizes >> 15) & 1)
|
||||
#define DESC_MODRM(desc) (((desc)->reg_types >> 14) & 1)
|
||||
#define DESC_IGN66(desc) (((desc)->reg_types >> 15) & 1)
|
||||
#define DESC_EVEX_SAE(desc) (((desc)->reg_types >> 8) & 1)
|
||||
#define DESC_EVEX_ER(desc) (((desc)->reg_types >> 9) & 1)
|
||||
#define DESC_EVEX_BCST16(desc) (((desc)->reg_types >> 10) & 1)
|
||||
#define DESC_REGTY_MODRM(desc) (((desc)->reg_types >> 0) & 7)
|
||||
#define DESC_REGTY_MODREG(desc) (((desc)->reg_types >> 3) & 7)
|
||||
#define DESC_REGTY_VEXREG(desc) (((desc)->reg_types >> 6) & 3)
|
||||
|
||||
int
|
||||
fd_decode(const uint8_t* buffer, size_t len_sz, int mode_int, uintptr_t address,
|
||||
FdInstr* instr)
|
||||
{
|
||||
int len = len_sz > 15 ? 15 : len_sz;
|
||||
|
||||
// Ensure that we can actually handle the decode request
|
||||
DecodeMode mode;
|
||||
unsigned table_root_idx;
|
||||
switch (mode_int)
|
||||
{
|
||||
#if defined(FD_TABLE_OFFSET_32)
|
||||
case 32: table_root_idx = FD_TABLE_OFFSET_32; mode = DECODE_32; break;
|
||||
#endif
|
||||
#if defined(FD_TABLE_OFFSET_64)
|
||||
case 64: table_root_idx = FD_TABLE_OFFSET_64; mode = DECODE_64; break;
|
||||
#endif
|
||||
default: return FD_ERR_INTERNAL;
|
||||
}
|
||||
|
||||
int off = 0;
|
||||
uint8_t vex_operand = 0;
|
||||
|
||||
uint8_t addr_size = mode == DECODE_64 ? 3 : 2;
|
||||
unsigned prefix_rex = 0;
|
||||
uint8_t prefix_rep = 0;
|
||||
unsigned vexl = 0;
|
||||
unsigned prefix_evex = 0;
|
||||
instr->segment = FD_REG_NONE;
|
||||
|
||||
// Values must match prefixes in parseinstrs.py.
|
||||
enum {
|
||||
PF_SEG1 = 0xfff8 - 0xfff8,
|
||||
PF_SEG2 = 0xfff9 - 0xfff8,
|
||||
PF_66 = 0xfffa - 0xfff8,
|
||||
PF_67 = 0xfffb - 0xfff8,
|
||||
PF_LOCK = 0xfffc - 0xfff8,
|
||||
PF_REP = 0xfffd - 0xfff8,
|
||||
PF_REX = 0xfffe - 0xfff8,
|
||||
};
|
||||
|
||||
uint8_t prefixes[8] = {0};
|
||||
unsigned table_entry = 0;
|
||||
while (true) {
|
||||
if (UNLIKELY(off >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
uint8_t prefix = buffer[off];
|
||||
table_entry = table_lookup(table_root_idx, prefix);
|
||||
if (LIKELY(table_entry - 0xfff8 >= 8))
|
||||
break;
|
||||
prefixes[PF_REX] = 0;
|
||||
prefixes[table_entry - 0xfff8] = prefix;
|
||||
off++;
|
||||
}
|
||||
if (off) {
|
||||
if (UNLIKELY(prefixes[PF_SEG2])) {
|
||||
if (prefixes[PF_SEG2] & 0x02)
|
||||
instr->segment = prefixes[PF_SEG2] >> 3 & 3;
|
||||
else
|
||||
instr->segment = prefixes[PF_SEG2] & 7;
|
||||
}
|
||||
if (UNLIKELY(prefixes[PF_67]))
|
||||
addr_size--;
|
||||
prefix_rex = prefixes[PF_REX];
|
||||
prefix_rep = prefixes[PF_REP];
|
||||
}
|
||||
|
||||
// table_entry kinds: INSTR(0), T16(1), ESCAPE_A(2), ESCAPE_B(3)
|
||||
if (LIKELY(!(table_entry & 2))) {
|
||||
off++;
|
||||
|
||||
// Then, walk through ModR/M-encoded opcode extensions.
|
||||
if (table_entry & 1) {
|
||||
if (UNLIKELY(off >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
unsigned isreg = buffer[off] >= 0xc0;
|
||||
table_entry = table_walk(table_entry, ((buffer[off] >> 2) & 0xe) | isreg);
|
||||
// table_entry kinds: INSTR(0), T8E(1)
|
||||
if (table_entry & 1)
|
||||
table_entry = table_walk(table_entry, buffer[off] & 7);
|
||||
}
|
||||
|
||||
// table_entry kinds: INSTR(0)
|
||||
goto direct;
|
||||
}
|
||||
|
||||
if (UNLIKELY(off >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
|
||||
unsigned opcode_escape = 0;
|
||||
uint8_t mandatory_prefix = 0; // without escape/VEX/EVEX, this is ignored.
|
||||
if (buffer[off] == 0x0f)
|
||||
{
|
||||
if (UNLIKELY(off + 1 >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
if (buffer[off + 1] == 0x38)
|
||||
opcode_escape = 2;
|
||||
else if (buffer[off + 1] == 0x3a)
|
||||
opcode_escape = 3;
|
||||
else
|
||||
opcode_escape = 1;
|
||||
off += opcode_escape >= 2 ? 2 : 1;
|
||||
|
||||
// If there is no REP/REPNZ prefix offer 66h as mandatory prefix. If
|
||||
// there is a REP prefix, then the 66h prefix is ignored here.
|
||||
mandatory_prefix = prefix_rep ? prefix_rep ^ 0xf1 : !!prefixes[PF_66];
|
||||
}
|
||||
else if (UNLIKELY((unsigned) buffer[off] - 0xc4 < 2 || buffer[off] == 0x62))
|
||||
{
|
||||
unsigned vex_prefix = buffer[off];
|
||||
// VEX (C4/C5) or EVEX (62)
|
||||
if (UNLIKELY(off + 1 >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
if (UNLIKELY(mode == DECODE_32 && buffer[off + 1] < 0xc0)) {
|
||||
off++;
|
||||
table_entry = table_walk(table_entry, 0);
|
||||
// table_entry kinds: INSTR(0)
|
||||
goto direct;
|
||||
}
|
||||
|
||||
// VEX/EVEX + 66/F3/F2/REX will #UD.
|
||||
// Note: REX is also here only respected if it immediately precedes the
|
||||
// opcode, in this case the VEX/EVEX "prefix".
|
||||
if (prefixes[PF_66] || prefixes[PF_REP] || prefix_rex)
|
||||
return FD_ERR_UD;
|
||||
|
||||
uint8_t byte = buffer[off + 1];
|
||||
if (vex_prefix == 0xc5) // 2-byte VEX
|
||||
{
|
||||
opcode_escape = 1;
|
||||
prefix_rex = byte & 0x80 ? 0 : PREFIX_REXR;
|
||||
}
|
||||
else // 3-byte VEX or EVEX
|
||||
{
|
||||
// SDM Vol 2A 2-15 (Dec. 2016): Ignored in 32-bit mode
|
||||
if (mode == DECODE_64)
|
||||
prefix_rex = byte >> 5 ^ 0x7;
|
||||
if (vex_prefix == 0x62) // EVEX
|
||||
{
|
||||
if (byte & 0x08) // Bit 3 of opcode_escape must be clear.
|
||||
return FD_ERR_UD;
|
||||
_Static_assert(PREFIX_REXRR == 0x10, "wrong REXRR value");
|
||||
if (mode == DECODE_64)
|
||||
prefix_rex |= (byte & PREFIX_REXRR) ^ PREFIX_REXRR;
|
||||
}
|
||||
else // 3-byte VEX
|
||||
{
|
||||
if (byte & 0x18) // Bits 4:3 of opcode_escape must be clear.
|
||||
return FD_ERR_UD;
|
||||
}
|
||||
|
||||
opcode_escape = (byte & 0x07);
|
||||
if (UNLIKELY(opcode_escape == 0)) {
|
||||
int prefix_len = vex_prefix == 0x62 ? 4 : 3;
|
||||
// Pretend to decode the prefix plus one opcode byte.
|
||||
return off + prefix_len > len ? FD_ERR_PARTIAL : FD_ERR_UD;
|
||||
}
|
||||
|
||||
// Load third byte of VEX prefix
|
||||
if (UNLIKELY(off + 2 >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
byte = buffer[off + 2];
|
||||
prefix_rex |= byte & 0x80 ? PREFIX_REXW : 0;
|
||||
}
|
||||
|
||||
mandatory_prefix = byte & 3;
|
||||
vex_operand = ((byte & 0x78) >> 3) ^ 0xf;
|
||||
prefix_rex |= PREFIX_VEX;
|
||||
|
||||
if (vex_prefix == 0x62) // EVEX
|
||||
{
|
||||
if (!(byte & 0x04)) // Bit 10 must be 1.
|
||||
return FD_ERR_UD;
|
||||
if (UNLIKELY(off + 3 >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
byte = buffer[off + 3];
|
||||
// prefix_evex is z:L'L/RC:b:V':aaa
|
||||
vexl = (byte >> 5) & 3;
|
||||
prefix_evex = byte | 0x100; // Ensure that prefix_evex is non-zero.
|
||||
if (mode == DECODE_64) // V' causes UD in 32-bit mode
|
||||
vex_operand |= byte & 0x08 ? 0 : 0x10; // V'
|
||||
else if (!(byte & 0x08))
|
||||
return FD_ERR_UD;
|
||||
off += 4;
|
||||
}
|
||||
else // VEX
|
||||
{
|
||||
vexl = byte & 0x04 ? 1 : 0;
|
||||
off += 0xc7 - vex_prefix; // 3 for c4, 2 for c5
|
||||
}
|
||||
}
|
||||
|
||||
table_entry = table_walk(table_entry, opcode_escape);
|
||||
// table_entry kinds: INSTR(0) [only for invalid], T256(2)
|
||||
if (UNLIKELY(!table_entry))
|
||||
return FD_ERR_UD;
|
||||
if (UNLIKELY(off >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
table_entry = table_walk(table_entry, buffer[off++]);
|
||||
// table_entry kinds: INSTR(0), T16(1), TVEX(2), TPREFIX(3)
|
||||
|
||||
// Handle mandatory prefixes (which behave like an opcode ext.).
|
||||
if ((table_entry & 3) == 3)
|
||||
table_entry = table_walk(table_entry, mandatory_prefix);
|
||||
// table_entry kinds: INSTR(0), T16(1), TVEX(2)
|
||||
|
||||
// Then, walk through ModR/M-encoded opcode extensions.
|
||||
if (table_entry & 1) {
|
||||
if (UNLIKELY(off >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
unsigned isreg = buffer[off] >= 0xc0;
|
||||
table_entry = table_walk(table_entry, ((buffer[off] >> 2) & 0xe) | isreg);
|
||||
// table_entry kinds: INSTR(0), T8E(1), TVEX(2)
|
||||
if (table_entry & 1)
|
||||
table_entry = table_walk(table_entry, buffer[off] & 7);
|
||||
}
|
||||
// table_entry kinds: INSTR(0), TVEX(2)
|
||||
|
||||
// For VEX prefix, we have to distinguish between VEX.W and VEX.L which may
|
||||
// be part of the opcode.
|
||||
if (UNLIKELY(table_entry & 2))
|
||||
{
|
||||
uint8_t index = 0;
|
||||
index |= prefix_rex & PREFIX_REXW ? (1 << 0) : 0;
|
||||
// When EVEX.L'L is the rounding mode, the instruction must not have
|
||||
// L'L constraints.
|
||||
index |= vexl << 1;
|
||||
table_entry = table_walk(table_entry, index);
|
||||
}
|
||||
// table_entry kinds: INSTR(0)
|
||||
|
||||
direct:
|
||||
// table_entry kinds: INSTR(0)
|
||||
if (UNLIKELY(!table_entry))
|
||||
return FD_ERR_UD;
|
||||
|
||||
static _Alignas(16) const struct InstrDesc descs[] = {
|
||||
#define FD_DECODE_TABLE_DESCS
|
||||
#include <fadec-decode-private.inc>
|
||||
#undef FD_DECODE_TABLE_DESCS
|
||||
};
|
||||
const struct InstrDesc* desc = &descs[table_entry >> 2];
|
||||
|
||||
instr->type = desc->type;
|
||||
instr->addrsz = addr_size;
|
||||
instr->flags = ((prefix_rep + 1) & 6) + (mode == DECODE_64 ? FD_FLAG_64 : 0);
|
||||
instr->address = address;
|
||||
|
||||
for (unsigned i = 0; i < sizeof(instr->operands) / sizeof(FdOp); i++)
|
||||
instr->operands[i] = (FdOp) {0};
|
||||
|
||||
if (DESC_MODRM(desc) && UNLIKELY(off++ >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
unsigned op_byte = buffer[off - 1] | (!DESC_MODRM(desc) ? 0xc0 : 0);
|
||||
|
||||
if (UNLIKELY(prefix_evex)) {
|
||||
// VSIB inst (gather/scatter) without mask register or w/EVEX.z is UD
|
||||
if (DESC_VSIB(desc) && (!(prefix_evex & 0x07) || (prefix_evex & 0x80)))
|
||||
return FD_ERR_UD;
|
||||
// Inst doesn't support masking, so EVEX.z or EVEX.aaa is UD
|
||||
if (!DESC_EVEX_MASK(desc) && (prefix_evex & 0x87))
|
||||
return FD_ERR_UD;
|
||||
// EVEX.z without EVEX.aaa is UD. The Intel SDM is rather unprecise
|
||||
// about this, but real hardware doesn't accept this.
|
||||
if ((prefix_evex & 0x87) == 0x80)
|
||||
return FD_ERR_UD;
|
||||
|
||||
// Cases for SAE/RC (reg operands only):
|
||||
// - ER supported -> all ok
|
||||
// - SAE supported -> assume L'L is RC, but ignored (undocumented)
|
||||
// - Neither supported -> b == 0
|
||||
if ((prefix_evex & 0x10) && (op_byte & 0xc0) == 0xc0) { // EVEX.b+reg
|
||||
if (!DESC_EVEX_SAE(desc))
|
||||
return FD_ERR_UD;
|
||||
vexl = 2;
|
||||
if (DESC_EVEX_ER(desc))
|
||||
instr->evex = prefix_evex;
|
||||
else
|
||||
instr->evex = (prefix_evex & 0x87) | 0x60; // set RC, clear B
|
||||
} else {
|
||||
if (UNLIKELY(vexl == 3)) // EVEX.L'L == 11b is UD
|
||||
return FD_ERR_UD;
|
||||
instr->evex = prefix_evex & 0x87; // clear RC, clear B
|
||||
}
|
||||
|
||||
if (DESC_VSIB(desc))
|
||||
vex_operand &= 0xf; // EVEX.V' is used as index extension instead.
|
||||
} else {
|
||||
instr->evex = 0;
|
||||
}
|
||||
|
||||
unsigned op_size;
|
||||
unsigned op_size_alt = 0;
|
||||
if (!(DESC_OPSIZE(desc) & 4)) {
|
||||
if (mode == DECODE_64)
|
||||
op_size = ((prefix_rex & PREFIX_REXW) || DESC_OPSIZE(desc) == 3) ? 4 :
|
||||
UNLIKELY(prefixes[PF_66] && !DESC_IGN66(desc)) ? 2 :
|
||||
DESC_OPSIZE(desc) ? 4 :
|
||||
3;
|
||||
else
|
||||
op_size = UNLIKELY(prefixes[PF_66] && !DESC_IGN66(desc)) ? 2 : 3;
|
||||
} else {
|
||||
op_size = 5 + vexl;
|
||||
op_size_alt = op_size - (DESC_OPSIZE(desc) & 3);
|
||||
}
|
||||
|
||||
uint8_t operand_sizes[4] = {
|
||||
DESC_SIZE_FIX1(desc), DESC_SIZE_FIX2(desc) + 1, op_size, op_size_alt
|
||||
};
|
||||
|
||||
if (UNLIKELY(instr->type == FDI_MOV_CR || instr->type == FDI_MOV_DR)) {
|
||||
unsigned modreg = (op_byte >> 3) & 0x7;
|
||||
unsigned modrm = op_byte & 0x7;
|
||||
|
||||
FdOp* op_modreg = &instr->operands[DESC_MODREG_IDX(desc)];
|
||||
op_modreg->type = FD_OT_REG;
|
||||
op_modreg->size = op_size;
|
||||
op_modreg->reg = modreg | (prefix_rex & PREFIX_REXR ? 8 : 0);
|
||||
op_modreg->misc = instr->type == FDI_MOV_CR ? FD_RT_CR : FD_RT_DR;
|
||||
if (instr->type == FDI_MOV_CR && (~0x011d >> op_modreg->reg) & 1)
|
||||
return FD_ERR_UD;
|
||||
else if (instr->type == FDI_MOV_DR && prefix_rex & PREFIX_REXR)
|
||||
return FD_ERR_UD;
|
||||
|
||||
FdOp* op_modrm = &instr->operands[DESC_MODRM_IDX(desc)];
|
||||
op_modrm->type = FD_OT_REG;
|
||||
op_modrm->size = op_size;
|
||||
op_modrm->reg = modrm | (prefix_rex & PREFIX_REXB ? 8 : 0);
|
||||
op_modrm->misc = FD_RT_GPL;
|
||||
goto skip_modrm;
|
||||
}
|
||||
|
||||
if (DESC_HAS_MODREG(desc))
|
||||
{
|
||||
FdOp* op_modreg = &instr->operands[DESC_MODREG_IDX(desc)];
|
||||
unsigned reg_idx = (op_byte & 0x38) >> 3;
|
||||
unsigned reg_ty = DESC_REGTY_MODREG(desc);
|
||||
op_modreg->misc = reg_ty;
|
||||
if (LIKELY(reg_ty < 2))
|
||||
reg_idx += prefix_rex & PREFIX_REXR ? 8 : 0;
|
||||
else if (reg_ty == 7 && (prefix_rex & PREFIX_REXR || prefix_evex & 0x80))
|
||||
return FD_ERR_UD; // REXR in 64-bit mode or EVEX.z with mask as dest
|
||||
if (UNLIKELY(reg_ty == FD_RT_VEC)) // REXRR ignored above in 32-bit mode
|
||||
reg_idx += prefix_rex & PREFIX_REXRR ? 16 : 0;
|
||||
else if (UNLIKELY(prefix_rex & PREFIX_REXRR))
|
||||
return FD_ERR_UD;
|
||||
op_modreg->type = FD_OT_REG;
|
||||
op_modreg->size = operand_sizes[DESC_MODREG_SIZE(desc)];
|
||||
op_modreg->reg = reg_idx;
|
||||
}
|
||||
|
||||
if (DESC_HAS_MODRM(desc))
|
||||
{
|
||||
FdOp* op_modrm = &instr->operands[DESC_MODRM_IDX(desc)];
|
||||
op_modrm->size = operand_sizes[DESC_MODRM_SIZE(desc)];
|
||||
|
||||
unsigned rm = op_byte & 0x07;
|
||||
if (op_byte >= 0xc0)
|
||||
{
|
||||
uint8_t reg_idx = rm;
|
||||
unsigned reg_ty = DESC_REGTY_MODRM(desc);
|
||||
op_modrm->misc = reg_ty;
|
||||
if (LIKELY(reg_ty < 2))
|
||||
reg_idx += prefix_rex & PREFIX_REXB ? 8 : 0;
|
||||
if (prefix_evex && reg_ty == 0) // vector registers only
|
||||
reg_idx += prefix_rex & PREFIX_REXX ? 16 : 0;
|
||||
op_modrm->type = FD_OT_REG;
|
||||
op_modrm->reg = reg_idx;
|
||||
}
|
||||
else
|
||||
{
|
||||
unsigned dispscale = 0;
|
||||
|
||||
if (UNLIKELY(prefix_evex)) {
|
||||
// EVEX.z for memory destination operand is UD.
|
||||
if (UNLIKELY(prefix_evex & 0x80) && DESC_MODRM_IDX(desc) == 0)
|
||||
return FD_ERR_UD;
|
||||
|
||||
// EVEX.b for memory-operand without broadcast support is UD.
|
||||
if (UNLIKELY(prefix_evex & 0x10)) {
|
||||
if (UNLIKELY(!DESC_EVEX_BCST(desc)))
|
||||
return FD_ERR_UD;
|
||||
if (UNLIKELY(DESC_EVEX_BCST16(desc)))
|
||||
dispscale = 1;
|
||||
else
|
||||
dispscale = prefix_rex & PREFIX_REXW ? 3 : 2;
|
||||
instr->segment |= dispscale << 6; // Store broadcast size
|
||||
op_modrm->type = FD_OT_MEMBCST;
|
||||
} else {
|
||||
dispscale = op_modrm->size - 1;
|
||||
op_modrm->type = FD_OT_MEM;
|
||||
}
|
||||
} else {
|
||||
op_modrm->type = FD_OT_MEM;
|
||||
}
|
||||
|
||||
// 16-bit address size implies different ModRM encoding
|
||||
if (UNLIKELY(addr_size == 1)) {
|
||||
ASSUME(mode == DECODE_32);
|
||||
if (UNLIKELY(DESC_VSIB(desc))) // 16-bit addr size + VSIB is UD
|
||||
return FD_ERR_UD;
|
||||
if (rm < 6)
|
||||
op_modrm->misc = rm & 1 ? FD_REG_DI : FD_REG_SI;
|
||||
else
|
||||
op_modrm->misc = FD_REG_NONE;
|
||||
|
||||
if (rm < 4)
|
||||
op_modrm->reg = rm & 2 ? FD_REG_BP : FD_REG_BX;
|
||||
else if (rm < 6 || (op_byte & 0xc7) == 0x06)
|
||||
op_modrm->reg = FD_REG_NONE;
|
||||
else
|
||||
op_modrm->reg = rm == 6 ? FD_REG_BP : FD_REG_BX;
|
||||
|
||||
const uint8_t* dispbase = &buffer[off];
|
||||
if (op_byte & 0x40) {
|
||||
if (UNLIKELY((off += 1) > len))
|
||||
return FD_ERR_PARTIAL;
|
||||
instr->disp = (int8_t) LOAD_LE_1(dispbase) * (1 << dispscale);
|
||||
} else if (op_byte & 0x80 || (op_byte & 0xc7) == 0x06) {
|
||||
if (UNLIKELY((off += 2) > len))
|
||||
return FD_ERR_PARTIAL;
|
||||
instr->disp = (int16_t) LOAD_LE_2(dispbase);
|
||||
} else {
|
||||
instr->disp = 0;
|
||||
}
|
||||
goto end_modrm;
|
||||
}
|
||||
|
||||
// SIB byte
|
||||
uint8_t base = rm;
|
||||
if (rm == 4) {
|
||||
if (UNLIKELY(off >= len))
|
||||
return FD_ERR_PARTIAL;
|
||||
uint8_t sib = buffer[off++];
|
||||
unsigned scale = sib & 0xc0;
|
||||
unsigned idx = (sib & 0x38) >> 3;
|
||||
idx += prefix_rex & PREFIX_REXX ? 8 : 0;
|
||||
base = sib & 0x07;
|
||||
if (idx == 4)
|
||||
idx = FD_REG_NONE;
|
||||
op_modrm->misc = scale | idx;
|
||||
} else {
|
||||
op_modrm->misc = FD_REG_NONE;
|
||||
}
|
||||
|
||||
if (UNLIKELY(DESC_VSIB(desc))) {
|
||||
// VSIB must have a memory operand with SIB byte.
|
||||
if (rm != 4)
|
||||
return FD_ERR_UD;
|
||||
_Static_assert(FD_REG_NONE == 0x3f, "unexpected FD_REG_NONE");
|
||||
// idx 4 is valid for VSIB
|
||||
if ((op_modrm->misc & 0x3f) == FD_REG_NONE)
|
||||
op_modrm->misc &= 0xc4;
|
||||
if (prefix_evex) // EVEX.V':EVEX.X:SIB.idx
|
||||
op_modrm->misc |= prefix_evex & 0x8 ? 0 : 0x10;
|
||||
}
|
||||
|
||||
// RIP-relative addressing only if SIB-byte is absent
|
||||
if (op_byte < 0x40 && rm == 5 && mode == DECODE_64)
|
||||
op_modrm->reg = FD_REG_IP;
|
||||
else if (op_byte < 0x40 && base == 5)
|
||||
op_modrm->reg = FD_REG_NONE;
|
||||
else
|
||||
op_modrm->reg = base + (prefix_rex & PREFIX_REXB ? 8 : 0);
|
||||
|
||||
const uint8_t* dispbase = &buffer[off];
|
||||
if (op_byte & 0x40) {
|
||||
if (UNLIKELY((off += 1) > len))
|
||||
return FD_ERR_PARTIAL;
|
||||
instr->disp = (int8_t) LOAD_LE_1(dispbase) * (1 << dispscale);
|
||||
} else if (op_byte & 0x80 || (op_byte < 0x40 && base == 5)) {
|
||||
if (UNLIKELY((off += 4) > len))
|
||||
return FD_ERR_PARTIAL;
|
||||
instr->disp = (int32_t) LOAD_LE_4(dispbase);
|
||||
} else {
|
||||
instr->disp = 0;
|
||||
}
|
||||
end_modrm:;
|
||||
}
|
||||
}
|
||||
|
||||
if (UNLIKELY(DESC_HAS_VEXREG(desc)))
|
||||
{
|
||||
FdOp* operand = &instr->operands[DESC_VEXREG_IDX(desc)];
|
||||
if (DESC_ZEROREG_VAL(desc)) {
|
||||
operand->type = FD_OT_REG;
|
||||
operand->size = 1;
|
||||
operand->reg = FD_REG_CL;
|
||||
operand->misc = FD_RT_GPL;
|
||||
} else {
|
||||
operand->type = FD_OT_REG;
|
||||
// Without VEX prefix, this encodes an implicit register
|
||||
operand->size = operand_sizes[DESC_VEXREG_SIZE(desc)];
|
||||
if (mode == DECODE_32)
|
||||
vex_operand &= 0x7;
|
||||
// Note: 32-bit will never UD here. EVEX.V' is caught above already.
|
||||
// Note: UD if > 16 for non-VEC. No EVEX-encoded instruction uses
|
||||
// EVEX.vvvv to refer to non-vector registers. Verified in parseinstrs.
|
||||
operand->reg = vex_operand;
|
||||
|
||||
unsigned reg_ty = DESC_REGTY_VEXREG(desc); // VEC GPL MSK FPU/TMM
|
||||
if (prefix_rex & PREFIX_VEX) { // TMM with VEX, FPU otherwise
|
||||
// In 64-bit mode: UD if FD_RT_MASK and vex_operand&8 != 0
|
||||
if (reg_ty == 2 && vex_operand >= 8)
|
||||
return FD_ERR_UD;
|
||||
if (UNLIKELY(reg_ty == 3)) // TMM
|
||||
operand->reg &= 0x7; // TODO: verify
|
||||
operand->misc = (06710 >> (3 * reg_ty)) & 0x7;
|
||||
} else {
|
||||
operand->misc = (04710 >> (3 * reg_ty)) & 0x7;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (vex_operand != 0)
|
||||
{
|
||||
// TODO: bit 3 ignored in 32-bit mode? unverified
|
||||
return FD_ERR_UD;
|
||||
}
|
||||
|
||||
uint32_t imm_control = UNLIKELY(DESC_IMM_CONTROL(desc));
|
||||
if (LIKELY(!imm_control)) {
|
||||
} else if (UNLIKELY(imm_control == 1))
|
||||
{
|
||||
// 1 = immediate constant 1, used for shifts
|
||||
FdOp* operand = &instr->operands[DESC_IMM_IDX(desc)];
|
||||
operand->type = FD_OT_IMM;
|
||||
operand->size = 1;
|
||||
instr->imm = 1;
|
||||
}
|
||||
else if (UNLIKELY(imm_control == 2))
|
||||
{
|
||||
// 2 = memory, address-sized, used for mov with moffs operand
|
||||
FdOp* operand = &instr->operands[DESC_IMM_IDX(desc)];
|
||||
operand->type = FD_OT_MEM;
|
||||
operand->size = operand_sizes[DESC_IMM_SIZE(desc)];
|
||||
operand->reg = FD_REG_NONE;
|
||||
operand->misc = FD_REG_NONE;
|
||||
|
||||
int moffsz = 1 << addr_size;
|
||||
if (UNLIKELY(off + moffsz > len))
|
||||
return FD_ERR_PARTIAL;
|
||||
if (moffsz == 2)
|
||||
instr->disp = LOAD_LE_2(&buffer[off]);
|
||||
if (moffsz == 4)
|
||||
instr->disp = LOAD_LE_4(&buffer[off]);
|
||||
if (LIKELY(moffsz == 8))
|
||||
instr->disp = LOAD_LE_8(&buffer[off]);
|
||||
off += moffsz;
|
||||
}
|
||||
else if (UNLIKELY(imm_control == 3))
|
||||
{
|
||||
// 3 = register in imm8[7:4], used for RVMR encoding with VBLENDVP[SD]
|
||||
FdOp* operand = &instr->operands[DESC_IMM_IDX(desc)];
|
||||
operand->type = FD_OT_REG;
|
||||
operand->size = op_size;
|
||||
operand->misc = FD_RT_VEC;
|
||||
|
||||
if (UNLIKELY(off + 1 > len))
|
||||
return FD_ERR_PARTIAL;
|
||||
uint8_t reg = (uint8_t) LOAD_LE_1(&buffer[off]);
|
||||
off += 1;
|
||||
|
||||
if (mode == DECODE_32)
|
||||
reg &= 0x7f;
|
||||
operand->reg = reg >> 4;
|
||||
instr->imm = reg & 0x0f;
|
||||
}
|
||||
else if (imm_control != 0)
|
||||
{
|
||||
// 4/5 = immediate, operand-sized/8 bit
|
||||
// 6/7 = offset, operand-sized/8 bit (used for jumps/calls)
|
||||
int imm_byte = imm_control & 1;
|
||||
int imm_offset = imm_control & 2;
|
||||
|
||||
FdOp* operand = &instr->operands[DESC_IMM_IDX(desc)];
|
||||
operand->type = FD_OT_IMM;
|
||||
|
||||
if (imm_byte) {
|
||||
if (UNLIKELY(off + 1 > len))
|
||||
return FD_ERR_PARTIAL;
|
||||
instr->imm = (int8_t) LOAD_LE_1(&buffer[off++]);
|
||||
operand->size = DESC_IMM_SIZE(desc) & 1 ? 1 : op_size;
|
||||
} else {
|
||||
operand->size = operand_sizes[DESC_IMM_SIZE(desc)];
|
||||
|
||||
uint8_t imm_size;
|
||||
if (UNLIKELY(instr->type == FDI_RET || instr->type == FDI_RETF ||
|
||||
instr->type == FDI_SSE_EXTRQ ||
|
||||
instr->type == FDI_SSE_INSERTQ))
|
||||
imm_size = 2;
|
||||
else if (UNLIKELY(instr->type == FDI_JMPF || instr->type == FDI_CALLF))
|
||||
imm_size = (1 << op_size >> 1) + 2;
|
||||
else if (UNLIKELY(instr->type == FDI_ENTER))
|
||||
imm_size = 3;
|
||||
else if (instr->type == FDI_MOVABS)
|
||||
imm_size = (1 << op_size >> 1);
|
||||
else
|
||||
imm_size = op_size == 2 ? 2 : 4;
|
||||
|
||||
if (UNLIKELY(off + imm_size > len))
|
||||
return FD_ERR_PARTIAL;
|
||||
|
||||
if (imm_size == 2)
|
||||
instr->imm = (int16_t) LOAD_LE_2(&buffer[off]);
|
||||
else if (imm_size == 3)
|
||||
instr->imm = LOAD_LE_3(&buffer[off]);
|
||||
else if (imm_size == 4)
|
||||
instr->imm = (int32_t) LOAD_LE_4(&buffer[off]);
|
||||
else if (imm_size == 6)
|
||||
instr->imm = LOAD_LE_4(&buffer[off]) | LOAD_LE_2(&buffer[off+4]) << 32;
|
||||
else if (imm_size == 8)
|
||||
instr->imm = (int64_t) LOAD_LE_8(&buffer[off]);
|
||||
off += imm_size;
|
||||
}
|
||||
|
||||
if (imm_offset)
|
||||
{
|
||||
if (instr->address != 0)
|
||||
instr->imm += instr->address + off;
|
||||
else
|
||||
operand->type = FD_OT_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
skip_modrm:
|
||||
if (UNLIKELY(prefixes[PF_LOCK])) {
|
||||
if (!DESC_LOCK(desc) || instr->operands[0].type != FD_OT_MEM)
|
||||
return FD_ERR_UD;
|
||||
instr->flags |= FD_FLAG_LOCK;
|
||||
}
|
||||
|
||||
if (UNLIKELY(DESC_LEGACY(desc))) {
|
||||
// Without REX prefix, convert one-byte GP regs to high-byte regs
|
||||
// This actually only applies to SZ8/MOVSX/MOVZX; but no VEX-encoded
|
||||
// instructions have a byte-sized GP register in the first two operands.
|
||||
if (!(prefix_rex & PREFIX_REX)) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
FdOp* operand = &instr->operands[i];
|
||||
if (operand->type == FD_OT_NONE)
|
||||
break;
|
||||
if (operand->type == FD_OT_REG && operand->misc == FD_RT_GPL &&
|
||||
operand->size == 1 && operand->reg >= 4)
|
||||
operand->misc = FD_RT_GPH;
|
||||
}
|
||||
}
|
||||
|
||||
if (instr->type == FDI_XCHG_NOP) {
|
||||
// Only 4890, 90, and 6690 are true NOPs.
|
||||
if (instr->operands[0].reg == 0) {
|
||||
instr->operands[0].type = FD_OT_NONE;
|
||||
instr->operands[1].type = FD_OT_NONE;
|
||||
instr->type = FD_HAS_REP(instr) ? FDI_PAUSE : FDI_NOP;
|
||||
} else if ((instr->operands[0].reg & 7) == 0 && FD_HAS_REP(instr)) {
|
||||
// On Intel, REX.B is ignored for F3.90.
|
||||
instr->operands[0].type = FD_OT_NONE;
|
||||
instr->operands[1].type = FD_OT_NONE;
|
||||
instr->type = FDI_PAUSE;
|
||||
} else {
|
||||
instr->type = FDI_XCHG;
|
||||
}
|
||||
}
|
||||
|
||||
if (UNLIKELY(instr->type == FDI_3DNOW)) {
|
||||
unsigned opc3dn = instr->imm;
|
||||
if (opc3dn & 0x40)
|
||||
return FD_ERR_UD;
|
||||
uint64_t msk = opc3dn & 0x80 ? 0x88d144d144d14400 : 0x30003000;
|
||||
if (!(msk >> (opc3dn & 0x3f) & 1))
|
||||
return FD_ERR_UD;
|
||||
}
|
||||
|
||||
instr->operandsz = UNLIKELY(DESC_INSTR_WIDTH(desc)) ? op_size - 1 : 0;
|
||||
} else {
|
||||
instr->operandsz = 0;
|
||||
}
|
||||
|
||||
instr->size = off;
|
||||
|
||||
return off;
|
||||
}
|
||||
62
third_party/fadec/encode-test.c
vendored
Normal file
62
third_party/fadec/encode-test.c
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <fadec-enc.h>
|
||||
|
||||
|
||||
static
|
||||
void
|
||||
print_hex(const uint8_t* buf, size_t len)
|
||||
{
|
||||
for (size_t i = 0; i < len; i++)
|
||||
printf("%02x", buf[i]);
|
||||
}
|
||||
|
||||
static
|
||||
int
|
||||
test(uint8_t* buf, const char* name, uint64_t mnem, uint64_t op0, uint64_t op1, uint64_t op2, uint64_t op3, const void* exp, size_t exp_len)
|
||||
{
|
||||
memset(buf, 0, 16);
|
||||
|
||||
uint8_t* inst = buf;
|
||||
int res = fe_enc64(&inst, mnem, op0, op1, op2, op3);
|
||||
if ((res != 0) != (exp_len == 0)) goto fail;
|
||||
if (inst - buf != (ptrdiff_t) exp_len) goto fail;
|
||||
if (memcmp(buf, exp, exp_len)) goto fail;
|
||||
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
printf("Failed case %s:\n", name);
|
||||
printf(" Exp (%2zu): ", exp_len);
|
||||
print_hex(exp, exp_len);
|
||||
printf("\n Got (%2zd): ", inst - buf);
|
||||
print_hex(buf, inst - buf);
|
||||
printf("\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
#define TEST2(str, exp, exp_len, mnem, flags, op0, op1, op2, op3, ...) test(buf, str, FE_ ## mnem|flags, op0, op1, op2, op3, exp, exp_len)
|
||||
#define TEST1(str, exp, ...) TEST2(str, exp, sizeof(exp)-1, __VA_ARGS__, 0, 0, 0, 0, 0)
|
||||
#define TEST(exp, ...) failed |= TEST1(#__VA_ARGS__, exp, __VA_ARGS__)
|
||||
|
||||
int
|
||||
main(int argc, char** argv)
|
||||
{
|
||||
(void) argc; (void) argv;
|
||||
|
||||
int failed = 0;
|
||||
uint8_t buf[16];
|
||||
|
||||
// VSIB encoding doesn't differ for this API
|
||||
#define FE_MEMV FE_MEM
|
||||
#define FE_PTR(off) ((intptr_t) buf + (off))
|
||||
#define FLAGMASK(flags, mask) (flags | FE_MASK(mask & 7))
|
||||
#include "encode-test.inc"
|
||||
|
||||
puts(failed ? "Some tests FAILED" : "All tests PASSED");
|
||||
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||
}
|
||||
2192
third_party/fadec/encode-test.inc
vendored
Normal file
2192
third_party/fadec/encode-test.inc
vendored
Normal file
File diff suppressed because it is too large
Load Diff
460
third_party/fadec/encode.c
vendored
Normal file
460
third_party/fadec/encode.c
vendored
Normal file
@@ -0,0 +1,460 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <fadec-enc.h>
|
||||
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define LIKELY(x) __builtin_expect((x), 1)
|
||||
#define UNLIKELY(x) __builtin_expect((x), 0)
|
||||
#else
|
||||
#define LIKELY(x) (x)
|
||||
#define UNLIKELY(x) (x)
|
||||
#endif
|
||||
|
||||
#define OPC_66 0x80000
|
||||
#define OPC_F2 0x100000
|
||||
#define OPC_F3 0x200000
|
||||
#define OPC_REXW 0x400000
|
||||
#define OPC_LOCK 0x800000
|
||||
#define OPC_VEXL0 0x1000000
|
||||
#define OPC_VEXL1 0x1800000
|
||||
#define OPC_EVEXL0 0x2000000
|
||||
#define OPC_EVEXL1 0x2800000
|
||||
#define OPC_EVEXL2 0x3000000
|
||||
#define OPC_EVEXL3 0x3800000
|
||||
#define OPC_EVEXB 0x4000000
|
||||
#define OPC_VSIB 0x8000000
|
||||
#define OPC_67 FE_ADDR32
|
||||
#define OPC_SEG_MSK 0xe0000000
|
||||
#define OPC_JMPL FE_JMPL
|
||||
#define OPC_MASK_MSK 0xe00000000
|
||||
#define OPC_EVEXZ 0x1000000000
|
||||
#define OPC_USER_MSK (OPC_67|OPC_SEG_MSK|OPC_MASK_MSK)
|
||||
#define OPC_FORCE_SIB 0x2000000000
|
||||
#define OPC_DOWNGRADE_VEX 0x4000000000
|
||||
#define OPC_DOWNGRADE_VEX_FLIPW 0x40000000000
|
||||
#define OPC_EVEX_DISP8SCALE 0x38000000000
|
||||
#define OPC_GPH_OP0 0x200000000000
|
||||
#define OPC_GPH_OP1 0x400000000000
|
||||
|
||||
#define EPFX_REX_MSK 0x43f
|
||||
#define EPFX_REX 0x20
|
||||
#define EPFX_EVEX 0x40
|
||||
#define EPFX_REXR 0x10
|
||||
#define EPFX_REXX 0x08
|
||||
#define EPFX_REXB 0x04
|
||||
#define EPFX_REXR4 0x02
|
||||
#define EPFX_REXB4 0x01
|
||||
#define EPFX_REXX4 0x400
|
||||
#define EPFX_VVVV_IDX 11
|
||||
|
||||
static bool op_mem(FeOp op) { return op < 0; }
|
||||
static bool op_reg(FeOp op) { return op >= 0; }
|
||||
static bool op_reg_gpl(FeOp op) { return (op & ~0x1f) == 0x100; }
|
||||
static bool op_reg_gph(FeOp op) { return (op & ~0x3) == 0x204; }
|
||||
static bool op_reg_xmm(FeOp op) { return (op & ~0x1f) == 0x600; }
|
||||
static int64_t op_mem_offset(FeOp op) { return (int32_t) op; }
|
||||
static unsigned op_mem_base(FeOp op) { return (op >> 32) & 0xfff; }
|
||||
static unsigned op_mem_idx(FeOp op) { return (op >> 44) & 0xfff; }
|
||||
static unsigned op_mem_scale(FeOp op) { return (op >> 56) & 0xf; }
|
||||
static unsigned op_reg_idx(FeOp op) { return op & 0xff; }
|
||||
static bool op_imm_n(FeOp imm, unsigned immsz) {
|
||||
if (immsz == 0 && !imm) return true;
|
||||
if (immsz == 1 && (int8_t) imm == imm) return true;
|
||||
if (immsz == 2 && (int16_t) imm == imm) return true;
|
||||
if (immsz == 3 && (imm&0xffffff) == imm) return true;
|
||||
if (immsz == 4 && (int32_t) imm == imm) return true;
|
||||
if (immsz == 8 && (int64_t) imm == imm) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static
|
||||
unsigned
|
||||
opc_size(uint64_t opc, uint64_t epfx)
|
||||
{
|
||||
unsigned res = 1;
|
||||
if (UNLIKELY(opc & OPC_EVEXL0)) {
|
||||
res += 4;
|
||||
} else if (UNLIKELY(opc & OPC_VEXL0)) {
|
||||
if (opc & (OPC_REXW|0x20000) || epfx & (EPFX_REXX|EPFX_REXB))
|
||||
res += 3;
|
||||
else
|
||||
res += 2;
|
||||
} else {
|
||||
if (opc & OPC_LOCK) res++;
|
||||
if (opc & OPC_66) res++;
|
||||
if (opc & (OPC_F2|OPC_F3)) res++;
|
||||
if (opc & OPC_REXW || epfx & EPFX_REX_MSK) res++;
|
||||
if (opc & 0x30000) res++;
|
||||
if (opc & 0x20000) res++;
|
||||
}
|
||||
if (opc & OPC_SEG_MSK) res++;
|
||||
if (opc & OPC_67) res++;
|
||||
if (opc & 0x8000) res++;
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
int
|
||||
enc_opc(uint8_t** restrict buf, uint64_t opc, uint64_t epfx)
|
||||
{
|
||||
if (opc & OPC_SEG_MSK)
|
||||
*(*buf)++ = (0x65643e362e2600 >> (8 * ((opc >> 29) & 7))) & 0xff;
|
||||
if (opc & OPC_67) *(*buf)++ = 0x67;
|
||||
if (opc & OPC_EVEXL0) {
|
||||
*(*buf)++ = 0x62;
|
||||
unsigned b1 = opc >> 16 & 7;
|
||||
if (!(epfx & EPFX_REXR)) b1 |= 0x80;
|
||||
if (!(epfx & EPFX_REXX)) b1 |= 0x40;
|
||||
if (!(epfx & EPFX_REXB)) b1 |= 0x20;
|
||||
if (!(epfx & EPFX_REXR4)) b1 |= 0x10;
|
||||
if ((epfx & EPFX_REXB4)) b1 |= 0x08;
|
||||
*(*buf)++ = b1;
|
||||
unsigned b2 = opc >> 20 & 3;
|
||||
if (!(epfx & EPFX_REXX4)) b2 |= 0x04;
|
||||
b2 |= (~(epfx >> EPFX_VVVV_IDX) & 0xf) << 3;
|
||||
if (opc & OPC_REXW) b2 |= 0x80;
|
||||
*(*buf)++ = b2;
|
||||
unsigned b3 = opc >> 33 & 7;
|
||||
b3 |= (~(epfx >> EPFX_VVVV_IDX) & 0x10) >> 1;
|
||||
if (opc & OPC_EVEXB) b3 |= 0x10;
|
||||
b3 |= (opc >> 23 & 3) << 5;
|
||||
if (opc & OPC_EVEXZ) b3 |= 0x80;
|
||||
*(*buf)++ = b3;
|
||||
} else if (opc & OPC_VEXL0) {
|
||||
if (epfx & (EPFX_REXR4|EPFX_REXX4|EPFX_REXB4|(0x10<<EPFX_VVVV_IDX))) return -1;
|
||||
bool vex3 = opc & (OPC_REXW|0x20000) || epfx & (EPFX_REXX|EPFX_REXB);
|
||||
unsigned pp = opc >> 20 & 3;
|
||||
*(*buf)++ = 0xc4 | !vex3;
|
||||
unsigned b2 = pp | (opc & 0x800000 ? 0x4 : 0);
|
||||
if (vex3) {
|
||||
unsigned b1 = opc >> 16 & 7;
|
||||
if (!(epfx & EPFX_REXR)) b1 |= 0x80;
|
||||
if (!(epfx & EPFX_REXX)) b1 |= 0x40;
|
||||
if (!(epfx & EPFX_REXB)) b1 |= 0x20;
|
||||
*(*buf)++ = b1;
|
||||
if (opc & OPC_REXW) b2 |= 0x80;
|
||||
} else {
|
||||
if (!(epfx & EPFX_REXR)) b2 |= 0x80;
|
||||
}
|
||||
b2 |= (~(epfx >> EPFX_VVVV_IDX) & 0xf) << 3;
|
||||
*(*buf)++ = b2;
|
||||
} else {
|
||||
if (opc & OPC_LOCK) *(*buf)++ = 0xF0;
|
||||
if (opc & OPC_66) *(*buf)++ = 0x66;
|
||||
if (opc & OPC_F2) *(*buf)++ = 0xF2;
|
||||
if (opc & OPC_F3) *(*buf)++ = 0xF3;
|
||||
if (opc & OPC_REXW || epfx & (EPFX_REX_MSK)) {
|
||||
unsigned rex = 0x40;
|
||||
if (opc & OPC_REXW) rex |= 8;
|
||||
if (epfx & EPFX_REXR) rex |= 4;
|
||||
if (epfx & EPFX_REXX) rex |= 2;
|
||||
if (epfx & EPFX_REXB) rex |= 1;
|
||||
*(*buf)++ = rex;
|
||||
}
|
||||
if (opc & 0x30000) *(*buf)++ = 0x0F;
|
||||
if ((opc & 0x30000) == 0x20000) *(*buf)++ = 0x38;
|
||||
if ((opc & 0x30000) == 0x30000) *(*buf)++ = 0x3A;
|
||||
}
|
||||
*(*buf)++ = opc & 0xff;
|
||||
if (opc & 0x8000) *(*buf)++ = (opc >> 8) & 0xff;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int
|
||||
enc_imm(uint8_t** restrict buf, uint64_t imm, unsigned immsz)
|
||||
{
|
||||
if (!op_imm_n(imm, immsz)) return -1;
|
||||
for (unsigned i = 0; i < immsz; i++)
|
||||
*(*buf)++ = imm >> 8 * i;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int
|
||||
enc_o(uint8_t** restrict buf, uint64_t opc, uint64_t epfx, uint64_t op0)
|
||||
{
|
||||
if (op_reg_idx(op0) & 0x8) epfx |= EPFX_REXB;
|
||||
|
||||
// NB: this cannot happen. There is only one O-encoded instruction which
|
||||
// accepts high-byte registers (b0+/MOVABS Rb,Ib), which will never have a
|
||||
// REx prefix if the operand is a high-byte register.
|
||||
// bool has_rex = opc & OPC_REXW || epfx & EPFX_REX_MSK;
|
||||
// if (has_rex && op_reg_gph(op0)) return -1;
|
||||
|
||||
if (enc_opc(buf, opc, epfx)) return -1;
|
||||
*(*buf - 1) = (*(*buf - 1) & 0xf8) | (op_reg_idx(op0) & 0x7);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static
|
||||
int
|
||||
enc_mr(uint8_t** restrict buf, uint64_t opc, uint64_t epfx, uint64_t op0,
|
||||
uint64_t op1, unsigned immsz)
|
||||
{
|
||||
// If !op_reg(op1), it is a constant value for ModRM.reg
|
||||
if (op_reg(op0) && (op_reg_idx(op0) & 0x8)) epfx |= EPFX_REXB;
|
||||
if (op_reg(op0) && (op_reg_idx(op0) & 0x10))
|
||||
epfx |= 0 ? EPFX_REXB4 : EPFX_REXX|EPFX_EVEX;
|
||||
if (op_mem(op0) && (op_mem_base(op0) & 0x8)) epfx |= EPFX_REXB;
|
||||
if (op_mem(op0) && (op_mem_base(op0) & 0x10)) epfx |= EPFX_REXB4;
|
||||
if (op_mem(op0) && (op_mem_idx(op0) & 0x8)) epfx |= EPFX_REXX;
|
||||
if (op_mem(op0) && (op_mem_idx(op0) & 0x10))
|
||||
epfx |= opc & OPC_VSIB ? 0x10<<EPFX_VVVV_IDX : EPFX_REXX4;
|
||||
if (op_reg(op1) && (op_reg_idx(op1) & 0x8)) epfx |= EPFX_REXR;
|
||||
if (op_reg(op1) && (op_reg_idx(op1) & 0x10)) epfx |= EPFX_REXR4;
|
||||
|
||||
bool has_rex = opc & (OPC_REXW|OPC_VEXL0|OPC_EVEXL0) || (epfx & EPFX_REX_MSK);
|
||||
if (has_rex && (op_reg_gph(op0) || op_reg_gph(op1))) return -1;
|
||||
|
||||
if (epfx & (EPFX_EVEX|EPFX_REXB4|EPFX_REXX4|EPFX_REXR4|(0x10<<EPFX_VVVV_IDX))) {
|
||||
if (!(opc & OPC_EVEXL0)) return -1;
|
||||
} else if (opc & OPC_DOWNGRADE_VEX) { // downgrade EVEX to VEX
|
||||
// clear EVEX and disp8scale, set VEX
|
||||
opc = (opc & ~(uint64_t) (OPC_EVEXL0|OPC_EVEX_DISP8SCALE)) | OPC_VEXL0;
|
||||
if (opc & OPC_DOWNGRADE_VEX_FLIPW)
|
||||
opc ^= OPC_REXW;
|
||||
}
|
||||
|
||||
if (LIKELY(op_reg(op0))) {
|
||||
if (enc_opc(buf, opc, epfx)) return -1;
|
||||
*(*buf)++ = 0xc0 | ((op_reg_idx(op1) & 7) << 3) | (op_reg_idx(op0) & 7);
|
||||
return 0;
|
||||
}
|
||||
|
||||
unsigned opcsz = opc_size(opc, epfx);
|
||||
|
||||
int mod = 0, reg = op1 & 7, rm;
|
||||
int scale = 0, idx = 4, base = 0;
|
||||
int32_t off = op_mem_offset(op0);
|
||||
bool withsib = opc & OPC_FORCE_SIB;
|
||||
|
||||
if (!!op_mem_idx(op0) != !!op_mem_scale(op0)) return -1;
|
||||
if (!op_mem_idx(op0) && (opc & OPC_VSIB)) return -1;
|
||||
if (op_mem_idx(op0))
|
||||
{
|
||||
if (opc & OPC_VSIB)
|
||||
{
|
||||
if (!op_reg_xmm(op_mem_idx(op0))) return -1;
|
||||
// EVEX VSIB requires non-zero opmask
|
||||
if ((opc & OPC_EVEXL0) && !(opc & OPC_MASK_MSK)) return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!op_reg_gpl(op_mem_idx(op0))) return -1;
|
||||
if (op_reg_idx(op_mem_idx(op0)) == 4) return -1;
|
||||
}
|
||||
idx = op_mem_idx(op0) & 7;
|
||||
int scalabs = op_mem_scale(op0);
|
||||
if (scalabs & (scalabs - 1)) return -1;
|
||||
scale = (scalabs & 0xA ? 1 : 0) | (scalabs & 0xC ? 2 : 0);
|
||||
withsib = true;
|
||||
}
|
||||
|
||||
unsigned dispsz = 0;
|
||||
if (!op_mem_base(op0))
|
||||
{
|
||||
base = 5;
|
||||
rm = 4;
|
||||
dispsz = 4;
|
||||
}
|
||||
else if (op_mem_base(op0) == FE_IP)
|
||||
{
|
||||
rm = 5;
|
||||
dispsz = 4;
|
||||
// Adjust offset, caller doesn't know instruction length.
|
||||
off -= opcsz + 5 + immsz;
|
||||
if (withsib) return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!op_reg_gpl(op_mem_base(op0))) return -1;
|
||||
rm = op_reg_idx(op_mem_base(op0)) & 7;
|
||||
if (withsib || rm == 4) {
|
||||
base = rm;
|
||||
rm = 4;
|
||||
}
|
||||
if (off) {
|
||||
unsigned disp8scale = (opc & OPC_EVEX_DISP8SCALE) >> 39;
|
||||
if (!(off & ((1 << disp8scale) - 1)) && op_imm_n(off >> disp8scale, 1)) {
|
||||
mod = 0x40;
|
||||
dispsz = 1;
|
||||
off >>= disp8scale;
|
||||
} else {
|
||||
mod = 0x80;
|
||||
dispsz = 4;
|
||||
}
|
||||
} else if (rm == 5) {
|
||||
mod = 0x40;
|
||||
dispsz = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (opcsz + 1 + (rm == 4) + dispsz + immsz > 15) return -1;
|
||||
|
||||
if (enc_opc(buf, opc, epfx)) return -1;
|
||||
*(*buf)++ = mod | (reg << 3) | rm;
|
||||
if (UNLIKELY(rm == 4))
|
||||
*(*buf)++ = (scale << 6) | (idx << 3) | base;
|
||||
return enc_imm(buf, off, dispsz);
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
ENC_NP, ENC_M, ENC_R, ENC_M1, ENC_MC, ENC_MR, ENC_RM, ENC_RMA, ENC_MRC,
|
||||
ENC_AM, ENC_MA, ENC_I, ENC_O, ENC_OA, ENC_S, ENC_A, ENC_D, ENC_FD, ENC_TD,
|
||||
ENC_IM,
|
||||
ENC_RVM, ENC_RVMR, ENC_RMV, ENC_VM, ENC_MVR, ENC_MRV,
|
||||
ENC_MAX
|
||||
} Encoding;
|
||||
|
||||
struct EncodingInfo {
|
||||
uint8_t modrm : 2;
|
||||
uint8_t modreg : 2;
|
||||
uint8_t vexreg : 2;
|
||||
uint8_t immidx : 2;
|
||||
// 0 = normal or jump, 1 = constant 1, 2 = address-size, 3 = RVMR
|
||||
uint8_t immctl : 3;
|
||||
uint8_t zregidx : 2;
|
||||
uint8_t zregval : 1;
|
||||
};
|
||||
|
||||
const struct EncodingInfo encoding_infos[ENC_MAX] = {
|
||||
[ENC_NP] = { 0 },
|
||||
[ENC_M] = { .modrm = 0x0^3, .immidx = 1 },
|
||||
[ENC_R] = { .modreg = 0x0^3 },
|
||||
[ENC_M1] = { .modrm = 0x0^3, .immctl = 1, .immidx = 1 },
|
||||
[ENC_MC] = { .modrm = 0x0^3, .zregidx = 0x1^3, .zregval = 1 },
|
||||
[ENC_MR] = { .modrm = 0x0^3, .modreg = 0x1^3, .immidx = 2 },
|
||||
[ENC_RM] = { .modrm = 0x1^3, .modreg = 0x0^3, .immidx = 2 },
|
||||
[ENC_RMA] = { .modrm = 0x1^3, .modreg = 0x0^3, .zregidx = 0x2^3, .zregval = 0 },
|
||||
[ENC_MRC] = { .modrm = 0x0^3, .modreg = 0x1^3, .zregidx = 0x2^3, .zregval = 1 },
|
||||
[ENC_AM] = { .modrm = 0x1^3, .zregidx = 0x0^3, .zregval = 0 },
|
||||
[ENC_MA] = { .modrm = 0x0^3, .zregidx = 0x1^3, .zregval = 0 },
|
||||
[ENC_I] = { .immidx = 0 },
|
||||
[ENC_O] = { .modreg = 0x0^3, .immidx = 1 },
|
||||
[ENC_OA] = { .modreg = 0x0^3, .zregidx = 0x1^3, .zregval = 0 },
|
||||
[ENC_S] = { 0 },
|
||||
[ENC_A] = { .zregidx = 0x0^3, .zregval = 0, .immidx = 1 },
|
||||
[ENC_D] = { .immidx = 0 },
|
||||
[ENC_FD] = { .zregidx = 0x0^3, .zregval = 0, .immctl = 2, .immidx = 1 },
|
||||
[ENC_TD] = { .zregidx = 0x1^3, .zregval = 0, .immctl = 2, .immidx = 0 },
|
||||
[ENC_IM] = { .modrm = 0x1^3, .immidx = 0 },
|
||||
[ENC_RVM] = { .modrm = 0x2^3, .modreg = 0x0^3, .vexreg = 0x1^3, .immidx = 3 },
|
||||
[ENC_RVMR] = { .modrm = 0x2^3, .modreg = 0x0^3, .vexreg = 0x1^3, .immctl = 3, .immidx = 3 },
|
||||
[ENC_RMV] = { .modrm = 0x1^3, .modreg = 0x0^3, .vexreg = 0x2^3 },
|
||||
[ENC_VM] = { .modrm = 0x1^3, .vexreg = 0x0^3, .immidx = 2 },
|
||||
[ENC_MVR] = { .modrm = 0x0^3, .modreg = 0x2^3, .vexreg = 0x1^3 },
|
||||
[ENC_MRV] = { .modrm = 0x0^3, .modreg = 0x1^3, .vexreg = 0x2^3 },
|
||||
};
|
||||
|
||||
static const uint64_t alt_tab[] = {
|
||||
#include <fadec-encode-private.inc>
|
||||
};
|
||||
|
||||
int
|
||||
fe_enc64_impl(uint8_t** restrict buf, uint64_t opc, FeOp op0, FeOp op1,
|
||||
FeOp op2, FeOp op3)
|
||||
{
|
||||
uint8_t* buf_start = *buf;
|
||||
uint64_t ops[4] = {op0, op1, op2, op3};
|
||||
|
||||
uint64_t epfx = 0;
|
||||
// Doesn't change between variants
|
||||
if ((opc & OPC_GPH_OP0) && op_reg_gpl(op0) && op0 >= FE_SP)
|
||||
epfx |= EPFX_REX;
|
||||
else if (!(opc & OPC_GPH_OP0) && op_reg_gph(op0))
|
||||
goto fail;
|
||||
if ((opc & OPC_GPH_OP1) && op_reg_gpl(op1) && op1 >= FE_SP)
|
||||
epfx |= EPFX_REX;
|
||||
else if (!(opc & OPC_GPH_OP1) && op_reg_gph(op1))
|
||||
goto fail;
|
||||
|
||||
try_encode:;
|
||||
unsigned enc = (opc >> 51) & 0x1f;
|
||||
const struct EncodingInfo* ei = &encoding_infos[enc];
|
||||
|
||||
int64_t imm = 0xcc;
|
||||
unsigned immsz = (opc >> 47) & 0xf;
|
||||
|
||||
if (UNLIKELY(ei->zregidx && op_reg_idx(ops[ei->zregidx^3]) != ei->zregval))
|
||||
goto next;
|
||||
|
||||
if (UNLIKELY(enc == ENC_S)) {
|
||||
if ((op_reg_idx(op0) << 3 & 0x20) != (opc & 0x20)) goto next;
|
||||
opc |= op_reg_idx(op0) << 3;
|
||||
}
|
||||
|
||||
if (immsz) {
|
||||
imm = ops[ei->immidx];
|
||||
if (UNLIKELY(ei->immctl)) {
|
||||
if (ei->immctl == 2) {
|
||||
immsz = UNLIKELY(opc & OPC_67) ? 4 : 8;
|
||||
if (immsz == 4) imm = (int32_t) imm; // address are zero-extended
|
||||
} else if (ei->immctl == 3) {
|
||||
if (!op_reg_xmm(imm)) goto fail;
|
||||
imm = op_reg_idx(imm) << 4;
|
||||
if (!op_imm_n(imm, 1)) goto fail;
|
||||
} else if (ei->immctl == 1) {
|
||||
if (imm != 1) goto next;
|
||||
immsz = 0;
|
||||
}
|
||||
} else if (enc == ENC_D) {
|
||||
imm -= (int64_t) *buf + opc_size(opc, epfx) + immsz;
|
||||
bool has_alt = opc >> 56 != 0;
|
||||
bool skip_to_alt = has_alt && UNLIKELY(opc & FE_JMPL);
|
||||
if (skip_to_alt || !op_imm_n(imm, immsz)) {
|
||||
if (!has_alt) goto fail;
|
||||
// JMP/Jcc special case
|
||||
immsz = 4;
|
||||
if (opc & 0x80) { // JMP
|
||||
opc -= 2; // Convert opcode 0xeb to 0xe9
|
||||
imm -= 3; // 3 extra immediate bytes
|
||||
} else { // Jcc
|
||||
opc += 0x10010; // Add 0f escape + 0x10 to opcode
|
||||
imm -= 4; // 0f escape + 3 extra immediate bytes
|
||||
}
|
||||
if (!op_imm_n(imm, immsz)) goto fail;
|
||||
}
|
||||
} else {
|
||||
if (!op_imm_n(imm, immsz)) goto next;
|
||||
}
|
||||
}
|
||||
|
||||
// NOP has no operands, so this must be the 32-bit OA XCHG
|
||||
if ((opc & 0xfffffff) == 0x90 && ops[0] == FE_AX) goto next;
|
||||
|
||||
if (UNLIKELY(enc == ENC_R)) {
|
||||
if (enc_mr(buf, opc, epfx, 0, ops[0], immsz)) goto fail;
|
||||
} else if (ei->modrm) {
|
||||
FeOp modreg = ei->modreg ? ops[ei->modreg^3] : (opc & 0xff00) >> 8;
|
||||
if (ei->vexreg)
|
||||
epfx |= ((uint64_t) op_reg_idx(ops[ei->vexreg^3])) << EPFX_VVVV_IDX;
|
||||
// Can fail for upgrade to EVEX due to high register numbers
|
||||
if (enc_mr(buf, opc, epfx, ops[ei->modrm^3], modreg, immsz)) goto next;
|
||||
} else if (ei->modreg) {
|
||||
if (enc_o(buf, opc, epfx, ops[ei->modreg^3])) goto fail;
|
||||
} else {
|
||||
if (enc_opc(buf, opc, epfx)) goto fail;
|
||||
}
|
||||
|
||||
if (immsz)
|
||||
if (enc_imm(buf, imm, immsz)) goto fail;
|
||||
|
||||
return 0;
|
||||
|
||||
next:;
|
||||
uint64_t alt = opc >> 56;
|
||||
if (alt) { // try alternative encoding, if available
|
||||
opc = alt_tab[alt] | (opc & OPC_USER_MSK);
|
||||
goto try_encode;
|
||||
}
|
||||
|
||||
fail:
|
||||
// Don't advance buffer on error; though we shouldn't write anything.
|
||||
*buf = buf_start;
|
||||
return -1;
|
||||
}
|
||||
64
third_party/fadec/encode2-test.c
vendored
Normal file
64
third_party/fadec/encode2-test.c
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <fadec-enc2.h>
|
||||
|
||||
|
||||
static
|
||||
void print_hex(const uint8_t* buf, size_t len) {
|
||||
for (size_t i = 0; i < len; i++)
|
||||
printf("%02x", buf[i]);
|
||||
}
|
||||
|
||||
static int
|
||||
check(const uint8_t* buf, const void* exp, size_t exp_len, unsigned res, const char* name) {
|
||||
if (res == exp_len && !memcmp(buf, exp, exp_len))
|
||||
return 0;
|
||||
printf("Failed case (new) %s:\n", name);
|
||||
printf(" Exp (%2zu): ", exp_len);
|
||||
print_hex((const uint8_t*)exp, exp_len);
|
||||
printf("\n Got (%2u): ", res);
|
||||
print_hex(buf, res);
|
||||
printf("\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
#define TEST1(str, exp, name, ...) do { \
|
||||
memset(buf, 0, sizeof buf); \
|
||||
unsigned res = fe64_ ## name(buf, __VA_ARGS__); \
|
||||
failed |= check(buf, exp, sizeof(exp) - 1, res, str); \
|
||||
} while (0)
|
||||
#define TEST(exp, ...) TEST1(#__VA_ARGS__, exp, __VA_ARGS__)
|
||||
|
||||
int
|
||||
main(void) {
|
||||
int failed = 0;
|
||||
uint8_t buf[16];
|
||||
|
||||
// This API is type safe and prohibits compilation of reg-type mismatches
|
||||
#define ENC_TEST_TYPESAFE
|
||||
// Silence -Warray-bounds with double cast
|
||||
#define FE_PTR(off) (const void*) ((uintptr_t) buf + (off))
|
||||
#define FLAGMASK(flags, mask) flags, mask
|
||||
#include "encode-test.inc"
|
||||
|
||||
TEST("\x90", NOP, 0);
|
||||
TEST("\x90", NOP, 1);
|
||||
TEST("\x66\x90", NOP, 2);
|
||||
TEST("\x0f\x1f\x00", NOP, 3);
|
||||
TEST("\x0f\x1f\x40\x00", NOP, 4);
|
||||
TEST("\x0f\x1f\x44\x00\x00", NOP, 5);
|
||||
TEST("\x66\x0f\x1f\x44\x00\x00", NOP, 6);
|
||||
TEST("\x0f\x1f\x80\x00\x00\x00\x00", NOP, 7);
|
||||
TEST("\x0f\x1f\x84\x00\x00\x00\x00\x00", NOP, 8);
|
||||
TEST("\x66\x0f\x1f\x84\x00\x00\x00\x00\x00", NOP, 9);
|
||||
TEST("\x66\x0f\x1f\x84\x00\x00\x00\x00\x00\x90", NOP, 10);
|
||||
TEST("\x66\x0f\x1f\x84\x00\x00\x00\x00\x00\x66\x90", NOP, 11);
|
||||
TEST("\x66\x0f\x1f\x84\x00\x00\x00\x00\x00\x0f\x1f\x00", NOP, 12);
|
||||
|
||||
puts(failed ? "Some tests FAILED" : "All tests PASSED");
|
||||
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||
}
|
||||
64
third_party/fadec/encode2-test.cc
vendored
Normal file
64
third_party/fadec/encode2-test.cc
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <fadec-enc2.h>
|
||||
|
||||
|
||||
using Buffer = std::array<uint8_t, 16>;
|
||||
|
||||
static
|
||||
void print_hex(const uint8_t* buf, size_t len) {
|
||||
for (size_t i = 0; i < len; i++)
|
||||
std::printf("%02x", buf[i]);
|
||||
}
|
||||
|
||||
static int
|
||||
check(const Buffer& buf, const char* exp, size_t exp_len, unsigned res, const char* name) {
|
||||
if (res == exp_len && !std::memcmp(buf.data(), exp, exp_len))
|
||||
return 0;
|
||||
std::printf("Failed case (new) %s:\n", name);
|
||||
std::printf(" Exp (%2zu): ", exp_len);
|
||||
print_hex(reinterpret_cast<const uint8_t*>(exp), exp_len);
|
||||
std::printf("\n Got (%2u): ", res);
|
||||
print_hex(buf.data(), res);
|
||||
std::printf("\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
#define TEST1(str, exp, name, ...) do { \
|
||||
buf.fill(0); \
|
||||
unsigned res = fe64_ ## name(buf.data(), __VA_ARGS__); \
|
||||
failed |= check(buf, exp, sizeof(exp) - 1, res, str); \
|
||||
} while (0)
|
||||
#define TEST(exp, ...) TEST1(#__VA_ARGS__, exp, __VA_ARGS__)
|
||||
|
||||
#define TEST_CPP1(str, exp, expr) do { \
|
||||
buf.fill(0); \
|
||||
unsigned res = (expr); \
|
||||
failed |= check(buf, exp, sizeof(exp) - 1, res, str); \
|
||||
} while (0)
|
||||
#define TEST_CPP(exp, ...) TEST_CPP1(#__VA_ARGS__, exp, __VA_ARGS__)
|
||||
|
||||
int main() {
|
||||
int failed = 0;
|
||||
Buffer buf{};
|
||||
|
||||
// This API is type safe and prohibits compilation of reg-type mismatches
|
||||
#define ENC_TEST_TYPESAFE
|
||||
// Silence -Warray-bounds with double cast
|
||||
#define FE_PTR(off) (const void*) ((uintptr_t) buf.data() + (off))
|
||||
#define FLAGMASK(flags, mask) flags, mask
|
||||
#include "encode-test.inc"
|
||||
|
||||
// Test implicit conversion of parameters also on the actual functions
|
||||
TEST_CPP("\x0f\x90\xc0", fe64_SETO8r(buf.data(), 0, FE_AX));
|
||||
TEST_CPP("\x0f\x90\xc0", (fe64_SETO8r)(buf.data(), 0, FE_AX));
|
||||
TEST_CPP("\x0f\x90\xc4", fe64_SETO8r(buf.data(), 0, FE_AH));
|
||||
TEST_CPP("\x0f\x90\xc4", (fe64_SETO8r)(buf.data(), 0, FE_AH));
|
||||
|
||||
std::puts(failed ? "Some tests FAILED" : "All tests PASSED");
|
||||
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
|
||||
}
|
||||
345
third_party/fadec/encode2.c
vendored
Normal file
345
third_party/fadec/encode2.c
vendored
Normal file
@@ -0,0 +1,345 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <fadec-enc2.h>
|
||||
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define LIKELY(x) __builtin_expect(!!(x), 1)
|
||||
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
|
||||
#if __has_attribute(cold) && __has_attribute(preserve_most)
|
||||
#define HINT_COLD __attribute__((cold,preserve_most,noinline))
|
||||
#elif __has_attribute(cold)
|
||||
#define HINT_COLD __attribute__((cold,noinline))
|
||||
#else
|
||||
#define HINT_COLD
|
||||
#endif
|
||||
#else
|
||||
#define LIKELY(x) (x)
|
||||
#define UNLIKELY(x) (x)
|
||||
#define HINT_COLD
|
||||
#endif
|
||||
|
||||
#define op_reg_idx(op) (op).idx
|
||||
#define op_reg_gph(op) (((op).idx & ~0x3) == 0x24)
|
||||
#define op_mem_base(mem) op_reg_idx((mem).base)
|
||||
#define op_mem_idx(mem) op_reg_idx((mem).idx)
|
||||
|
||||
static bool
|
||||
op_imm_n(int64_t imm, unsigned immsz) {
|
||||
if (immsz == 0 && !imm) return true;
|
||||
if (immsz == 1 && (int8_t) imm == imm) return true;
|
||||
if (immsz == 2 && (int16_t) imm == imm) return true;
|
||||
if (immsz == 3 && (imm&0xffffff) == imm) return true;
|
||||
if (immsz == 4 && (int32_t) imm == imm) return true;
|
||||
if (immsz == 8 && (int64_t) imm == imm) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
HINT_COLD static unsigned
|
||||
enc_seg67(uint8_t* buf, unsigned flags) {
|
||||
unsigned idx = 0;
|
||||
if (UNLIKELY(flags & FE_SEG_MASK)) {
|
||||
unsigned seg = (0x65643e362e2600 >> (8 * (flags & FE_SEG_MASK))) & 0xff;
|
||||
buf[idx++] = seg;
|
||||
}
|
||||
if (UNLIKELY(flags & FE_ADDR32)) buf[idx++] = 0x67;
|
||||
return idx;
|
||||
}
|
||||
|
||||
static unsigned
|
||||
enc_rex_mem(FeMem op0, uint64_t op1) {
|
||||
// Essentially just an and+or due to struct layout.
|
||||
uint32_t val = op1 | op0.flags | (op_mem_base(op0) << 8) |
|
||||
((uint32_t)op_mem_idx(op0) << 24);
|
||||
// Combine REX.RXB using multiplication for branch-less code.
|
||||
uint32_t masked = val & 0x08000808;
|
||||
return masked ? (uint8_t) (masked * (1|(1<<15)|(1<<25)) >> 26) + 0x40 : 0;
|
||||
}
|
||||
|
||||
static void
|
||||
enc_imm(uint8_t* buf, uint64_t imm, unsigned immsz) {
|
||||
#ifdef __GNUC__
|
||||
// Clang doesn't fold the loop into a single store.
|
||||
// See: https://github.com/llvm/llvm-project/issues/154696
|
||||
if (__builtin_constant_p(immsz)) {
|
||||
__builtin_memcpy(buf, &imm, immsz);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
for (unsigned i = 0; i < immsz; i++)
|
||||
*buf++ = imm >> 8 * i;
|
||||
}
|
||||
|
||||
static int
|
||||
enc_mem_common(uint8_t* buf, unsigned ripoff, FeMem op0, uint64_t op1,
|
||||
unsigned disp8scale) {
|
||||
int mod = 0, reg = op1 & 7, rm;
|
||||
unsigned sib = 0x20;
|
||||
bool withsib = false;
|
||||
unsigned dispsz = 0;
|
||||
int32_t off = op0.off;
|
||||
|
||||
if (op_reg_idx(op0.idx) < 0x80) {
|
||||
int scalabs = op0.scale;
|
||||
if (UNLIKELY((unsigned) (op0.scale - 1) >= 8 ||
|
||||
(op0.scale & (op0.scale - 1))))
|
||||
return 0;
|
||||
unsigned scale = (scalabs & 0xA ? 1 : 0) | (scalabs & 0xC ? 2 : 0);
|
||||
sib = scale << 6 | (op_reg_idx(op0.idx) & 7) << 3;
|
||||
withsib = true;
|
||||
} else if (UNLIKELY(op0.scale != 0)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (UNLIKELY(op0.base.idx >= 0x20)) {
|
||||
if (UNLIKELY(op0.base.idx >= op_reg_idx(FE_NOREG))) {
|
||||
*buf++ = (reg << 3) | 4;
|
||||
*buf++ = sib | 5;
|
||||
enc_imm(buf, off, 4);
|
||||
return ripoff + 6;
|
||||
} else if (LIKELY(op0.base.idx == FE_IP.idx)) {
|
||||
if (withsib)
|
||||
return 0;
|
||||
*buf++ = (reg << 3) | 5;
|
||||
// Adjust offset, caller doesn't know instruction length.
|
||||
enc_imm(buf, off - ripoff - 5, 4);
|
||||
return ripoff + 5;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
rm = op_reg_idx(op0.base) & 7;
|
||||
|
||||
if (off) {
|
||||
if (LIKELY(!disp8scale)) {
|
||||
mod = (int8_t) off == off ? 0x40 : 0x80;
|
||||
dispsz = (int8_t) off == off ? 1 : 4;
|
||||
} else {
|
||||
if (!(off & ((1 << disp8scale) - 1)) && op_imm_n(off >> disp8scale, 1))
|
||||
off >>= disp8scale, mod = 0x40, dispsz = 1;
|
||||
else
|
||||
mod = 0x80, dispsz = 4;
|
||||
}
|
||||
} else if (rm == 5) {
|
||||
dispsz = 1;
|
||||
mod = 0x40;
|
||||
}
|
||||
|
||||
// Always write four bytes of displacement. The buffer is always large
|
||||
// enough, and we truncate by returning a smaller "written bytes" count.
|
||||
if (withsib || rm == 4) {
|
||||
*buf++ = mod | (reg << 3) | 4;
|
||||
*buf++ = sib | rm;
|
||||
enc_imm(buf, off, 4);
|
||||
return ripoff + 2 + dispsz;
|
||||
} else {
|
||||
*buf++ = mod | (reg << 3) | rm;
|
||||
enc_imm(buf, off, 4);
|
||||
return ripoff + 1 + dispsz;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
enc_mem(uint8_t* buf, unsigned ripoff, FeMem op0, uint64_t op1, bool forcesib,
|
||||
unsigned disp8scale) {
|
||||
if (UNLIKELY(op_reg_idx(op0.idx) == 4))
|
||||
return 0;
|
||||
if (forcesib && op_reg_idx(op0.idx) == op_reg_idx(FE_NOREG)) {
|
||||
op0.scale = 1;
|
||||
op0.idx = FE_GP(4);
|
||||
}
|
||||
return enc_mem_common(buf, ripoff, op0, op1, disp8scale);
|
||||
}
|
||||
|
||||
static int
|
||||
enc_mem_vsib(uint8_t* buf, unsigned ripoff, FeMemV op0, uint64_t op1,
|
||||
bool forcesib, unsigned disp8scale) {
|
||||
(void) forcesib;
|
||||
FeMem mem = FE_MEM(op0.base, op0.scale, FE_GP(op_reg_idx(op0.idx)), op0.off);
|
||||
return enc_mem_common(buf, ripoff, mem, op1, disp8scale);
|
||||
}
|
||||
|
||||
// EVEX/VEX "Opcode" format:
|
||||
//
|
||||
// | EVEX byte 4 | P P M M M - - W | Opcode byte | VEX-D VEX-D-FLIPW
|
||||
// 0 8 16 24
|
||||
|
||||
enum {
|
||||
FE_OPC_VEX_WPP_SHIFT = 8,
|
||||
FE_OPC_VEX_WPP_MASK = 0x83 << FE_OPC_VEX_WPP_SHIFT,
|
||||
FE_OPC_VEX_MMM_SHIFT = 10,
|
||||
FE_OPC_VEX_MMM_MASK = 0x1f << FE_OPC_VEX_MMM_SHIFT,
|
||||
FE_OPC_VEX_DOWNGRADE_VEX = 1 << 24,
|
||||
FE_OPC_VEX_DOWNGRADE_VEX_FLIPW = 1 << 25,
|
||||
};
|
||||
|
||||
static int
|
||||
enc_vex_common(uint8_t* buf, unsigned opcode, unsigned base,
|
||||
unsigned idx, unsigned reg, unsigned vvvv) {
|
||||
if ((base | idx | reg | vvvv) & 0x10) return 0;
|
||||
bool vex3 = ((base | idx) & 0x08) || (opcode & 0xfc00) != 0x0400;
|
||||
if (vex3) {
|
||||
*buf++ = 0xc4;
|
||||
unsigned b1 = (opcode & FE_OPC_VEX_MMM_MASK) >> FE_OPC_VEX_MMM_SHIFT;
|
||||
if (!(reg & 0x08)) b1 |= 0x80;
|
||||
if (!(idx & 0x08)) b1 |= 0x40;
|
||||
if (!(base & 0x08)) b1 |= 0x20;
|
||||
*buf++ = b1;
|
||||
unsigned b2 = (opcode & FE_OPC_VEX_WPP_MASK) >> FE_OPC_VEX_WPP_SHIFT;
|
||||
if (opcode & 0x20) b2 |= 0x04;
|
||||
b2 |= (vvvv ^ 0xf) << 3;
|
||||
*buf++ = b2;
|
||||
} else {
|
||||
*buf++ = 0xc5;
|
||||
unsigned b2 = opcode >> FE_OPC_VEX_WPP_SHIFT & 3;
|
||||
if (opcode & 0x20) b2 |= 0x04;
|
||||
if (!(reg & 0x08)) b2 |= 0x80;
|
||||
b2 |= (vvvv ^ 0xf) << 3;
|
||||
*buf++ = b2;
|
||||
}
|
||||
*buf++ = (opcode & 0xff0000) >> 16;
|
||||
return 3 + vex3;
|
||||
}
|
||||
|
||||
static int
|
||||
enc_vex_reg(uint8_t* buf, unsigned opcode, uint64_t rm, uint64_t reg,
|
||||
uint64_t vvvv) {
|
||||
unsigned off = enc_vex_common(buf, opcode, rm, 0, reg, vvvv);
|
||||
buf[off] = 0xc0 | (reg << 3 & 0x38) | (rm & 7);
|
||||
return off ? off + 1 : 0;
|
||||
}
|
||||
|
||||
static int
|
||||
enc_vex_mem(uint8_t* buf, unsigned opcode, FeMem rm, uint64_t reg,
|
||||
uint64_t vvvv, unsigned ripoff, bool forcesib, unsigned disp8scale) {
|
||||
unsigned off = enc_vex_common(buf, opcode, op_reg_idx(rm.base), op_reg_idx(rm.idx), reg, vvvv);
|
||||
unsigned memoff = enc_mem(buf + off, ripoff + off, rm, reg, forcesib, disp8scale);
|
||||
return off && memoff ? memoff : 0;
|
||||
}
|
||||
|
||||
static int
|
||||
enc_vex_vsib(uint8_t* buf, unsigned opcode, FeMemV rm, uint64_t reg,
|
||||
uint64_t vvvv, unsigned ripoff, bool forcesib, unsigned disp8scale) {
|
||||
unsigned off = enc_vex_common(buf, opcode, op_reg_idx(rm.base), op_reg_idx(rm.idx), reg, vvvv);
|
||||
unsigned memoff = enc_mem_vsib(buf + off, ripoff + off, rm, reg, forcesib, disp8scale);
|
||||
return off && memoff ? memoff : 0;
|
||||
}
|
||||
|
||||
static int
|
||||
enc_evex_common(uint8_t* buf, unsigned opcode, unsigned base,
|
||||
unsigned idx, unsigned reg, unsigned vvvv) {
|
||||
*buf++ = 0x62;
|
||||
bool evexr3 = reg & 0x08;
|
||||
bool evexr4 = reg & 0x10;
|
||||
bool evexb3 = base & 0x08;
|
||||
bool evexb4 = base & 0x10; // evexb4 is unused in AVX-512 encoding
|
||||
bool evexx3 = idx & 0x08;
|
||||
bool evexx4 = idx & 0x10;
|
||||
bool evexv4 = vvvv & 0x10;
|
||||
unsigned b1 = (opcode & FE_OPC_VEX_MMM_MASK) >> FE_OPC_VEX_MMM_SHIFT;
|
||||
if (!evexr3) b1 |= 0x80;
|
||||
if (!evexx3) b1 |= 0x40;
|
||||
if (!evexb3) b1 |= 0x20;
|
||||
if (!evexr4) b1 |= 0x10;
|
||||
if (evexb4) b1 |= 0x08;
|
||||
*buf++ = b1;
|
||||
unsigned b2 = (opcode & FE_OPC_VEX_WPP_MASK) >> FE_OPC_VEX_WPP_SHIFT;
|
||||
if (!evexx4) b2 |= 0x04;
|
||||
b2 |= (~vvvv & 0xf) << 3;
|
||||
*buf++ = b2;
|
||||
unsigned b3 = opcode & 0xff;
|
||||
if (!evexv4) b3 |= 0x08;
|
||||
*buf++ = b3;
|
||||
*buf++ = (opcode & 0xff0000) >> 16;
|
||||
return 5;
|
||||
}
|
||||
|
||||
static unsigned
|
||||
enc_evex_to_vex(unsigned opcode) {
|
||||
return opcode & FE_OPC_VEX_DOWNGRADE_VEX_FLIPW ? opcode ^ 0x8000 : opcode;
|
||||
}
|
||||
|
||||
// Encode AVX-512 EVEX r/m-reg, non-xmm reg, vvvv, prefer vex
|
||||
static int
|
||||
enc_evex_reg(uint8_t* buf, unsigned opcode, unsigned rm,
|
||||
unsigned reg, unsigned vvvv) {
|
||||
unsigned off;
|
||||
if (!((rm | reg | vvvv) & 0x10) && (opcode & FE_OPC_VEX_DOWNGRADE_VEX))
|
||||
off = enc_vex_common(buf, enc_evex_to_vex(opcode), rm, 0, reg, vvvv);
|
||||
else
|
||||
off = enc_evex_common(buf, opcode, rm, 0, reg, vvvv);
|
||||
buf[off] = 0xc0 | (reg << 3 & 0x38) | (rm & 7);
|
||||
return off + 1;
|
||||
}
|
||||
|
||||
// Encode AVX-512 EVEX r/m-reg, xmm reg, vvvv, prefer vex
|
||||
static int
|
||||
enc_evex_xmm(uint8_t* buf, unsigned opcode, unsigned rm,
|
||||
unsigned reg, unsigned vvvv) {
|
||||
unsigned off;
|
||||
if (!((rm | reg | vvvv) & 0x10) && (opcode & FE_OPC_VEX_DOWNGRADE_VEX))
|
||||
off = enc_vex_common(buf, enc_evex_to_vex(opcode), rm, 0, reg, vvvv);
|
||||
else
|
||||
// AVX-512 XMM reg encoding uses X3 instead of B4.
|
||||
off = enc_evex_common(buf, opcode, rm & 0x0f, rm >> 1, reg, vvvv);
|
||||
buf[off] = 0xc0 | (reg << 3 & 0x38) | (rm & 7);
|
||||
return off + 1;
|
||||
}
|
||||
|
||||
static int
|
||||
enc_evex_mem(uint8_t* buf, unsigned opcode, FeMem rm, uint64_t reg,
|
||||
uint64_t vvvv, unsigned ripoff, bool forcesib, unsigned disp8scale) {
|
||||
unsigned off;
|
||||
if (!((op_reg_idx(rm.base) | op_reg_idx(rm.idx) | reg | vvvv) & 0x10) &&
|
||||
(opcode & FE_OPC_VEX_DOWNGRADE_VEX)) {
|
||||
disp8scale = 0; // Only AVX-512 EVEX compresses displacement
|
||||
off = enc_vex_common(buf, enc_evex_to_vex(opcode), op_reg_idx(rm.base), op_reg_idx(rm.idx), reg, vvvv);
|
||||
} else {
|
||||
off = enc_evex_common(buf, opcode, op_reg_idx(rm.base), op_reg_idx(rm.idx), reg, vvvv);
|
||||
}
|
||||
unsigned memoff = enc_mem(buf + off, ripoff + off, rm, reg, forcesib, disp8scale);
|
||||
return off && memoff ? memoff : 0;
|
||||
}
|
||||
|
||||
static int
|
||||
enc_evex_vsib(uint8_t* buf, unsigned opcode, FeMemV rm, uint64_t reg,
|
||||
uint64_t vvvv, unsigned ripoff, bool forcesib, unsigned disp8scale) {
|
||||
(void) vvvv;
|
||||
// EVEX VSIB requires non-zero mask operand
|
||||
if (!(opcode & 0x7)) return 0;
|
||||
// EVEX.X4 is encoded in EVEX.V4
|
||||
unsigned idx = op_reg_idx(rm.idx);
|
||||
unsigned off = enc_evex_common(buf, opcode, op_reg_idx(rm.base), idx & 0x0f, reg, idx & 0x10);
|
||||
unsigned memoff = enc_mem_vsib(buf + off, ripoff + off, rm, reg, forcesib, disp8scale);
|
||||
return off && memoff ? memoff : 0;
|
||||
}
|
||||
|
||||
unsigned fe64_NOP(uint8_t* buf, unsigned flags) {
|
||||
unsigned len = flags ? flags : 1;
|
||||
// Taken from Intel SDM
|
||||
static const uint8_t tbl[] = {
|
||||
0x90,
|
||||
0x66, 0x90,
|
||||
0x0f, 0x1f, 0x00,
|
||||
0x0f, 0x1f, 0x40, 0x00,
|
||||
0x0f, 0x1f, 0x44, 0x00, 0x00,
|
||||
0x66, 0x0f, 0x1f, 0x44, 0x00, 0x00,
|
||||
0x0f, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||
0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x66, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
unsigned remain = len;
|
||||
for (; remain > 9; remain -= 9)
|
||||
for (unsigned i = 0; i < 9; i++)
|
||||
*(buf++) = tbl[36 + i];
|
||||
const uint8_t* src = tbl + (remain * (remain - 1)) / 2;
|
||||
for (unsigned i = 0; i < remain; i++)
|
||||
*(buf++) = src[i];
|
||||
return len;
|
||||
}
|
||||
|
||||
#include <fadec-encode2-private.inc>
|
||||
18
third_party/fadec/fadec-decode-private.inc
vendored
Normal file
18
third_party/fadec/fadec-decode-private.inc
vendored
Normal file
File diff suppressed because one or more lines are too long
1888
third_party/fadec/fadec-decode-public.inc
vendored
Normal file
1888
third_party/fadec/fadec-decode-public.inc
vendored
Normal file
File diff suppressed because it is too large
Load Diff
113
third_party/fadec/fadec-enc.h
vendored
Normal file
113
third_party/fadec/fadec-enc.h
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
|
||||
#ifndef FD_FADEC_ENC_H_
|
||||
#define FD_FADEC_ENC_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
FE_AX = 0x100, FE_CX, FE_DX, FE_BX, FE_SP, FE_BP, FE_SI, FE_DI,
|
||||
FE_R8, FE_R9, FE_R10, FE_R11, FE_R12, FE_R13, FE_R14, FE_R15,
|
||||
FE_IP = 0x120,
|
||||
FE_AH = 0x204, FE_CH, FE_DH, FE_BH,
|
||||
FE_ES = 0x300, FE_CS, FE_SS, FE_DS, FE_FS, FE_GS,
|
||||
FE_ST0 = 0x400, FE_ST1, FE_ST2, FE_ST3, FE_ST4, FE_ST5, FE_ST6, FE_ST7,
|
||||
FE_MM0 = 0x500, FE_MM1, FE_MM2, FE_MM3, FE_MM4, FE_MM5, FE_MM6, FE_MM7,
|
||||
FE_XMM0 = 0x600, FE_XMM1, FE_XMM2, FE_XMM3, FE_XMM4, FE_XMM5, FE_XMM6, FE_XMM7,
|
||||
FE_XMM8, FE_XMM9, FE_XMM10, FE_XMM11, FE_XMM12, FE_XMM13, FE_XMM14, FE_XMM15,
|
||||
FE_XMM16, FE_XMM17, FE_XMM18, FE_XMM19, FE_XMM20, FE_XMM21, FE_XMM22, FE_XMM23,
|
||||
FE_XMM24, FE_XMM25, FE_XMM26, FE_XMM27, FE_XMM28, FE_XMM29, FE_XMM30, FE_XMM31,
|
||||
FE_K0 = 0x700, FE_K1, FE_K2, FE_K3, FE_K4, FE_K5, FE_K6, FE_K7,
|
||||
FE_TMM0 = 0x800, FE_TMM1, FE_TMM2, FE_TMM3, FE_TMM4, FE_TMM5, FE_TMM6, FE_TMM7,
|
||||
} FeReg;
|
||||
|
||||
typedef int64_t FeOp;
|
||||
|
||||
/** Construct a memory operand. Unused parts can be set to 0 and will be
|
||||
* ignored. FE_IP can be used as base register, in which case the offset is
|
||||
* interpreted as the offset from the /current/ position -- the size of the
|
||||
* encoded instruction will be subtracted during encoding. scale must be 1, 2,
|
||||
* 4, or 8; but is ignored if idx == 0. **/
|
||||
#define FE_MEM(base,sc,idx,off) (INT64_MIN | ((int64_t) ((base) & 0xfff) << 32) | ((int64_t) ((idx) & 0xfff) << 44) | ((int64_t) ((sc) & 0xf) << 56) | ((off) & 0xffffffff))
|
||||
#define FE_NOREG ((FeReg) 0)
|
||||
|
||||
/** Add segment override prefix. This may or may not generate prefixes for the
|
||||
* ignored prefixes ES/CS/DS/SS in 64-bit mode. **/
|
||||
#define FE_SEG(seg) ((uint64_t) (((seg) & 0x7) + 1) << 29)
|
||||
/** Do not use. **/
|
||||
#define FE_SEG_MASK 0xe0000000
|
||||
/** Overrides address size. **/
|
||||
#define FE_ADDR32 0x10000000
|
||||
/** Used together with a RIP-relative (conditional) jump, this will force the
|
||||
* use of the encoding with the largest distance. Useful for reserving a jump
|
||||
* when the target offset is still unknown; if the jump is re-encoded later on,
|
||||
* FE_JMPL must be specified there, too, so that the encoding lengths match. **/
|
||||
#define FE_JMPL 0x100000000
|
||||
#define FE_MASK(kreg) ((uint64_t) ((kreg) & 0x7) << 33)
|
||||
#define FE_RC_RN 0x0000000
|
||||
#define FE_RC_RD 0x0800000
|
||||
#define FE_RC_RU 0x1000000
|
||||
#define FE_RC_RZ 0x1800000
|
||||
|
||||
enum {
|
||||
FE_CC_O = 0x0,
|
||||
FE_CC_NO = 0x1,
|
||||
FE_CC_C = 0x2,
|
||||
FE_CC_B = FE_CC_C,
|
||||
FE_CC_NAE = FE_CC_C,
|
||||
FE_CC_NC = 0x3,
|
||||
FE_CC_AE = FE_CC_NC,
|
||||
FE_CC_NB = FE_CC_NC,
|
||||
FE_CC_Z = 0x4,
|
||||
FE_CC_E = FE_CC_Z,
|
||||
FE_CC_NZ = 0x5,
|
||||
FE_CC_NE = FE_CC_NZ,
|
||||
FE_CC_BE = 0x6,
|
||||
FE_CC_NA = FE_CC_BE,
|
||||
FE_CC_A = 0x7,
|
||||
FE_CC_NBE = FE_CC_A,
|
||||
FE_CC_S = 0x8,
|
||||
FE_CC_NS = 0x9,
|
||||
FE_CC_P = 0xa,
|
||||
FE_CC_PE = FE_CC_P,
|
||||
FE_CC_NP = 0xb,
|
||||
FE_CC_PO = FE_CC_NP,
|
||||
FE_CC_L = 0xc,
|
||||
FE_CC_NGE = FE_CC_L,
|
||||
FE_CC_GE = 0xd,
|
||||
FE_CC_NL = FE_CC_GE,
|
||||
FE_CC_LE = 0xe,
|
||||
FE_CC_NG = FE_CC_LE,
|
||||
FE_CC_G = 0xf,
|
||||
FE_CC_NLE = FE_CC_G,
|
||||
};
|
||||
|
||||
#include <fadec-encode-public.inc>
|
||||
|
||||
/** Do not use. **/
|
||||
#define fe_enc64_1(buf, mnem, op0, op1, op2, op3, ...) fe_enc64_impl(buf, mnem, op0, op1, op2, op3)
|
||||
/** Encode a single instruction for 64-bit mode.
|
||||
* \param buf Pointer to the buffer for instruction bytes, must have a size of
|
||||
* 15 bytes. The pointer is advanced by the number of bytes used for
|
||||
* encoding the specified instruction.
|
||||
* \param mnem Mnemonic, optionally or-ed with FE_SEG(), FE_ADDR32, or FE_JMPL.
|
||||
* \param operands... Instruction operands. Immediate operands are passed as
|
||||
* plain value; register operands using the FeReg enum; memory operands
|
||||
* using FE_MEM(); and offset operands for RIP-relative jumps/calls are
|
||||
* specified as _address in buf_, e.g. (intptr_t) jmptgt, the address of
|
||||
* buf and the size of the encoded instruction are subtracted internally.
|
||||
* \return Zero for success or a negative value in case of an error.
|
||||
**/
|
||||
#define fe_enc64(buf, ...) fe_enc64_1(buf, __VA_ARGS__, 0, 0, 0, 0, 0)
|
||||
/** Do not use. **/
|
||||
int fe_enc64_impl(uint8_t** buf, uint64_t mnem, FeOp op0, FeOp op1, FeOp op2, FeOp op3);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
226
third_party/fadec/fadec-enc2.h
vendored
Normal file
226
third_party/fadec/fadec-enc2.h
vendored
Normal file
@@ -0,0 +1,226 @@
|
||||
|
||||
#ifndef FD_FADEC_ENC2_H_
|
||||
#define FD_FADEC_ENC2_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define FE_STRUCT(name) name
|
||||
#else
|
||||
#define FE_STRUCT(name) (name)
|
||||
#endif
|
||||
|
||||
// Flags
|
||||
#define FE_JMPL 0x8
|
||||
#define FE_ADDR32 0x10
|
||||
#define FE_SEG_MASK 0x7
|
||||
#define FE_SEG(seg) (((seg).idx + 1) & FE_SEG_MASK)
|
||||
#define FE_RC_MASK 0x60
|
||||
#define FE_RC_RN 0x00
|
||||
#define FE_RC_RD 0x20
|
||||
#define FE_RC_RU 0x40
|
||||
#define FE_RC_RZ 0x60
|
||||
|
||||
// Condition codes
|
||||
typedef enum FeCond {
|
||||
FE_CC_O = 0x00000,
|
||||
FE_CC_NO = 0x10000,
|
||||
FE_CC_C = 0x20000,
|
||||
FE_CC_B = FE_CC_C,
|
||||
FE_CC_NAE = FE_CC_C,
|
||||
FE_CC_NC = 0x30000,
|
||||
FE_CC_AE = FE_CC_NC,
|
||||
FE_CC_NB = FE_CC_NC,
|
||||
FE_CC_Z = 0x40000,
|
||||
FE_CC_E = FE_CC_Z,
|
||||
FE_CC_NZ = 0x50000,
|
||||
FE_CC_NE = FE_CC_NZ,
|
||||
FE_CC_BE = 0x60000,
|
||||
FE_CC_NA = FE_CC_BE,
|
||||
FE_CC_A = 0x70000,
|
||||
FE_CC_NBE = FE_CC_A,
|
||||
FE_CC_S = 0x80000,
|
||||
FE_CC_NS = 0x90000,
|
||||
FE_CC_P = 0xa0000,
|
||||
FE_CC_PE = FE_CC_P,
|
||||
FE_CC_NP = 0xb0000,
|
||||
FE_CC_PO = FE_CC_NP,
|
||||
FE_CC_L = 0xc0000,
|
||||
FE_CC_NGE = FE_CC_L,
|
||||
FE_CC_GE = 0xd0000,
|
||||
FE_CC_NL = FE_CC_GE,
|
||||
FE_CC_LE = 0xe0000,
|
||||
FE_CC_NG = FE_CC_LE,
|
||||
FE_CC_G = 0xf0000,
|
||||
FE_CC_NLE = FE_CC_G,
|
||||
|
||||
FE_CC_MASK = 0xf0000
|
||||
} FeCond;
|
||||
|
||||
typedef struct FeRegGP { unsigned char idx; } FeRegGP;
|
||||
#define FE_GP(idx) (FE_STRUCT(FeRegGP) { idx })
|
||||
#define FE_AX FE_GP(0)
|
||||
#define FE_CX FE_GP(1)
|
||||
#define FE_DX FE_GP(2)
|
||||
#define FE_BX FE_GP(3)
|
||||
#define FE_SP FE_GP(4)
|
||||
#define FE_BP FE_GP(5)
|
||||
#define FE_SI FE_GP(6)
|
||||
#define FE_DI FE_GP(7)
|
||||
#define FE_R8 FE_GP(8)
|
||||
#define FE_R9 FE_GP(9)
|
||||
#define FE_R10 FE_GP(10)
|
||||
#define FE_R11 FE_GP(11)
|
||||
#define FE_R12 FE_GP(12)
|
||||
#define FE_R13 FE_GP(13)
|
||||
#define FE_R14 FE_GP(14)
|
||||
#define FE_R15 FE_GP(15)
|
||||
#define FE_IP FE_GP(0x20)
|
||||
#define FE_NOREG FE_GP(0x80)
|
||||
typedef struct FeRegGPH { unsigned char idx; } FeRegGPH;
|
||||
#define FE_GPH(idx) (FE_STRUCT(FeRegGPH) { idx })
|
||||
#define FE_AH FE_GPH(4)
|
||||
#define FE_CH FE_GPH(5)
|
||||
#define FE_DH FE_GPH(6)
|
||||
#define FE_BH FE_GPH(7)
|
||||
typedef struct FeRegSREG { unsigned char idx; } FeRegSREG;
|
||||
#define FE_SREG(idx) (FE_STRUCT(FeRegSREG) { idx })
|
||||
#define FE_ES FE_SREG(0)
|
||||
#define FE_CS FE_SREG(1)
|
||||
#define FE_SS FE_SREG(2)
|
||||
#define FE_DS FE_SREG(3)
|
||||
#define FE_FS FE_SREG(4)
|
||||
#define FE_GS FE_SREG(5)
|
||||
typedef struct FeRegST { unsigned char idx; } FeRegST;
|
||||
#define FE_ST(idx) (FE_STRUCT(FeRegST) { idx })
|
||||
#define FE_ST0 FE_ST(0)
|
||||
#define FE_ST1 FE_ST(1)
|
||||
#define FE_ST2 FE_ST(2)
|
||||
#define FE_ST3 FE_ST(3)
|
||||
#define FE_ST4 FE_ST(4)
|
||||
#define FE_ST5 FE_ST(5)
|
||||
#define FE_ST6 FE_ST(6)
|
||||
#define FE_ST7 FE_ST(7)
|
||||
typedef struct FeRegMM { unsigned char idx; } FeRegMM;
|
||||
#define FE_MM(idx) (FE_STRUCT(FeRegMM) { idx })
|
||||
#define FE_MM0 FE_MM(0)
|
||||
#define FE_MM1 FE_MM(1)
|
||||
#define FE_MM2 FE_MM(2)
|
||||
#define FE_MM3 FE_MM(3)
|
||||
#define FE_MM4 FE_MM(4)
|
||||
#define FE_MM5 FE_MM(5)
|
||||
#define FE_MM6 FE_MM(6)
|
||||
#define FE_MM7 FE_MM(7)
|
||||
typedef struct FeRegXMM { unsigned char idx; } FeRegXMM;
|
||||
#define FE_XMM(idx) (FE_STRUCT(FeRegXMM) { idx })
|
||||
#define FE_XMM0 FE_XMM(0)
|
||||
#define FE_XMM1 FE_XMM(1)
|
||||
#define FE_XMM2 FE_XMM(2)
|
||||
#define FE_XMM3 FE_XMM(3)
|
||||
#define FE_XMM4 FE_XMM(4)
|
||||
#define FE_XMM5 FE_XMM(5)
|
||||
#define FE_XMM6 FE_XMM(6)
|
||||
#define FE_XMM7 FE_XMM(7)
|
||||
#define FE_XMM8 FE_XMM(8)
|
||||
#define FE_XMM9 FE_XMM(9)
|
||||
#define FE_XMM10 FE_XMM(10)
|
||||
#define FE_XMM11 FE_XMM(11)
|
||||
#define FE_XMM12 FE_XMM(12)
|
||||
#define FE_XMM13 FE_XMM(13)
|
||||
#define FE_XMM14 FE_XMM(14)
|
||||
#define FE_XMM15 FE_XMM(15)
|
||||
#define FE_XMM16 FE_XMM(16)
|
||||
#define FE_XMM17 FE_XMM(17)
|
||||
#define FE_XMM18 FE_XMM(18)
|
||||
#define FE_XMM19 FE_XMM(19)
|
||||
#define FE_XMM20 FE_XMM(20)
|
||||
#define FE_XMM21 FE_XMM(21)
|
||||
#define FE_XMM22 FE_XMM(22)
|
||||
#define FE_XMM23 FE_XMM(23)
|
||||
#define FE_XMM24 FE_XMM(24)
|
||||
#define FE_XMM25 FE_XMM(25)
|
||||
#define FE_XMM26 FE_XMM(26)
|
||||
#define FE_XMM27 FE_XMM(27)
|
||||
#define FE_XMM28 FE_XMM(28)
|
||||
#define FE_XMM29 FE_XMM(29)
|
||||
#define FE_XMM30 FE_XMM(30)
|
||||
#define FE_XMM31 FE_XMM(31)
|
||||
typedef struct FeRegMASK { unsigned char idx; } FeRegMASK;
|
||||
#define FE_K(idx) (FE_STRUCT(FeRegMASK) { idx })
|
||||
#define FE_K0 FE_K(0)
|
||||
#define FE_K1 FE_K(1)
|
||||
#define FE_K2 FE_K(2)
|
||||
#define FE_K3 FE_K(3)
|
||||
#define FE_K4 FE_K(4)
|
||||
#define FE_K5 FE_K(5)
|
||||
#define FE_K6 FE_K(6)
|
||||
#define FE_K7 FE_K(7)
|
||||
typedef struct FeRegTMM { unsigned char idx; } FeRegTMM;
|
||||
#define FE_TMM(idx) (FE_STRUCT(FeRegTMM) { idx })
|
||||
#define FE_TMM0 FE_TMM(0)
|
||||
#define FE_TMM1 FE_TMM(1)
|
||||
#define FE_TMM2 FE_TMM(2)
|
||||
#define FE_TMM3 FE_TMM(3)
|
||||
#define FE_TMM4 FE_TMM(4)
|
||||
#define FE_TMM5 FE_TMM(5)
|
||||
#define FE_TMM6 FE_TMM(6)
|
||||
#define FE_TMM7 FE_TMM(7)
|
||||
typedef struct FeRegCR { unsigned char idx; } FeRegCR;
|
||||
#define FE_CR(idx) (FE_STRUCT(FeRegCR) { idx })
|
||||
typedef struct FeRegDR { unsigned char idx; } FeRegDR;
|
||||
#define FE_DR(idx) (FE_STRUCT(FeRegDR) { idx })
|
||||
|
||||
// Internal only
|
||||
// Disambiguate GP and GPH -- C++ uses conversion constructors; C uses _Generic.
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
namespace {
|
||||
struct FeRegGPLH {
|
||||
unsigned char idx;
|
||||
FeRegGPLH(FeRegGP gp) : idx(gp.idx) {}
|
||||
FeRegGPLH(FeRegGPH gp) : idx(gp.idx | 0x20) {}
|
||||
};
|
||||
}
|
||||
extern "C" {
|
||||
#define FE_MAKE_GPLH(reg) reg
|
||||
#else
|
||||
typedef struct FeRegGPLH { unsigned char idx; } FeRegGPLH;
|
||||
#define FE_GPLH(idx) (FE_STRUCT(FeRegGPLH) { idx })
|
||||
#define FE_MAKE_GPLH(reg) FE_GPLH(_Generic((reg), FeRegGPH: 0x20, FeRegGP: 0) | (reg).idx)
|
||||
#endif
|
||||
|
||||
typedef struct FeMem {
|
||||
uint8_t flags;
|
||||
FeRegGP base;
|
||||
unsigned char scale;
|
||||
// union {
|
||||
FeRegGP idx;
|
||||
// FeRegXMM idx_xmm;
|
||||
// };
|
||||
int32_t off;
|
||||
} FeMem;
|
||||
#define FE_MEM(base,sc,idx,off) (FE_STRUCT(FeMem) { 0, base, sc, idx, off })
|
||||
typedef struct FeMemV {
|
||||
uint8_t flags;
|
||||
FeRegGP base;
|
||||
unsigned char scale;
|
||||
FeRegXMM idx;
|
||||
int32_t off;
|
||||
} FeMemV;
|
||||
#define FE_MEMV(base,sc,idx,off) (FE_STRUCT(FeMemV) { 0, base, sc, idx, off })
|
||||
|
||||
// NOP is special: flags is interpreted as the length in bytes, 0 = 1 byte, too.
|
||||
unsigned fe64_NOP(uint8_t* buf, unsigned flags);
|
||||
|
||||
#include <fadec-encode2-public.inc>
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
286
third_party/fadec/fadec.h
vendored
Normal file
286
third_party/fadec/fadec.h
vendored
Normal file
@@ -0,0 +1,286 @@
|
||||
|
||||
#ifndef FD_FADEC_H_
|
||||
#define FD_FADEC_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
FD_REG_R0 = 0, FD_REG_R1, FD_REG_R2, FD_REG_R3,
|
||||
FD_REG_R4, FD_REG_R5, FD_REG_R6, FD_REG_R7,
|
||||
FD_REG_R8, FD_REG_R9, FD_REG_R10, FD_REG_R11,
|
||||
FD_REG_R12, FD_REG_R13, FD_REG_R14, FD_REG_R15,
|
||||
// Alternative names for byte registers
|
||||
FD_REG_AL = 0, FD_REG_CL, FD_REG_DL, FD_REG_BL,
|
||||
FD_REG_AH, FD_REG_CH, FD_REG_DH, FD_REG_BH,
|
||||
// Alternative names for general purpose registers
|
||||
FD_REG_AX = 0, FD_REG_CX, FD_REG_DX, FD_REG_BX,
|
||||
FD_REG_SP, FD_REG_BP, FD_REG_SI, FD_REG_DI,
|
||||
// FD_REG_IP can only be accessed in long mode (64-bit)
|
||||
FD_REG_IP = 0x10,
|
||||
// Segment register values
|
||||
FD_REG_ES = 0, FD_REG_CS, FD_REG_SS, FD_REG_DS, FD_REG_FS, FD_REG_GS,
|
||||
// No register specified
|
||||
FD_REG_NONE = 0x3f
|
||||
} FdReg;
|
||||
|
||||
typedef enum {
|
||||
#define FD_MNEMONIC(name,value) FDI_ ## name = value,
|
||||
#include <fadec-decode-public.inc>
|
||||
#undef FD_MNEMONIC
|
||||
} FdInstrType;
|
||||
|
||||
/** Internal use only. **/
|
||||
enum {
|
||||
FD_FLAG_LOCK = 1 << 0,
|
||||
FD_FLAG_REP = 1 << 2,
|
||||
FD_FLAG_REPNZ = 1 << 1,
|
||||
FD_FLAG_64 = 1 << 7,
|
||||
};
|
||||
|
||||
/** Operand types. **/
|
||||
typedef enum {
|
||||
FD_OT_NONE = 0,
|
||||
FD_OT_REG = 1,
|
||||
FD_OT_IMM = 2,
|
||||
FD_OT_MEM = 3,
|
||||
FD_OT_OFF = 4,
|
||||
FD_OT_MEMBCST = 5,
|
||||
} FdOpType;
|
||||
|
||||
typedef enum {
|
||||
/** Vector (SSE/AVX) register XMMn/YMMn/ZMMn **/
|
||||
FD_RT_VEC = 0,
|
||||
/** Low general purpose register **/
|
||||
FD_RT_GPL = 1,
|
||||
/** High-byte general purpose register **/
|
||||
FD_RT_GPH = 2,
|
||||
/** Segment register **/
|
||||
FD_RT_SEG = 3,
|
||||
/** FPU register ST(n) **/
|
||||
FD_RT_FPU = 4,
|
||||
/** MMX register MMn **/
|
||||
FD_RT_MMX = 5,
|
||||
/** TMM register TMMn **/
|
||||
FD_RT_TMM = 6,
|
||||
/** Vector mask (AVX-512) register Kn **/
|
||||
FD_RT_MASK = 7,
|
||||
/** Bound register BNDn **/
|
||||
FD_RT_BND = 8,
|
||||
/** Control Register CRn **/
|
||||
FD_RT_CR = 9,
|
||||
/** Debug Register DRn **/
|
||||
FD_RT_DR = 10,
|
||||
/** Must be a memory operand **/
|
||||
FD_RT_MEM = 15,
|
||||
} FdRegType;
|
||||
|
||||
/** Do not depend on the actual enum values. **/
|
||||
typedef enum {
|
||||
/** Round to nearest (even) **/
|
||||
FD_RC_RN = 1,
|
||||
/** Round down **/
|
||||
FD_RC_RD = 3,
|
||||
/** Round up **/
|
||||
FD_RC_RU = 5,
|
||||
/** Round to zero (truncate) **/
|
||||
FD_RC_RZ = 7,
|
||||
/** Rounding mode as specified in MXCSR **/
|
||||
FD_RC_MXCSR = 0,
|
||||
/** Rounding mode irrelevant, but SAE **/
|
||||
FD_RC_SAE = 6,
|
||||
} FdRoundControl;
|
||||
|
||||
/** Internal use only. **/
|
||||
typedef struct {
|
||||
uint8_t type;
|
||||
uint8_t size;
|
||||
uint8_t reg;
|
||||
uint8_t misc;
|
||||
} FdOp;
|
||||
|
||||
/** Never(!) access struct fields directly. Use the macros defined below. **/
|
||||
typedef struct {
|
||||
uint16_t type;
|
||||
uint8_t flags;
|
||||
uint8_t segment;
|
||||
uint8_t addrsz;
|
||||
uint8_t operandsz;
|
||||
uint8_t size;
|
||||
uint8_t evex;
|
||||
|
||||
FdOp operands[4];
|
||||
|
||||
int64_t disp;
|
||||
int64_t imm;
|
||||
|
||||
uint64_t address;
|
||||
} FdInstr;
|
||||
|
||||
typedef enum {
|
||||
FD_ERR_UD = -1,
|
||||
FD_ERR_INTERNAL = -2,
|
||||
FD_ERR_PARTIAL = -3,
|
||||
} FdErr;
|
||||
|
||||
|
||||
/** Decode an instruction.
|
||||
* \param buf Buffer for instruction bytes.
|
||||
* \param len Length of the buffer (in bytes). An instruction is not longer than
|
||||
* 15 bytes on all x86 architectures.
|
||||
* \param mode Decoding mode, either 32 for protected/compatibility mode or 64
|
||||
* for long mode. 16-bit mode is not supported.
|
||||
* \param address Virtual address where the decoded instruction. This is used
|
||||
* for computing jump targets. If "0" is passed, operands which require
|
||||
* adding EIP/RIP will be stored as FD_OT_OFF operands.
|
||||
* DEPRECATED: Strongly prefer passing 0 and using FD_OT_OFF operands.
|
||||
* \param out_instr Pointer to the instruction buffer. Note that this may get
|
||||
* partially written even if an error is returned.
|
||||
* \return The number of bytes consumed by the instruction, or a negative number
|
||||
* indicating an error.
|
||||
**/
|
||||
int fd_decode(const uint8_t* buf, size_t len, int mode, uintptr_t address,
|
||||
FdInstr* out_instr);
|
||||
|
||||
/** Format an instruction to a string.
|
||||
* \param instr The instruction.
|
||||
* \param buf The buffer to hold the formatted string.
|
||||
* \param len The length of the buffer.
|
||||
**/
|
||||
void fd_format(const FdInstr* instr, char* buf, size_t len);
|
||||
|
||||
/** Format an instruction to a string.
|
||||
* NOTE: API stability is currently not guaranteed for this function; its name
|
||||
* and/or signature may change in future.
|
||||
*
|
||||
* \param instr The instruction.
|
||||
* \param addr The base address to use for printing FD_OT_OFF operands.
|
||||
* \param buf The buffer to hold the formatted string.
|
||||
* \param len The length of the buffer.
|
||||
**/
|
||||
void fd_format_abs(const FdInstr* instr, uint64_t addr, char* buf, size_t len);
|
||||
|
||||
/** Get the stringified name of an instruction type.
|
||||
* NOTE: API stability is currently not guaranteed for this function; changes
|
||||
* to the signature and/or the returned string can be expected. E.g., a future
|
||||
* version may take an extra parameter for the instruction operand size; or may
|
||||
* take a complete decoded instruction as first parameter and return the
|
||||
* mnemonic returned by fd_format.
|
||||
*
|
||||
* \param ty An instruction type
|
||||
* \return The instruction type as string, or "(invalid)".
|
||||
**/
|
||||
const char* fdi_name(FdInstrType ty);
|
||||
|
||||
|
||||
/** Gets the type/mnemonic of the instruction.
|
||||
* ABI STABILITY NOTE: different versions or builds of the library may use
|
||||
* different values. When linking as shared library, any interpretation of this
|
||||
* value is meaningless; in such cases use fdi_name.
|
||||
*
|
||||
* API STABILITY NOTE: a future version of this library may decode string
|
||||
* instructions prefixed with REP/REPNZ and instructions prefixed with LOCK as
|
||||
* separate instruction types. **/
|
||||
#define FD_TYPE(instr) ((FdInstrType) (instr)->type)
|
||||
/** DEPRECATED: This functionality is obsolete in favor of FD_OT_OFF.
|
||||
* Gets the address of the instruction. Invalid if decoded address == 0. **/
|
||||
#define FD_ADDRESS(instr) ((instr)->address)
|
||||
/** Gets the size of the instruction in bytes. **/
|
||||
#define FD_SIZE(instr) ((instr)->size)
|
||||
/** Gets the specified segment override, or FD_REG_NONE for default segment. **/
|
||||
#define FD_SEGMENT(instr) ((FdReg) (instr)->segment & 0x3f)
|
||||
/** Gets the address size attribute of the instruction in bytes. **/
|
||||
#define FD_ADDRSIZE(instr) (1 << (instr)->addrsz)
|
||||
/** Get the logarithmic address size; FD_ADDRSIZE == 1 << FD_ADDRSIZELG **/
|
||||
#define FD_ADDRSIZELG(instr) ((instr)->addrsz)
|
||||
/** Gets the operation width in bytes of the instruction if this is not encoded
|
||||
* in the operands, for example for the string instruction (e.g. MOVS). **/
|
||||
#define FD_OPSIZE(instr) (1 << (instr)->operandsz)
|
||||
/** Get the logarithmic operand size; FD_OPSIZE == 1 << FD_OPSIZELG iff
|
||||
* FD_OPSIZE is valid. **/
|
||||
#define FD_OPSIZELG(instr) ((instr)->operandsz)
|
||||
/** Indicates whether the instruction was encoded with a REP prefix. Needed for:
|
||||
* (1) Handling the instructions MOVS, STOS, LODS, INS and OUTS properly.
|
||||
* (2) Handling the instructions SCAS and CMPS, for which this means REPZ. **/
|
||||
#define FD_HAS_REP(instr) ((instr)->flags & FD_FLAG_REP)
|
||||
/** Indicates whether the instruction was encoded with a REPNZ prefix. **/
|
||||
#define FD_HAS_REPNZ(instr) ((instr)->flags & FD_FLAG_REPNZ)
|
||||
/** Indicates whether the instruction was encoded with a LOCK prefix. **/
|
||||
#define FD_HAS_LOCK(instr) ((instr)->flags & FD_FLAG_LOCK)
|
||||
/** Do not use. **/
|
||||
#define FD_IS64(instr) ((instr)->flags & FD_FLAG_64)
|
||||
|
||||
/** Gets the type of an operand at the given index. **/
|
||||
#define FD_OP_TYPE(instr,idx) ((FdOpType) (instr)->operands[idx].type)
|
||||
/** Gets the size in bytes of an operand. However, there are a few exceptions:
|
||||
* (1) For some register types, e.g., segment registers, or x87 registers, the
|
||||
* size is zero. (This allows some simplifications internally.)
|
||||
* (2) On some vector instructions this may be only an approximation of the
|
||||
* actually needed operand size (that is, an instruction may/must only use
|
||||
* a smaller part than specified here). The real operand size is always
|
||||
* fully recoverable in combination with the instruction type. **/
|
||||
#define FD_OP_SIZE(instr,idx) (1 << (instr)->operands[idx].size >> 1)
|
||||
/** Get the logarithmic size of an operand; see FD_OP_SIZE for special cases.
|
||||
* The following equality holds: FD_OP_SIZE == 1 << (FD_OP_SIZELG + 1) >> 1
|
||||
* Note that typically FD_OP_SIZE == 1 << FD_OP_SIZELG unless a zero-sized
|
||||
* memory operand, FPU register, or mask register is involved. **/
|
||||
#define FD_OP_SIZELG(instr,idx) ((instr)->operands[idx].size - 1)
|
||||
/** Gets the accessed register index of a register operand. Note that /only/ the
|
||||
* index is returned, no further interpretation of the index (which depends on
|
||||
* the instruction type) is done. The register type can be fetched using
|
||||
* FD_OP_REG_TYPE, e.g. for distinguishing high-byte registers.
|
||||
* Only valid if FD_OP_TYPE == FD_OT_REG **/
|
||||
#define FD_OP_REG(instr,idx) ((FdReg) (instr)->operands[idx].reg)
|
||||
/** Gets the type of the accessed register.
|
||||
* Only valid if FD_OP_TYPE == FD_OT_REG **/
|
||||
#define FD_OP_REG_TYPE(instr,idx) ((FdRegType) (instr)->operands[idx].misc)
|
||||
/** DEPRECATED: use FD_OP_REG_TYPE() == FD_RT_GPH instead.
|
||||
* Returns whether the accessed register is a high-byte register. In that case,
|
||||
* the register index has to be decreased by 4.
|
||||
* Only valid if FD_OP_TYPE == FD_OT_REG **/
|
||||
#define FD_OP_REG_HIGH(instr,idx) (FD_OP_REG_TYPE(instr,idx) == FD_RT_GPH)
|
||||
/** Gets the index of the base register from a memory operand, or FD_REG_NONE,
|
||||
* if the memory operand has no base register. This is the only case where the
|
||||
* 64-bit register RIP can be returned, in which case the operand also has no
|
||||
* scaled index register.
|
||||
* Only valid if FD_OP_TYPE == FD_OT_MEM/MEMBCST **/
|
||||
#define FD_OP_BASE(instr,idx) ((FdReg) (instr)->operands[idx].reg)
|
||||
/** Gets the index of the index register from a memory operand, or FD_REG_NONE,
|
||||
* if the memory operand has no scaled index register.
|
||||
* Only valid if FD_OP_TYPE == FD_OT_MEM/MEMBCST **/
|
||||
#define FD_OP_INDEX(instr,idx) ((FdReg) (instr)->operands[idx].misc & 0x3f)
|
||||
/** Gets the scale of the index register from a memory operand when existent.
|
||||
* This does /not/ return the scale in an absolute value but returns the amount
|
||||
* of bits the index register is shifted to the left (i.e. the value in in the
|
||||
* range 0-3). The actual scale can be computed easily using 1<<FD_OP_SCALE.
|
||||
* Only valid if FD_OP_TYPE == FD_OT_MEM/MEMBCST and FD_OP_INDEX != NONE **/
|
||||
#define FD_OP_SCALE(instr,idx) ((instr)->operands[idx].misc >> 6)
|
||||
/** Gets the sign-extended displacement of a memory operand.
|
||||
* Only valid if FD_OP_TYPE == FD_OT_MEM/MEMBCST **/
|
||||
#define FD_OP_DISP(instr,idx) ((int64_t) (instr)->disp)
|
||||
/** Get memory broadcast size in bytes.
|
||||
* Only valid if FD_OP_TYPE == FD_OT_MEMBCST **/
|
||||
#define FD_OP_BCSTSZ(instr,idx) (1 << FD_OP_BCSTSZLG(instr,idx))
|
||||
/** Get logarithmic memory broadcast size (1 = 2-byte; 2=4-byte; 3=8-byte).
|
||||
* Only valid if FD_OP_TYPE == FD_OT_MEMBCST **/
|
||||
#define FD_OP_BCSTSZLG(instr,idx) ((instr)->segment >> 6)
|
||||
/** Gets the (sign-extended) encoded constant for an immediate operand.
|
||||
* Only valid if FD_OP_TYPE == FD_OT_IMM or FD_OP_TYPE == FD_OT_OFF **/
|
||||
#define FD_OP_IMM(instr,idx) ((instr)->imm)
|
||||
|
||||
/** Get the opmask register for EVEX-encoded instructions; 0 for no mask. **/
|
||||
#define FD_MASKREG(instr) ((instr)->evex & 0x07)
|
||||
/** Get whether zero masking shall be used. Only valid if FD_MASKREG != 0. **/
|
||||
#define FD_MASKZERO(instr) ((instr)->evex & 0x80)
|
||||
/** Get rounding mode for EVEX-encoded instructions. See FdRoundControl. **/
|
||||
#define FD_ROUNDCONTROL(instr) ((FdRoundControl) (((instr)->evex & 0x70) >> 4))
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
563
third_party/fadec/format.c
vendored
Normal file
563
third_party/fadec/format.c
vendored
Normal file
@@ -0,0 +1,563 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#ifdef _MSC_VER
|
||||
#include <intrin.h>
|
||||
#endif
|
||||
|
||||
#include <fadec.h>
|
||||
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define LIKELY(x) __builtin_expect(!!(x), 1)
|
||||
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
|
||||
#define DECLARE_ARRAY_SIZE(n) static n
|
||||
#define DECLARE_RESTRICTED_ARRAY_SIZE(n) restrict static n
|
||||
#else
|
||||
#define LIKELY(x) (x)
|
||||
#define UNLIKELY(x) (x)
|
||||
#define DECLARE_ARRAY_SIZE(n) n
|
||||
#define DECLARE_RESTRICTED_ARRAY_SIZE(n) n
|
||||
#endif
|
||||
|
||||
#if defined(__has_attribute)
|
||||
#if __has_attribute(fallthrough)
|
||||
#define FALLTHROUGH() __attribute__((fallthrough))
|
||||
#endif
|
||||
#endif
|
||||
#if !defined(FALLTHROUGH)
|
||||
#define FALLTHROUGH() ((void)0)
|
||||
#endif
|
||||
|
||||
struct FdStr {
|
||||
const char* s;
|
||||
unsigned sz;
|
||||
};
|
||||
|
||||
#define fd_stre(s) ((struct FdStr) { (s "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"), sizeof (s)-1 })
|
||||
|
||||
static char*
|
||||
fd_strpcat(char* restrict dst, struct FdStr src) {
|
||||
#ifdef __GNUC__
|
||||
unsigned lim = __builtin_constant_p(src.sz) && src.sz <= 8 ? 8 : 16;
|
||||
#else
|
||||
unsigned lim = 16;
|
||||
#endif
|
||||
for (unsigned i = 0; i < lim; i++)
|
||||
dst[i] = src.s[i];
|
||||
// __builtin_memcpy(dst, src.s, 16);
|
||||
return dst + src.sz;
|
||||
}
|
||||
|
||||
static unsigned
|
||||
fd_clz64(uint64_t v) {
|
||||
#if defined(__GNUC__)
|
||||
return __builtin_clzll(v);
|
||||
#elif defined(_MSC_VER)
|
||||
unsigned long index;
|
||||
|
||||
// 32-bit MSVC doesn't support _BitScanReverse64. This is an attempt to
|
||||
// identify this case.
|
||||
#if INTPTR_MAX == INT64_MAX
|
||||
_BitScanReverse64(&index, v);
|
||||
#else
|
||||
if (_BitScanReverse(&index, v >> 32))
|
||||
return 31 - index;
|
||||
|
||||
_BitScanReverse(&index, v & 0xffffffff);
|
||||
#endif
|
||||
|
||||
return 63 - index;
|
||||
#else
|
||||
#error Unsupported compiler.
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(__SSE2__)
|
||||
#include <immintrin.h>
|
||||
#endif
|
||||
|
||||
static char*
|
||||
fd_strpcatnum(char dst[DECLARE_ARRAY_SIZE(18)], uint64_t val) {
|
||||
unsigned lz = fd_clz64(val|1);
|
||||
unsigned numbytes = 16 - (lz / 4);
|
||||
#if defined(__SSE2__)
|
||||
__m128i mv = _mm_set_epi64x(0, val << (lz & -4));
|
||||
__m128i mvp = _mm_unpacklo_epi8(mv, mv);
|
||||
__m128i mva = _mm_srli_epi16(mvp, 12);
|
||||
__m128i mvb = _mm_and_si128(mvp, _mm_set1_epi16(0x0f00u));
|
||||
__m128i ml = _mm_or_si128(mva, mvb);
|
||||
__m128i mn = _mm_or_si128(ml, _mm_set1_epi8(0x30));
|
||||
__m128i mgt = _mm_cmpgt_epi8(ml, _mm_set1_epi8(9));
|
||||
__m128i mgtm = _mm_and_si128(mgt, _mm_set1_epi8(0x61 - 0x3a));
|
||||
__m128i ma = _mm_add_epi8(mn, mgtm);
|
||||
__m128i msw = _mm_shufflehi_epi16(_mm_shufflelo_epi16(ma, 0x1b), 0x1b);
|
||||
__m128i ms = _mm_shuffle_epi32(msw, 0x4e);
|
||||
_mm_storeu_si128((__m128i_u*) (dst + 2), ms);
|
||||
#else
|
||||
unsigned idx = numbytes + 2;
|
||||
do {
|
||||
dst[--idx] = "0123456789abcdef"[val % 16];
|
||||
val /= 16;
|
||||
} while (val);
|
||||
#endif
|
||||
dst[0] = '0';
|
||||
dst[1] = 'x';
|
||||
return dst + numbytes + 2;
|
||||
}
|
||||
|
||||
static char*
|
||||
fd_strpcatreg(char* restrict dst, size_t rt, size_t ri, unsigned size) {
|
||||
const char* nametab =
|
||||
"\2al\4bnd0\2cl\4bnd1\2dl\4bnd2\2bl\4bnd3"
|
||||
"\3spl\0 \3bpl\0 \3sil\0 \3dil\0 "
|
||||
"\3r8b\0 \3r9b\0 \4r10b\0 \4r11b\0 "
|
||||
"\4r12b\2ah\4r13b\2ch\4r14b\2dh\4r15b\2bh\0\0 "
|
||||
|
||||
"\2ax\4tmm0\2cx\4tmm1\2dx\4tmm2\2bx\4tmm3"
|
||||
"\2sp\4tmm4\2bp\4tmm5\2si\4tmm6\2di\4tmm7"
|
||||
"\3r8w \2es\3r9w \2cs\4r10w\2ss\4r11w\2ds"
|
||||
"\4r12w\2fs\4r13w\2gs\4r14w\0 \4r15w\0 \2ip\0 "
|
||||
|
||||
"\3eax\3mm0\3ecx\3mm1\3edx\3mm2\3ebx\3mm3"
|
||||
"\3esp\3mm4\3ebp\3mm5\3esi\3mm6\3edi\3mm7"
|
||||
"\3r8d \2k0\3r9d \2k1\4r10d\2k2\4r11d\2k3"
|
||||
"\4r12d\2k4\4r13d\2k5\4r14d\2k6\4r15d\2k7\3eip\0 "
|
||||
|
||||
"\3rax\3cr0\3rcx\0 \3rdx\3cr2\3rbx\3cr3"
|
||||
"\3rsp\3cr4\3rbp\0 \3rsi\0 \3rdi\0 "
|
||||
"\2r8 \3cr8\2r9 \3dr0\3r10\3dr1\3r11\3dr2"
|
||||
"\3r12\3dr3\3r13\3dr4\3r14\3dr5\3r15\3dr6\3rip\3dr7"
|
||||
|
||||
"\5st(0)\0 \5st(1)\0 \5st(2)\0 \5st(3)\0 "
|
||||
"\5st(4)\0 \5st(5)\0 \5st(6)\0 \5st(7)\0 "
|
||||
|
||||
"\4xmm0\0 \4xmm1\0 \4xmm2\0 \4xmm3\0 "
|
||||
"\4xmm4\0 \4xmm5\0 \4xmm6\0 \4xmm7\0 "
|
||||
"\4xmm8\0 \4xmm9\0 \5xmm10\0 \5xmm11\0 "
|
||||
"\5xmm12\0 \5xmm13\0 \5xmm14\0 \5xmm15\0 "
|
||||
"\5xmm16\0 \5xmm17\0 \5xmm18\0 \5xmm19\0 "
|
||||
"\5xmm20\0 \5xmm21\0 \5xmm22\0 \5xmm23\0 "
|
||||
"\5xmm24\0 \5xmm25\0 \5xmm26\0 \5xmm27\0 "
|
||||
"\5xmm28\0 \5xmm29\0 \5xmm30\0 \5xmm31\0 ";
|
||||
|
||||
static const uint16_t nametabidx[] = {
|
||||
[FD_RT_GPL] = 0 * 17*8 + 0 * 8 + 0,
|
||||
[FD_RT_GPH] = 0 * 17*8 + 8 * 8 + 5,
|
||||
[FD_RT_SEG] = 1 * 17*8 + 8 * 8 + 5,
|
||||
[FD_RT_FPU] = 4 * 17*8 + 0 * 8 + 0,
|
||||
[FD_RT_MMX] = 2 * 17*8 + 0 * 8 + 4,
|
||||
[FD_RT_VEC] = 4 * 17*8 + 8 * 8 + 0,
|
||||
[FD_RT_MASK]= 2 * 17*8 + 8 * 8 + 5,
|
||||
[FD_RT_BND] = 0 * 17*8 + 0 * 8 + 3,
|
||||
[FD_RT_CR] = 3 * 17*8 + 0 * 8 + 4,
|
||||
[FD_RT_DR] = 3 * 17*8 + 9 * 8 + 4,
|
||||
[FD_RT_TMM] = 1 * 17*8 + 0 * 8 + 3,
|
||||
};
|
||||
|
||||
unsigned idx = rt == FD_RT_GPL ? size * 17*8 : nametabidx[rt];
|
||||
const char* name = nametab + idx + 8*ri;
|
||||
for (unsigned i = 0; i < 8; i++)
|
||||
dst[i] = name[i+1];
|
||||
if (UNLIKELY(rt == FD_RT_VEC && size > 4))
|
||||
dst[0] += size - 4;
|
||||
return dst + *name;
|
||||
}
|
||||
|
||||
const char*
|
||||
fdi_name(FdInstrType ty) {
|
||||
(void) ty;
|
||||
return "(invalid)";
|
||||
}
|
||||
|
||||
static char*
|
||||
fd_mnemonic(char buf[DECLARE_RESTRICTED_ARRAY_SIZE(48)], const FdInstr* instr) {
|
||||
#define FD_DECODE_TABLE_STRTAB1
|
||||
static const char* mnemonic_str =
|
||||
#include <fadec-decode-private.inc>
|
||||
// 20 NULL Bytes to prevent out-of-bounds reads
|
||||
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
||||
#undef FD_DECODE_TABLE_STRTAB1
|
||||
|
||||
#define FD_DECODE_TABLE_STRTAB2
|
||||
static const uint16_t mnemonic_offs[] = {
|
||||
#include <fadec-decode-private.inc>
|
||||
};
|
||||
#undef FD_DECODE_TABLE_STRTAB2
|
||||
|
||||
#define FD_DECODE_TABLE_STRTAB3
|
||||
static const uint8_t mnemonic_lens[] = {
|
||||
#include <fadec-decode-private.inc>
|
||||
};
|
||||
#undef FD_DECODE_TABLE_STRTAB3
|
||||
|
||||
const char* mnem = &mnemonic_str[mnemonic_offs[FD_TYPE(instr)]];
|
||||
unsigned mnemlen = mnemonic_lens[FD_TYPE(instr)];
|
||||
|
||||
bool prefix_xacq_xrel = false;
|
||||
bool prefix_segment = false;
|
||||
|
||||
char sizesuffix[4] = {0};
|
||||
unsigned sizesuffixlen = 0;
|
||||
|
||||
if (UNLIKELY(FD_OP_TYPE(instr, 0) == FD_OT_OFF && FD_OP_SIZELG(instr, 0) == 1))
|
||||
sizesuffix[0] = 'w', sizesuffixlen = 1;
|
||||
|
||||
switch (FD_TYPE(instr)) {
|
||||
case FDI_C_SEP:
|
||||
mnem += FD_OPSIZE(instr) & 0xc;
|
||||
mnemlen = 3;
|
||||
break;
|
||||
case FDI_C_EX:
|
||||
mnem += FD_OPSIZE(instr) & 0xc;
|
||||
mnemlen = FD_OPSIZE(instr) < 4 ? 3 : 4;
|
||||
break;
|
||||
case FDI_CMPXCHGD:
|
||||
switch (FD_OPSIZELG(instr)) {
|
||||
default: break;
|
||||
case 2: sizesuffix[0] = '8', sizesuffix[1] = 'b', sizesuffixlen = 2; break;
|
||||
case 3: sizesuffix[0] = '1', sizesuffix[1] = '6', sizesuffix[2] = 'b', sizesuffixlen = 3; break;
|
||||
}
|
||||
break;
|
||||
case FDI_JCXZ:
|
||||
mnemlen = FD_ADDRSIZELG(instr) == 1 ? 4 : 5;
|
||||
mnem += 5 * (FD_ADDRSIZELG(instr) - 1);
|
||||
break;
|
||||
case FDI_PUSH:
|
||||
if (FD_OP_SIZELG(instr, 0) == 1 && FD_OP_TYPE(instr, 0) == FD_OT_IMM)
|
||||
sizesuffix[0] = 'w', sizesuffixlen = 1;
|
||||
FALLTHROUGH();
|
||||
case FDI_POP:
|
||||
if (FD_OP_SIZELG(instr, 0) == 1 && FD_OP_TYPE(instr, 0) == FD_OT_REG &&
|
||||
FD_OP_REG_TYPE(instr, 0) == FD_RT_SEG)
|
||||
sizesuffix[0] = 'w', sizesuffixlen = 1;
|
||||
break;
|
||||
case FDI_XCHG:
|
||||
if (FD_OP_TYPE(instr, 0) == FD_OT_MEM)
|
||||
prefix_xacq_xrel = true;
|
||||
break;
|
||||
case FDI_MOV:
|
||||
// MOV C6h/C7h can have XRELEASE prefix.
|
||||
if (FD_HAS_REP(instr) && FD_OP_TYPE(instr, 0) == FD_OT_MEM &&
|
||||
FD_OP_TYPE(instr, 1) == FD_OT_IMM)
|
||||
prefix_xacq_xrel = true;
|
||||
break;
|
||||
case FDI_FXSAVE:
|
||||
case FDI_FXRSTOR:
|
||||
case FDI_XSAVE:
|
||||
case FDI_XSAVEC:
|
||||
case FDI_XSAVEOPT:
|
||||
case FDI_XSAVES:
|
||||
case FDI_XRSTOR:
|
||||
case FDI_XRSTORS:
|
||||
if (FD_OPSIZELG(instr) == 3)
|
||||
sizesuffix[0] = '6', sizesuffix[1] = '4', sizesuffixlen = 2;
|
||||
break;
|
||||
case FDI_EVX_MOV_G2X:
|
||||
case FDI_EVX_MOV_X2G:
|
||||
case FDI_EVX_PEXTR:
|
||||
sizesuffix[0] = "bwdq"[FD_OP_SIZELG(instr, 0)];
|
||||
sizesuffixlen = 1;
|
||||
break;
|
||||
case FDI_EVX_PBROADCAST:
|
||||
sizesuffix[0] = "bwdq"[FD_OP_SIZELG(instr, 1)];
|
||||
sizesuffixlen = 1;
|
||||
break;
|
||||
case FDI_EVX_PINSR:
|
||||
sizesuffix[0] = "bwdq"[FD_OP_SIZELG(instr, 2)];
|
||||
sizesuffixlen = 1;
|
||||
break;
|
||||
case FDI_RET:
|
||||
case FDI_ENTER:
|
||||
case FDI_LEAVE:
|
||||
if (FD_OPSIZELG(instr) == 1)
|
||||
sizesuffix[0] = 'w', sizesuffixlen = 1;
|
||||
break;
|
||||
case FDI_LODS:
|
||||
case FDI_MOVS:
|
||||
case FDI_CMPS:
|
||||
case FDI_OUTS:
|
||||
prefix_segment = true;
|
||||
FALLTHROUGH();
|
||||
case FDI_STOS:
|
||||
case FDI_SCAS:
|
||||
case FDI_INS:
|
||||
if (FD_HAS_REP(instr))
|
||||
buf = fd_strpcat(buf, fd_stre("rep "));
|
||||
if (FD_HAS_REPNZ(instr))
|
||||
buf = fd_strpcat(buf, fd_stre("repnz "));
|
||||
if (FD_IS64(instr) && FD_ADDRSIZELG(instr) == 2)
|
||||
buf = fd_strpcat(buf, fd_stre("addr32 "));
|
||||
if (!FD_IS64(instr) && FD_ADDRSIZELG(instr) == 1)
|
||||
buf = fd_strpcat(buf, fd_stre("addr16 "));
|
||||
FALLTHROUGH();
|
||||
case FDI_IN:
|
||||
case FDI_OUT:
|
||||
if (FD_OP_TYPE(instr, 0) != FD_OT_NONE)
|
||||
break;
|
||||
FALLTHROUGH();
|
||||
case FDI_PUSHA:
|
||||
case FDI_POPA:
|
||||
case FDI_PUSHF:
|
||||
case FDI_POPF:
|
||||
case FDI_RETF:
|
||||
case FDI_IRET:
|
||||
sizesuffix[0] = "bwdq"[FD_OPSIZELG(instr)];
|
||||
sizesuffixlen = 1;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (UNLIKELY(prefix_xacq_xrel || FD_HAS_LOCK(instr))) {
|
||||
if (FD_HAS_REP(instr))
|
||||
buf = fd_strpcat(buf, fd_stre("xrelease "));
|
||||
if (FD_HAS_REPNZ(instr))
|
||||
buf = fd_strpcat(buf, fd_stre("xacquire "));
|
||||
}
|
||||
if (UNLIKELY(FD_HAS_LOCK(instr)))
|
||||
buf = fd_strpcat(buf, fd_stre("lock "));
|
||||
if (UNLIKELY(prefix_segment && FD_SEGMENT(instr) != FD_REG_NONE)) {
|
||||
*buf++ = "ecsdfg\0"[FD_SEGMENT(instr) & 7];
|
||||
*buf++ = 's';
|
||||
*buf++ = ' ';
|
||||
}
|
||||
|
||||
for (unsigned i = 0; i < 20; i++)
|
||||
buf[i] = mnem[i];
|
||||
buf += mnemlen;
|
||||
for (unsigned i = 0; i < 4; i++)
|
||||
buf[i] = sizesuffix[i];
|
||||
buf += sizesuffixlen;
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
static char*
|
||||
fd_format_impl(char buf[DECLARE_RESTRICTED_ARRAY_SIZE(128)], const FdInstr* instr, uint64_t addr) {
|
||||
buf = fd_mnemonic(buf, instr);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
FdOpType op_type = FD_OP_TYPE(instr, i);
|
||||
if (op_type == FD_OT_NONE)
|
||||
break;
|
||||
if (i > 0)
|
||||
*buf++ = ',';
|
||||
*buf++ = ' ';
|
||||
|
||||
int size = FD_OP_SIZELG(instr, i);
|
||||
|
||||
if (op_type == FD_OT_REG) {
|
||||
unsigned type = FD_OP_REG_TYPE(instr, i);
|
||||
unsigned idx = FD_OP_REG(instr, i);
|
||||
buf = fd_strpcatreg(buf, type, idx, size);
|
||||
} else if (op_type == FD_OT_MEM || op_type == FD_OT_MEMBCST) {
|
||||
unsigned idx_rt = FD_RT_GPL;
|
||||
unsigned idx_sz = FD_ADDRSIZELG(instr);
|
||||
switch (FD_TYPE(instr)) {
|
||||
case FDI_CMPXCHGD: size = FD_OPSIZELG(instr) + 1; break;
|
||||
case FDI_BOUND: size += 1; break;
|
||||
case FDI_JMPF:
|
||||
case FDI_CALLF:
|
||||
case FDI_LDS:
|
||||
case FDI_LES:
|
||||
case FDI_LFS:
|
||||
case FDI_LGS:
|
||||
case FDI_LSS:
|
||||
size += 6;
|
||||
break;
|
||||
case FDI_FLD:
|
||||
case FDI_FSTP:
|
||||
case FDI_FBLD:
|
||||
case FDI_FBSTP:
|
||||
size = size >= 0 ? size : 9;
|
||||
break;
|
||||
case FDI_VPGATHERQD:
|
||||
case FDI_VGATHERQPS:
|
||||
case FDI_EVX_PGATHERQD:
|
||||
case FDI_EVX_GATHERQPS:
|
||||
idx_rt = FD_RT_VEC;
|
||||
idx_sz = FD_OP_SIZELG(instr, 0) + 1;
|
||||
break;
|
||||
case FDI_EVX_PSCATTERQD:
|
||||
case FDI_EVX_SCATTERQPS:
|
||||
idx_rt = FD_RT_VEC;
|
||||
idx_sz = FD_OP_SIZELG(instr, 1) + 1;
|
||||
break;
|
||||
case FDI_VPGATHERDQ:
|
||||
case FDI_VGATHERDPD:
|
||||
case FDI_EVX_PGATHERDQ:
|
||||
case FDI_EVX_GATHERDPD:
|
||||
idx_rt = FD_RT_VEC;
|
||||
idx_sz = FD_OP_SIZELG(instr, 0) - 1;
|
||||
break;
|
||||
case FDI_EVX_PSCATTERDQ:
|
||||
case FDI_EVX_SCATTERDPD:
|
||||
idx_rt = FD_RT_VEC;
|
||||
idx_sz = FD_OP_SIZELG(instr, 1) - 1;
|
||||
break;
|
||||
case FDI_VPGATHERDD:
|
||||
case FDI_VPGATHERQQ:
|
||||
case FDI_VGATHERDPS:
|
||||
case FDI_VGATHERQPD:
|
||||
case FDI_EVX_PGATHERDD:
|
||||
case FDI_EVX_PGATHERQQ:
|
||||
case FDI_EVX_GATHERDPS:
|
||||
case FDI_EVX_GATHERQPD:
|
||||
idx_rt = FD_RT_VEC;
|
||||
idx_sz = FD_OP_SIZELG(instr, 0);
|
||||
break;
|
||||
case FDI_EVX_PSCATTERDD:
|
||||
case FDI_EVX_PSCATTERQQ:
|
||||
case FDI_EVX_SCATTERDPS:
|
||||
case FDI_EVX_SCATTERQPD:
|
||||
idx_rt = FD_RT_VEC;
|
||||
idx_sz = FD_OP_SIZELG(instr, 1);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
if (op_type == FD_OT_MEMBCST)
|
||||
size = FD_OP_BCSTSZLG(instr, i);
|
||||
|
||||
const char* ptrsizes =
|
||||
"\00 "
|
||||
"\11byte ptr "
|
||||
"\11word ptr "
|
||||
"\12dword ptr "
|
||||
"\12qword ptr "
|
||||
"\14xmmword ptr "
|
||||
"\14ymmword ptr "
|
||||
"\14zmmword ptr "
|
||||
"\12dword ptr " // far ptr; word + 2
|
||||
"\12fword ptr " // far ptr; dword + 2
|
||||
"\12tbyte ptr "; // far ptr/FPU; qword + 2
|
||||
const char* ptrsize = ptrsizes + 16 * (size + 1);
|
||||
buf = fd_strpcat(buf, (struct FdStr) { ptrsize+1, *ptrsize });
|
||||
|
||||
unsigned seg = FD_SEGMENT(instr);
|
||||
if (seg != FD_REG_NONE) {
|
||||
*buf++ = "ecsdfg\0"[seg & 7];
|
||||
*buf++ = 's';
|
||||
*buf++ = ':';
|
||||
}
|
||||
*buf++ = '[';
|
||||
|
||||
bool has_base = FD_OP_BASE(instr, i) != FD_REG_NONE;
|
||||
bool has_idx = FD_OP_INDEX(instr, i) != FD_REG_NONE;
|
||||
if (has_base)
|
||||
buf = fd_strpcatreg(buf, FD_RT_GPL, FD_OP_BASE(instr, i), FD_ADDRSIZELG(instr));
|
||||
if (has_idx) {
|
||||
if (has_base)
|
||||
*buf++ = '+';
|
||||
*buf++ = '0' + (1 << FD_OP_SCALE(instr, i));
|
||||
*buf++ = '*';
|
||||
buf = fd_strpcatreg(buf, idx_rt, FD_OP_INDEX(instr, i), idx_sz);
|
||||
}
|
||||
uint64_t disp = FD_OP_DISP(instr, i);
|
||||
if (disp && (has_base || has_idx)) {
|
||||
*buf++ = (int64_t) disp < 0 ? '-' : '+';
|
||||
if ((int64_t) disp < 0)
|
||||
disp = -disp;
|
||||
}
|
||||
if (FD_ADDRSIZELG(instr) == 1)
|
||||
disp &= 0xffff;
|
||||
else if (FD_ADDRSIZELG(instr) == 2)
|
||||
disp &= 0xffffffff;
|
||||
if (disp || (!has_base && !has_idx))
|
||||
buf = fd_strpcatnum(buf, disp);
|
||||
*buf++ = ']';
|
||||
|
||||
if (UNLIKELY(op_type == FD_OT_MEMBCST)) {
|
||||
// {1toX}, X = FD_OP_SIZE(instr, i) / BCSTSZ (=> 2/4/8/16/32)
|
||||
unsigned bcstszidx = FD_OP_SIZELG(instr, i) - FD_OP_BCSTSZLG(instr, i) - 1;
|
||||
const char* bcstsizes = "\6{1to2} \6{1to4} \6{1to8} \7{1to16}\7{1to32} ";
|
||||
const char* bcstsize = bcstsizes + bcstszidx * 8;
|
||||
buf = fd_strpcat(buf, (struct FdStr) { bcstsize+1, *bcstsize });
|
||||
}
|
||||
} else if (op_type == FD_OT_IMM || op_type == FD_OT_OFF) {
|
||||
uint64_t immediate = FD_OP_IMM(instr, i);
|
||||
// Some instructions have actually two immediate operands which are
|
||||
// decoded as a single operand. Split them here appropriately.
|
||||
switch (FD_TYPE(instr)) {
|
||||
default:
|
||||
goto nosplitimm;
|
||||
case FDI_SSE_EXTRQ:
|
||||
case FDI_SSE_INSERTQ:
|
||||
buf = fd_strpcatnum(buf, immediate & 0xff);
|
||||
buf = fd_strpcat(buf, fd_stre(", "));
|
||||
immediate = (immediate >> 8) & 0xff;
|
||||
break;
|
||||
case FDI_ENTER:
|
||||
buf = fd_strpcatnum(buf, immediate & 0xffff);
|
||||
buf = fd_strpcat(buf, fd_stre(", "));
|
||||
immediate = (immediate >> 16) & 0xff;
|
||||
break;
|
||||
case FDI_JMPF:
|
||||
case FDI_CALLF:
|
||||
buf = fd_strpcatnum(buf, (immediate >> (8 << size)) & 0xffff);
|
||||
*buf++ = ':';
|
||||
// immediate is masked below.
|
||||
break;
|
||||
}
|
||||
|
||||
nosplitimm:
|
||||
if (op_type == FD_OT_OFF)
|
||||
immediate += addr + FD_SIZE(instr);
|
||||
if (size == 0)
|
||||
immediate &= 0xff;
|
||||
else if (size == 1)
|
||||
immediate &= 0xffff;
|
||||
else if (size == 2)
|
||||
immediate &= 0xffffffff;
|
||||
buf = fd_strpcatnum(buf, immediate);
|
||||
}
|
||||
|
||||
if (i == 0 && FD_MASKREG(instr)) {
|
||||
*buf++ = '{';
|
||||
buf = fd_strpcatreg(buf, FD_RT_MASK, FD_MASKREG(instr), 0);
|
||||
*buf++ = '}';
|
||||
if (FD_MASKZERO(instr))
|
||||
buf = fd_strpcat(buf, fd_stre("{z}"));
|
||||
}
|
||||
}
|
||||
if (UNLIKELY(FD_ROUNDCONTROL(instr) != FD_RC_MXCSR)) {
|
||||
switch (FD_ROUNDCONTROL(instr)) {
|
||||
case FD_RC_RN: buf = fd_strpcat(buf, fd_stre(", {rn-sae}")); break;
|
||||
case FD_RC_RD: buf = fd_strpcat(buf, fd_stre(", {rd-sae}")); break;
|
||||
case FD_RC_RU: buf = fd_strpcat(buf, fd_stre(", {ru-sae}")); break;
|
||||
case FD_RC_RZ: buf = fd_strpcat(buf, fd_stre(", {rz-sae}")); break;
|
||||
case FD_RC_SAE: buf = fd_strpcat(buf, fd_stre(", {sae}")); break;
|
||||
default: break; // should not happen
|
||||
}
|
||||
}
|
||||
*buf++ = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
void
|
||||
fd_format(const FdInstr* instr, char* buffer, size_t len)
|
||||
{
|
||||
fd_format_abs(instr, 0, buffer, len);
|
||||
}
|
||||
|
||||
void
|
||||
fd_format_abs(const FdInstr* instr, uint64_t addr, char* restrict buffer, size_t len) {
|
||||
char tmp[128];
|
||||
char* buf = buffer;
|
||||
if (UNLIKELY(len < 128)) {
|
||||
if (!len)
|
||||
return;
|
||||
buf = tmp;
|
||||
}
|
||||
|
||||
char* end = fd_format_impl(buf, instr, addr);
|
||||
|
||||
if (buf != buffer) {
|
||||
unsigned i;
|
||||
for (i = 0; i < (end - tmp) && i < len-1; i++)
|
||||
buffer[i] = tmp[i];
|
||||
buffer[i] = '\0';
|
||||
}
|
||||
}
|
||||
2596
third_party/fadec/instrs.txt
vendored
Normal file
2596
third_party/fadec/instrs.txt
vendored
Normal file
File diff suppressed because it is too large
Load Diff
126
third_party/fadec/meson.build
vendored
Normal file
126
third_party/fadec/meson.build
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
project('fadec', ['c'], default_options: ['warning_level=3', 'c_std=c11'],
|
||||
meson_version: '>=0.49')
|
||||
|
||||
python3 = find_program('python3')
|
||||
|
||||
# Check Python version
|
||||
py_version_res = run_command(python3, ['--version'], check: true)
|
||||
py_version = py_version_res.stdout().split(' ')[1]
|
||||
if not py_version.version_compare('>=3.9')
|
||||
error('Python 3.9 required, got @0@'.format(py_version))
|
||||
endif
|
||||
|
||||
has_cpp = add_languages('cpp', required: false)
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
if cc.has_argument('-fstrict-aliasing')
|
||||
add_project_arguments('-fstrict-aliasing', language: 'c')
|
||||
endif
|
||||
if get_option('warning_level').to_int() >= 3
|
||||
extra_warnings = [
|
||||
'-Wmissing-prototypes', '-Wshadow', '-Wwrite-strings', '-Wswitch-default',
|
||||
'-Winline', '-Wstrict-prototypes', '-Wundef',
|
||||
# We have strings longer than 4095 characters
|
||||
'-Wno-overlength-strings',
|
||||
# GCC 8 requires an extra option for strict cast alignment checks, Clang
|
||||
# always warns, even on architectures without alignment requirements.
|
||||
'-Wcast-align', '-Wcast-align=strict',
|
||||
]
|
||||
add_project_arguments(cc.get_supported_arguments(extra_warnings), language: 'c')
|
||||
endif
|
||||
if cc.get_argument_syntax() == 'msvc'
|
||||
# Disable some warnings to align warnings with GCC and Clang:
|
||||
add_project_arguments('-D_CRT_SECURE_NO_WARNINGS',
|
||||
'/wd4018', # - Signed/unsigned comparison
|
||||
'/wd4146', # - Unary minus operator applied to unsigned
|
||||
# type, result still unsigned
|
||||
'/wd4244', # - Possible loss of data in conversion
|
||||
# from integer type to smaller integer type
|
||||
'/wd4245', # - Signed/unsigned assignment
|
||||
'/wd4267', # - Possible loss of data in conversion
|
||||
# from size_t to smaller type
|
||||
'/wd4310', # - Possible loss of data in conversion
|
||||
# of constant value to smaller type
|
||||
language: 'c')
|
||||
endif
|
||||
if cc.get_id() == 'msvc' and has_cpp
|
||||
cxx = meson.get_compiler('cpp')
|
||||
if cxx.get_id() == 'msvc'
|
||||
# Enable standard conformant preprocessor
|
||||
add_project_arguments(cxx.get_supported_arguments(['-Zc:preprocessor']), language: 'cpp')
|
||||
endif
|
||||
endif
|
||||
|
||||
sources = []
|
||||
headers = []
|
||||
components = []
|
||||
|
||||
if get_option('with_decode')
|
||||
components += 'decode'
|
||||
headers += files('fadec.h')
|
||||
sources += files('decode.c', 'format.c')
|
||||
endif
|
||||
if get_option('with_encode')
|
||||
components += 'encode'
|
||||
headers += files('fadec-enc.h')
|
||||
sources += files('encode.c')
|
||||
endif
|
||||
if get_option('with_encode2')
|
||||
components += 'encode2'
|
||||
headers += files('fadec-enc2.h')
|
||||
sources += files('encode2.c')
|
||||
endif
|
||||
|
||||
generate_args = []
|
||||
if get_option('archmode') != 'only64'
|
||||
generate_args += ['--32']
|
||||
endif
|
||||
if get_option('archmode') != 'only32'
|
||||
generate_args += ['--64']
|
||||
endif
|
||||
if get_option('with_undoc')
|
||||
generate_args += ['--with-undoc']
|
||||
endif
|
||||
if not meson.is_subproject()
|
||||
generate_args += ['--stats']
|
||||
endif
|
||||
|
||||
tables = []
|
||||
foreach component : components
|
||||
tables += custom_target('@0@_table'.format(component),
|
||||
command: [python3, '@INPUT0@', component,
|
||||
'@INPUT1@', '@OUTPUT@'] + generate_args,
|
||||
input: files('parseinstrs.py', 'instrs.txt'),
|
||||
output: ['fadec-@0@-public.inc'.format(component),
|
||||
'fadec-@0@-private.inc'.format(component)],
|
||||
install: true,
|
||||
install_dir: [get_option('includedir'), false])
|
||||
endforeach
|
||||
|
||||
libfadec = static_library('fadec', sources, tables, install: true)
|
||||
fadec = declare_dependency(link_with: libfadec,
|
||||
include_directories: include_directories('.'),
|
||||
sources: tables)
|
||||
install_headers(headers)
|
||||
|
||||
foreach component : components
|
||||
test(component, executable('@0@-test'.format(component),
|
||||
'@0@-test.c'.format(component),
|
||||
dependencies: fadec))
|
||||
if component == 'encode2' and has_cpp
|
||||
test(component + '-cpp', executable('@0@-test-cpp'.format(component),
|
||||
'@0@-test.cc'.format(component),
|
||||
dependencies: fadec))
|
||||
endif
|
||||
endforeach
|
||||
|
||||
if meson.version().version_compare('>=0.54.0')
|
||||
meson.override_dependency('fadec', fadec)
|
||||
endif
|
||||
|
||||
pkg = import('pkgconfig')
|
||||
pkg.generate(libraries: libfadec,
|
||||
version: '0.1',
|
||||
name: 'fadec',
|
||||
filebase: 'fadec',
|
||||
description: 'Fast Decoder for x86-32 and x86-64')
|
||||
6
third_party/fadec/meson_options.txt
vendored
Normal file
6
third_party/fadec/meson_options.txt
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
option('archmode', type: 'combo', choices: ['both', 'only32', 'only64'])
|
||||
option('with_undoc', type: 'boolean', value: false)
|
||||
option('with_decode', type: 'boolean', value: true)
|
||||
option('with_encode', type: 'boolean', value: true)
|
||||
# encode2 is off-by-default to reduce size and compile-time
|
||||
option('with_encode2', type: 'boolean', value: false)
|
||||
1403
third_party/fadec/parseinstrs.py
vendored
Normal file
1403
third_party/fadec/parseinstrs.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,9 @@
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <sys/select.h>
|
||||
#endif
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
|
||||
Reference in New Issue
Block a user