Compare commits
284 Commits
plugin-sys
...
mac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4727df3e9 | ||
|
|
dc6963e0d5 | ||
|
|
cb10bc8a82 | ||
|
|
b5521bd638 | ||
|
|
89d6e1944b | ||
|
|
7528d1bbbb | ||
|
|
4f2288048e | ||
|
|
97b6f55e1f | ||
|
|
6a30e0a402 | ||
|
|
1501a1542c | ||
|
|
4f82b39785 | ||
|
|
009ddc951c | ||
|
|
5921af2b4f | ||
|
|
5ded192990 | ||
|
|
54bee5022b | ||
|
|
5d2d324946 | ||
|
|
5b2cf1ae1f | ||
|
|
f1a36f2ad3 | ||
|
|
665138e688 | ||
|
|
7688bb5b92 | ||
|
|
701e088be8 | ||
|
|
3c0c248d54 | ||
|
|
7af969f6bd | ||
|
|
8ba1fd2492 | ||
|
|
b08736245b | ||
|
|
7f7bbdcc45 | ||
|
|
79b5125229 | ||
|
|
3aeb1a80d5 | ||
|
|
3b7ed682ac | ||
|
|
0582cb286b | ||
|
|
ea85b7a621 | ||
|
|
6c8b7d3d97 | ||
|
|
d1321b5165 | ||
|
|
483f87cfbd | ||
|
|
4d0782db68 | ||
|
|
51de48a6ed | ||
|
|
7b9b140823 | ||
|
|
a21e5a07a8 | ||
|
|
25afbe373b | ||
|
|
6a4cb47ed4 | ||
|
|
431e2b90c9 | ||
|
|
43365c1aff | ||
|
|
596f410b96 | ||
|
|
f0fc85f60f | ||
|
|
70c7404556 | ||
|
|
f27459c21b | ||
|
|
a5abcbeea6 | ||
|
|
7071402319 | ||
|
|
0dc390ed86 | ||
|
|
188c27c6e2 | ||
|
|
81f1e4319f | ||
|
|
3ab6affa5e | ||
|
|
35b3cd9ac1 | ||
|
|
e5938f7e82 | ||
|
|
03c49d19dd | ||
|
|
b7eebedf50 | ||
|
|
9ff456a8d6 | ||
|
|
580f285edd | ||
|
|
d23a6c7656 | ||
|
|
25d8de95b7 | ||
|
|
955db3813a | ||
|
|
f4f203e0f0 | ||
|
|
1d3f1a672a | ||
|
|
da29206bdb | ||
|
|
4986893fca | ||
|
|
17a1fb032e | ||
|
|
8d92957837 | ||
|
|
f981fe456d | ||
|
|
877ceea4c1 | ||
|
|
4160a229c6 | ||
|
|
1e1afc1640 | ||
|
|
f0cf6c549a | ||
|
|
683eab16ee | ||
|
|
b53dea8f9f | ||
|
|
f06abbab79 | ||
|
|
2477591ed2 | ||
|
|
6c13356d6d | ||
|
|
3b273a7ab2 | ||
|
|
3509a0d9dd | ||
|
|
43c3f5a842 | ||
|
|
0697ce4853 | ||
|
|
ed1bfd04cd | ||
|
|
c275eb33c9 | ||
|
|
636176ee8c | ||
|
|
9a716444f4 | ||
|
|
a46da4ee16 | ||
|
|
cd52451210 | ||
|
|
82bf9118c9 | ||
|
|
f4c7e9327d | ||
|
|
5944dbdc81 | ||
|
|
b3425aec9e | ||
|
|
2a8cfee719 | ||
|
|
e999c664b8 | ||
|
|
0dc4af6b1d | ||
|
|
376aad2169 | ||
|
|
4937c58062 | ||
|
|
9c72265901 | ||
|
|
86499e58ee | ||
|
|
b2ae8d5a5d | ||
|
|
6768f04e9a | ||
|
|
c6e5f6508f | ||
|
|
e6529052b3 | ||
|
|
d43e989992 | ||
|
|
879e9f4047 | ||
|
|
e0d5a799b4 | ||
|
|
efae193520 | ||
|
|
ba1c2f8e5a | ||
|
|
5a0a4d1802 | ||
|
|
030eb34510 | ||
|
|
2939b25895 | ||
|
|
d38cb02fa2 | ||
|
|
9f285b37b2 | ||
|
|
cae599a0c6 | ||
|
|
d0734ba8be | ||
|
|
696ff044ac | ||
|
|
da312ccac6 | ||
|
|
552b45b16c | ||
|
|
e89fd4a6c1 | ||
|
|
7524004b32 | ||
|
|
ed8a44917b | ||
|
|
ecfac3decf | ||
|
|
851d744263 | ||
|
|
41e2f9f662 | ||
|
|
95faf027a9 | ||
|
|
6a51c904de | ||
|
|
0d73575ea7 | ||
|
|
aa04cfcb5c | ||
|
|
1465e7fbed | ||
|
|
52f751e751 | ||
|
|
0a19789a9d | ||
|
|
62a68bef80 | ||
|
|
4941f860b6 | ||
|
|
c45d51d736 | ||
|
|
5b46065403 | ||
|
|
4706f7b782 | ||
|
|
fe9bfafa3b | ||
|
|
ff928df685 | ||
|
|
d6e3c182fc | ||
|
|
078a6028f0 | ||
|
|
d7a6e1862e | ||
|
|
1ddf47a754 | ||
|
|
1a885a8b1d | ||
|
|
67218d3e48 | ||
|
|
f651edd740 | ||
|
|
25aaace382 | ||
|
|
b5ddb042b8 | ||
|
|
e900dea836 | ||
|
|
b647a334bc | ||
|
|
fc390bc1f7 | ||
|
|
7efe740ec1 | ||
|
|
48409d1d38 | ||
|
|
df1435d9b7 | ||
|
|
5e11ff5496 | ||
|
|
22842d9801 | ||
|
|
50acde60cb | ||
|
|
1d7d384b93 | ||
|
|
3a76b03c85 | ||
|
|
ac94855d6c | ||
|
|
d65b6c5a29 | ||
|
|
d45ee9e4c9 | ||
|
|
31115014a5 | ||
|
|
8e88d588be | ||
|
|
b089e20d36 | ||
|
|
5fa1dd0ab4 | ||
|
|
3b1fe7ff35 | ||
|
|
4595b366e3 | ||
|
|
33d7dc74cb | ||
|
|
e118231bb1 | ||
|
|
0cfd7ad87a | ||
|
|
2d3ce63b54 | ||
|
|
0e087fa3a4 | ||
|
|
c7afe363f3 | ||
|
|
2a44d2ac57 | ||
|
|
d989e2a947 | ||
|
|
7678da033d | ||
|
|
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 | ||
|
|
c856ba2697 | ||
|
|
b44dc9e96b | ||
|
|
0f2ded471f | ||
|
|
c9377c3afd | ||
|
|
a86912add1 | ||
|
|
5a9a6b754f | ||
|
|
0df52e82b8 | ||
|
|
9a342286ee | ||
|
|
1af0e17ef8 | ||
|
|
a45d66dd4e | ||
|
|
6922166e3c | ||
|
|
ffde3343dd | ||
|
|
c86a6dbc73 | ||
|
|
b153665059 | ||
|
|
a88b584ca0 | ||
|
|
3e827194b8 | ||
|
|
9f1c85913c | ||
|
|
cb151ab850 | ||
|
|
b0aa7cda67 | ||
|
|
4b1d3e9d3f | ||
|
|
e73b783cda | ||
|
|
7e0b995f4d | ||
|
|
52d65b4a23 | ||
|
|
db5d3ae311 | ||
|
|
33a093ae7d | ||
|
|
968476b65a | ||
|
|
df07b61144 | ||
|
|
3db051f4ba | ||
|
|
fc48fd6d2d | ||
|
|
0ffb7d6f58 | ||
|
|
0b0d9f23e1 | ||
|
|
7194322831 | ||
|
|
5f1fd56171 | ||
|
|
6bd61a6b78 | ||
|
|
60990362a0 | ||
|
|
55ef83a39f | ||
|
|
e034fe6f6c | ||
|
|
2e387f2dfc | ||
|
|
4295460597 | ||
|
|
df566064ba | ||
|
|
24a7e68136 | ||
|
|
8eab304538 | ||
|
|
9dd104ff34 | ||
|
|
910b607b79 | ||
|
|
c415b11825 | ||
|
|
1d6fddb51e | ||
|
|
276dcae444 | ||
|
|
659fb7bd32 | ||
|
|
85b840379d | ||
|
|
f4149faa9a | ||
|
|
b6c713eb29 | ||
|
|
4029b05298 | ||
|
|
0e65b9997e | ||
|
|
4caa7daa44 |
262
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Qt6 and MinGW
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: "6.8.1"
|
||||
arch: "win64_mingw"
|
||||
tools: "tools_mingw1310,qt.tools.win64_mingw1310"
|
||||
cache: true
|
||||
|
||||
- name: Configure
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
|
||||
gcc --version
|
||||
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF \
|
||||
-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
|
||||
cmake --build build
|
||||
|
||||
- name: Install WDK NuGet
|
||||
shell: pwsh
|
||||
run: |
|
||||
nuget install Microsoft.Windows.WDK.x64 -OutputDirectory wdk_pkg
|
||||
$ntddk = Get-ChildItem wdk_pkg -Recurse -Filter "ntddk.h" |
|
||||
Where-Object { $_.DirectoryName -like "*km*" } |
|
||||
Select-Object -First 1
|
||||
if (!$ntddk) { throw "ntddk.h not found in WDK NuGet package" }
|
||||
$kmDir = $ntddk.DirectoryName
|
||||
$incRoot = Split-Path $kmDir -Parent
|
||||
Write-Host "WDK include root: $incRoot"
|
||||
echo "WDK_INC_ROOT=$incRoot" >> $env:GITHUB_ENV
|
||||
$ntos = Get-ChildItem wdk_pkg -Recurse -Filter "ntoskrnl.lib" |
|
||||
Where-Object { $_.DirectoryName -like "*x64*" } |
|
||||
Select-Object -First 1
|
||||
if (!$ntos) { throw "ntoskrnl.lib not found in WDK NuGet package" }
|
||||
$libRoot = Split-Path (Split-Path $ntos.DirectoryName -Parent) -Parent
|
||||
Write-Host "WDK lib root: $libRoot"
|
||||
echo "WDK_LIB_ROOT=$libRoot" >> $env:GITHUB_ENV
|
||||
$specstr = Get-ChildItem wdk_pkg -Recurse -Filter "specstrings.h" |
|
||||
Select-Object -First 1
|
||||
if (!$specstr) { throw "specstrings.h not found in SDK NuGet package" }
|
||||
$sdkIncRoot = Split-Path $specstr.DirectoryName -Parent
|
||||
Write-Host "SDK include root: $sdkIncRoot"
|
||||
echo "SDK_INC_ROOT=$sdkIncRoot" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Build kernel driver
|
||||
shell: cmd
|
||||
run: call plugins\KernelMemory\driver\build_driver.bat
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
|
||||
ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Package release zip
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
|
||||
mkdir -p release
|
||||
cp build/Reclass.exe release/
|
||||
cp build/ReclassMcpBridge.exe release/
|
||||
cp build/*.dll release/ 2>/dev/null || true
|
||||
cp -r build/platforms release/ 2>/dev/null || true
|
||||
cp -r build/styles release/ 2>/dev/null || true
|
||||
cp -r build/imageformats release/ 2>/dev/null || true
|
||||
cp -r build/iconengines release/ 2>/dev/null || true
|
||||
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw release/Reclass.exe
|
||||
mkdir -p release/Plugins
|
||||
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
|
||||
cp plugins/KernelMemory/driver/build/rcxdrv.sys release/Plugins/ 2>/dev/null || true
|
||||
cp -r build/themes release/ 2>/dev/null || true
|
||||
cp -r build/examples release/ 2>/dev/null || true
|
||||
cp build/screenshot.png release/ 2>/dev/null || true
|
||||
cd release && 7z a ../Reclass-win64-qt6.zip *
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Reclass-win64-qt6
|
||||
path: Reclass-win64-qt6.zip
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Qt6
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: "6.8.1"
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ninja-build libgl1-mesa-dev libfuse2 libxcb-cursor0
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Create AppImage
|
||||
run: |
|
||||
# Download linuxdeploy and Qt plugin
|
||||
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
|
||||
chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage
|
||||
|
||||
# Build AppDir structure
|
||||
mkdir -p AppDir/usr/bin AppDir/usr/share/icons/hicolor/256x256/apps
|
||||
cp build/Reclass AppDir/usr/bin/
|
||||
cp build/ReclassMcpBridge AppDir/usr/bin/
|
||||
cp -r build/themes AppDir/usr/bin/ 2>/dev/null || true
|
||||
cp -r build/examples AppDir/usr/bin/ 2>/dev/null || true
|
||||
mkdir -p AppDir/usr/bin/Plugins
|
||||
cp build/Plugins/*.so AppDir/usr/bin/Plugins/ 2>/dev/null || true
|
||||
cp src/icons/class.png AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png
|
||||
|
||||
# Create AppImage with Qt libs bundled
|
||||
# install-qt-action adds Qt bin to PATH; find qmake there
|
||||
QMAKE_BIN=$(which qmake 2>/dev/null || which qmake6 2>/dev/null || find "$RUNNER_WORKSPACE" -name qmake -path "*/bin/*" | head -1)
|
||||
echo "Found qmake at: $QMAKE_BIN"
|
||||
export QMAKE="$QMAKE_BIN"
|
||||
QT_ROOT=$(dirname "$(dirname "$QMAKE_BIN")")
|
||||
export LD_LIBRARY_PATH="$QT_ROOT/lib:$LD_LIBRARY_PATH"
|
||||
export EXTRA_QT_PLUGINS="svg;iconengines"
|
||||
./linuxdeploy-x86_64.AppImage --appdir AppDir \
|
||||
--desktop-file deploy/Reclass.desktop \
|
||||
--icon-file AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png \
|
||||
--plugin qt \
|
||||
--output appimage
|
||||
# Rename to final name
|
||||
ls Reclass-*.AppImage
|
||||
mv Reclass-*.AppImage Reclass-linux64-qt6.AppImage
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Reclass-linux64-qt6
|
||||
path: Reclass-linux64-qt6.AppImage
|
||||
|
||||
macos:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-15
|
||||
qt_arch: clang_arm64
|
||||
artifact_name: Reclass-macos-arm64-qt6
|
||||
zip_name: Reclass-macos-arm64-qt6.zip
|
||||
- os: macos-15-intel
|
||||
qt_arch: clang_64
|
||||
artifact_name: Reclass-macos-x86_64-qt6
|
||||
zip_name: Reclass-macos-x86_64-qt6.zip
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
brew update
|
||||
brew install cmake ninja qt
|
||||
|
||||
- name: Configure Qt paths
|
||||
run: |
|
||||
QT_PREFIX="$(brew --prefix qt)"
|
||||
echo "QT_PREFIX=$QT_PREFIX" >> "$GITHUB_ENV"
|
||||
echo "PATH=$QT_PREFIX/bin:$PATH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF -DCMAKE_PREFIX_PATH="$QT_PREFIX"
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Package app zip
|
||||
run: |
|
||||
MACDEPLOYQT_BIN="$QT_PREFIX/bin/macdeployqt"
|
||||
if [ ! -x "$MACDEPLOYQT_BIN" ]; then
|
||||
MACDEPLOYQT_BIN=$(which macdeployqt 2>/dev/null || find "$RUNNER_WORKSPACE" -name macdeployqt -path "*/bin/*" | head -1)
|
||||
fi
|
||||
echo "Found macdeployqt at: $MACDEPLOYQT_BIN"
|
||||
"$MACDEPLOYQT_BIN" build/Reclass.app -always-overwrite
|
||||
codesign --force --deep --sign - build/Reclass.app
|
||||
ditto -c -k --sequesterRsrc --keepParent build/Reclass.app "${{ matrix.zip_name }}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: ${{ matrix.zip_name }}
|
||||
|
||||
release:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: [windows, linux, macos]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Get date tag
|
||||
id: date
|
||||
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: snapshot-${{ steps.date.outputs.tag }}
|
||||
name: Snapshot ${{ steps.date.outputs.tag }}
|
||||
body: |
|
||||
Automated snapshot from main branch.
|
||||
Commit: ${{ github.sha }}
|
||||
prerelease: false
|
||||
files: |
|
||||
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
|
||||
artifacts/Reclass-linux64-qt6/Reclass-linux64-qt6.AppImage
|
||||
artifacts/Reclass-macos-arm64-qt6/Reclass-macos-arm64-qt6.zip
|
||||
artifacts/Reclass-macos-x86_64-qt6/Reclass-macos-x86_64-qt6.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
4
.gitignore
vendored
@@ -11,3 +11,7 @@ build/
|
||||
*.suo
|
||||
.vs/
|
||||
CMakeUserPresets.json
|
||||
plugins/RcNetPluginCompatLayer/bridge/obj
|
||||
plugins/RcNetPluginCompatLayer/bridge/bin
|
||||
.cache
|
||||
*.DS_Store
|
||||
|
||||
6
.gitmodules
vendored
@@ -1,3 +1,9 @@
|
||||
[submodule "third_party/qscintilla"]
|
||||
path = third_party/qscintilla
|
||||
url = https://github.com/brCreate/QScintilla.git
|
||||
[submodule "third_party/raw_pdb"]
|
||||
path = third_party/raw_pdb
|
||||
url = https://github.com/MolecularMatters/raw_pdb.git
|
||||
[submodule "third_party/fadec"]
|
||||
path = third_party/fadec
|
||||
url = https://github.com/aengelke/fadec.git
|
||||
|
||||
607
CMakeLists.txt
@@ -1,18 +1,97 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(ReclassX 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)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets PrintSupport Svg Concurrent)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
|
||||
|
||||
# Find Qt6 or Qt5 (config mode first, then FindQt5.cmake module for auto-download)
|
||||
set(_QT_COMPONENTS Core Widgets PrintSupport Svg Concurrent Network)
|
||||
find_package(QT NAMES Qt6 Qt5 COMPONENTS ${_QT_COMPONENTS} QUIET)
|
||||
if(NOT QT_FOUND)
|
||||
find_package(Qt5 REQUIRED COMPONENTS ${_QT_COMPONENTS})
|
||||
set(QT_VERSION_MAJOR 5)
|
||||
endif()
|
||||
# The NAMES variant only detects the version; load the actual component targets
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ${_QT_COMPONENTS})
|
||||
set(QT Qt${QT_VERSION_MAJOR})
|
||||
message(STATUS "Using ${QT}: ${${QT}_DIR}")
|
||||
|
||||
# ── ABI sanity check: prevent MSVC ↔ MinGW Qt mismatch ──
|
||||
# Building with MSVC against MinGW Qt (or vice versa) compiles fine but
|
||||
# crashes immediately at runtime (ABI mismatch in QString/QSettings internals).
|
||||
if(MSVC AND "${${QT}_DIR}" MATCHES "mingw")
|
||||
message(FATAL_ERROR
|
||||
"Qt installation was built with MinGW but this project is being compiled with MSVC.\n"
|
||||
" Qt found at: ${${QT}_DIR}\n"
|
||||
"This will compile but crash at startup due to ABI mismatch.\n"
|
||||
"Fix: install Qt for MSVC (e.g. msvc2019_64) and set CMAKE_PREFIX_PATH to it:\n"
|
||||
" cmake -DCMAKE_PREFIX_PATH=C:/Qt/6.5.2/msvc2019_64 ..")
|
||||
elseif(MINGW AND "${${QT}_DIR}" MATCHES "msvc")
|
||||
message(FATAL_ERROR
|
||||
"Qt installation was built with MSVC but this project is being compiled with MinGW.\n"
|
||||
" Qt found at: ${${QT}_DIR}\n"
|
||||
"This will compile but crash at startup due to ABI mismatch.\n"
|
||||
"Fix: install Qt for MinGW and set CMAKE_PREFIX_PATH to it:\n"
|
||||
" cmake -DCMAKE_PREFIX_PATH=C:/Qt/6.5.2/mingw_64 ..")
|
||||
endif()
|
||||
|
||||
# ── MSVC compile flags ──
|
||||
if(MSVC)
|
||||
# /utf-8: treat source and execution character sets as UTF-8
|
||||
# /MP: multi-processor compilation
|
||||
add_compile_options(/utf-8 /MP)
|
||||
endif()
|
||||
|
||||
# Qt5 on Windows needs WinExtras for HICON conversion
|
||||
set(_QT_WINEXTRAS "")
|
||||
if(QT_VERSION_MAJOR EQUAL 5 AND WIN32)
|
||||
find_package(Qt5 REQUIRED COMPONENTS WinExtras)
|
||||
set(_QT_WINEXTRAS Qt5::WinExtras)
|
||||
endif()
|
||||
|
||||
find_package(QScintilla REQUIRED)
|
||||
|
||||
add_executable(ReclassX
|
||||
# RawPDB — direct PDB file reader (no DIA SDK / msdia140.dll dependency)
|
||||
file(GLOB RAW_PDB_SRCS third_party/raw_pdb/src/*.cpp)
|
||||
add_library(raw_pdb STATIC ${RAW_PDB_SRCS})
|
||||
target_include_directories(raw_pdb PUBLIC third_party/raw_pdb/src)
|
||||
target_compile_features(raw_pdb PRIVATE cxx_std_11)
|
||||
# PDB_CRT.h forward-declares printf/memcmp/etc with __cdecl which conflicts
|
||||
# with non-MSVC compilers (GCC, Clang, MinGW). Force-include a prefix header
|
||||
# that pulls in the real CRT headers and strips __cdecl.
|
||||
if(NOT MSVC)
|
||||
target_compile_options(raw_pdb PUBLIC
|
||||
-include "${CMAKE_CURRENT_SOURCE_DIR}/cmake/raw_pdb_prefix.h")
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(raw_pdb PRIVATE rpcrt4)
|
||||
endif()
|
||||
|
||||
# Fadec — generate decode tables (.inc files) from instrs.txt at configure time
|
||||
find_package(Python3 3.9 REQUIRED)
|
||||
set(FADEC_DIR "${CMAKE_SOURCE_DIR}/third_party/fadec")
|
||||
if(NOT EXISTS "${FADEC_DIR}/fadec-decode-public.inc")
|
||||
message(STATUS "Generating fadec decode tables...")
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE} "${FADEC_DIR}/parseinstrs.py" decode
|
||||
"${FADEC_DIR}/instrs.txt"
|
||||
"${FADEC_DIR}/fadec-decode-public.inc"
|
||||
"${FADEC_DIR}/fadec-decode-private.inc"
|
||||
--32 --64
|
||||
RESULT_VARIABLE _fadec_result
|
||||
)
|
||||
if(NOT _fadec_result EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to generate fadec decode tables")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_executable(Reclass
|
||||
src/main.cpp
|
||||
src/editor.h
|
||||
src/editor.cpp
|
||||
@@ -28,39 +107,176 @@ add_executable(ReclassX
|
||||
src/resources.qrc
|
||||
src/core.h
|
||||
src/workspace_model.h
|
||||
src/providers/buffer_provider.h src/providers/null_provider.h src/providers/process_provider.h src/providers/provider.h src/providers/snapshot_provider.h
|
||||
src/providers/buffer_provider.h src/providers/null_provider.h src/providers/provider.h src/providers/snapshot_provider.h
|
||||
src/providerregistry.cpp
|
||||
src/providerregistry.h
|
||||
src/pluginmanager.cpp
|
||||
src/pluginmanager.h
|
||||
src/typeselectorpopup.h
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.h
|
||||
src/themes/theme.cpp
|
||||
src/themes/thememanager.h
|
||||
src/themes/thememanager.cpp
|
||||
src/themes/themeeditor.h
|
||||
src/themes/themeeditor.cpp
|
||||
src/imports/import_reclass_xml.h
|
||||
src/imports/import_reclass_xml.cpp
|
||||
src/imports/import_source.h
|
||||
src/imports/import_source.cpp
|
||||
src/imports/export_reclass_xml.h
|
||||
src/imports/export_reclass_xml.cpp
|
||||
src/imports/import_pdb.h
|
||||
src/imports/import_pdb.cpp
|
||||
src/imports/import_pdb_dialog.h
|
||||
src/imports/import_pdb_dialog.cpp
|
||||
src/scanner.h
|
||||
src/scanner.cpp
|
||||
src/scannerpanel.h
|
||||
src/scannerpanel.cpp
|
||||
src/mainwindow.h
|
||||
src/startpage.h
|
||||
src/dock_tab_buttons.h
|
||||
src/optionsdialog.h
|
||||
src/optionsdialog.cpp
|
||||
src/titlebar.h
|
||||
src/titlebar.cpp
|
||||
src/macos_titlebar.h
|
||||
$<$<PLATFORM_ID:Darwin>:src/macos_titlebar.mm>
|
||||
src/mcp/mcp_bridge.h
|
||||
src/mcp/mcp_bridge.cpp
|
||||
src/addressparser.h
|
||||
src/addressparser.cpp
|
||||
src/symbolstore.h
|
||||
src/symbolstore.cpp
|
||||
src/symbol_downloader.h
|
||||
src/symbol_downloader.cpp
|
||||
src/imports/pe_debug_info.h
|
||||
src/imports/pe_debug_info.cpp
|
||||
src/disasm.h
|
||||
src/disasm.cpp
|
||||
third_party/fadec/decode.c
|
||||
third_party/fadec/format.c
|
||||
$<$<PLATFORM_ID:Windows>:src/app.rc>
|
||||
)
|
||||
|
||||
target_include_directories(ReclassX PRIVATE src)
|
||||
if(APPLE)
|
||||
set_target_properties(Reclass PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_ICON_FILE "class.icns"
|
||||
)
|
||||
target_sources(Reclass PRIVATE src/icons/class.icns)
|
||||
set_source_files_properties(src/icons/class.icns
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
|
||||
endif()
|
||||
|
||||
target_link_libraries(ReclassX PRIVATE
|
||||
Qt6::Widgets
|
||||
Qt6::PrintSupport
|
||||
Qt6::Svg
|
||||
Qt6::Concurrent
|
||||
target_include_directories(Reclass PRIVATE src third_party/fadec)
|
||||
|
||||
target_link_libraries(Reclass PRIVATE
|
||||
${QT}::Widgets
|
||||
${QT}::PrintSupport
|
||||
${QT}::Svg
|
||||
${QT}::Concurrent
|
||||
${QT}::Network
|
||||
QScintilla::QScintilla
|
||||
dbghelp
|
||||
psapi
|
||||
${_QT_WINEXTRAS}
|
||||
)
|
||||
if(WIN32)
|
||||
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi raw_pdb)
|
||||
|
||||
add_custom_target(screenshot ALL
|
||||
COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
|
||||
DEPENDS ReclassX
|
||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||
COMMENT "Capturing UI screenshot with class open..."
|
||||
)
|
||||
# Copy Debugging Tools dbghelp.dll next to Reclass.exe so the Windows
|
||||
# loader picks it up (app dir > System32). The system dbghelp.dll
|
||||
# lacks StackWalk2 which the tools dbgeng.dll needs for remote debug.
|
||||
set(_DBG_TOOLS_DIRS
|
||||
"C:/Program Files (x86)/Windows Kits/10/Debuggers/x64"
|
||||
"C:/Program Files/Windows Kits/10/Debuggers/x64")
|
||||
foreach(_dir ${_DBG_TOOLS_DIRS})
|
||||
if(EXISTS "${_dir}/dbghelp.dll")
|
||||
foreach(_dll dbghelp.dll dbgcore.dll symsrv.dll)
|
||||
if(EXISTS "${_dir}/${_dll}")
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_dir}/${_dll}" $<TARGET_FILE_DIR:Reclass>
|
||||
COMMENT "Copying ${_dll} from Debugging Tools")
|
||||
endif()
|
||||
endforeach()
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
|
||||
target_link_libraries(ReclassMcpBridge PRIVATE ${QT}::Core ${QT}::Network)
|
||||
if(APPLE)
|
||||
add_custom_command(TARGET ReclassMcpBridge POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
$<TARGET_FILE:ReclassMcpBridge>
|
||||
$<TARGET_FILE_DIR:Reclass>/ReclassMcpBridge
|
||||
COMMENT "Bundling ReclassMcpBridge into Reclass.app"
|
||||
)
|
||||
endif()
|
||||
|
||||
# Copy built-in theme JSON files next to the executable.
|
||||
# For single-config generators (Ninja/Make) the exe is in ${CMAKE_BINARY_DIR},
|
||||
# for multi-config generators (MSVC/Xcode) it's in ${CMAKE_BINARY_DIR}/<config>.
|
||||
# Using a post-build copy with $<TARGET_FILE_DIR:Reclass> handles both.
|
||||
file(GLOB _theme_files "${CMAKE_SOURCE_DIR}/src/themes/defaults/*.json")
|
||||
|
||||
# Single-config: configure_file for IDE convenience (available before first build)
|
||||
if(NOT CMAKE_CONFIGURATION_TYPES)
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/themes")
|
||||
foreach(_tf ${_theme_files})
|
||||
get_filename_component(_name ${_tf} NAME)
|
||||
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# Post-build: always copy to the actual exe directory (works for all generators)
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/themes"
|
||||
COMMENT "Creating themes directory next to executable")
|
||||
foreach(_tf ${_theme_files})
|
||||
get_filename_component(_name ${_tf} NAME)
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_tf}" "$<TARGET_FILE_DIR:Reclass>/themes/${_name}")
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
target_sources(Reclass PRIVATE ${_theme_files})
|
||||
set_source_files_properties(${_theme_files}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/themes")
|
||||
endif()
|
||||
|
||||
# Copy example .rcx files next to the executable (same logic as themes)
|
||||
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
|
||||
|
||||
if(NOT CMAKE_CONFIGURATION_TYPES)
|
||||
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
|
||||
foreach(_ef ${_example_files})
|
||||
get_filename_component(_name ${_ef} NAME)
|
||||
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/examples"
|
||||
COMMENT "Creating examples directory next to executable")
|
||||
foreach(_ef ${_example_files})
|
||||
get_filename_component(_name ${_ef} NAME)
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${_ef}" "$<TARGET_FILE_DIR:Reclass>/examples/${_name}")
|
||||
endforeach()
|
||||
|
||||
if(APPLE)
|
||||
target_sources(Reclass PRIVATE ${_example_files})
|
||||
set_source_files_properties(${_example_files}
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/examples")
|
||||
endif()
|
||||
|
||||
include(deploy)
|
||||
|
||||
add_custom_target(copy_demo ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_BINARY_DIR}/demo.rcx
|
||||
${CMAKE_SOURCE_DIR}/src/examples/demo.rcx
|
||||
DEPENDS screenshot
|
||||
COMMENT "Copying demo.rcx to src/examples..."
|
||||
)
|
||||
|
||||
set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
|
||||
file(WRITE ${_combine_script} "
|
||||
@@ -77,7 +293,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}\")
|
||||
@@ -85,95 +301,310 @@ message(STATUS \"Combined sources -> \${_out}\")
|
||||
|
||||
add_custom_target(combined ALL
|
||||
COMMAND ${CMAKE_COMMAND} -P ${_combine_script}
|
||||
DEPENDS ReclassX
|
||||
DEPENDS Reclass
|
||||
COMMENT "Combining all source files into h_cpp_combined.txt"
|
||||
)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Test)
|
||||
find_package(${QT} REQUIRED COMPONENTS Test)
|
||||
enable_testing()
|
||||
|
||||
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp)
|
||||
# Disasm/Fadec sources needed by any test that links editor.cpp
|
||||
set(DISASM_SRCS src/disasm.cpp third_party/fadec/decode.c third_party/fadec/format.c)
|
||||
|
||||
# ── Headless tests (Qt::Core only — safe for CI without a display) ──
|
||||
|
||||
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_core PRIVATE src)
|
||||
target_link_libraries(test_core PRIVATE Qt6::Core Qt6::Test)
|
||||
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_core COMMAND test_core)
|
||||
|
||||
add_executable(test_format tests/test_format.cpp src/format.cpp)
|
||||
add_executable(test_typeinfer tests/test_typeinfer.cpp)
|
||||
target_include_directories(test_typeinfer PRIVATE src)
|
||||
target_link_libraries(test_typeinfer PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_typeinfer COMMAND test_typeinfer)
|
||||
|
||||
add_executable(test_format tests/test_format.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_format PRIVATE src)
|
||||
target_link_libraries(test_format PRIVATE Qt6::Core Qt6::Test)
|
||||
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_format COMMAND test_format)
|
||||
|
||||
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp)
|
||||
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_compose PRIVATE src)
|
||||
target_link_libraries(test_compose PRIVATE Qt6::Core Qt6::Test)
|
||||
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)
|
||||
target_include_directories(test_editor PRIVATE src)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::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 Qt6::Core Qt6::Test)
|
||||
target_link_libraries(test_provider PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_provider COMMAND test_provider)
|
||||
|
||||
add_executable(test_command_row tests/test_command_row.cpp)
|
||||
target_include_directories(test_command_row PRIVATE src)
|
||||
target_link_libraries(test_command_row PRIVATE Qt6::Core Qt6::Test)
|
||||
target_link_libraries(test_command_row PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_command_row COMMAND test_command_row)
|
||||
|
||||
add_executable(test_provider_getSymbol tests/test_provider_getSymbol.cpp)
|
||||
target_include_directories(test_provider_getSymbol PRIVATE src)
|
||||
target_link_libraries(test_provider_getSymbol PRIVATE Qt6::Core Qt6::Test)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_provider_getSymbol PRIVATE psapi)
|
||||
endif()
|
||||
add_test(NAME test_provider_getSymbol COMMAND test_provider_getSymbol)
|
||||
|
||||
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)
|
||||
target_include_directories(test_controller PRIVATE src)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
|
||||
add_executable(test_validation tests/test_validation.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp)
|
||||
target_include_directories(test_validation PRIVATE src)
|
||||
target_link_libraries(test_validation PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
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)
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_generator PRIVATE src)
|
||||
target_link_libraries(test_generator PRIVATE Qt6::Core Qt6::Test)
|
||||
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)
|
||||
target_include_directories(test_context_menu PRIVATE src)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
add_executable(test_import_xml tests/test_import_xml.cpp
|
||||
src/imports/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_import_xml PRIVATE src)
|
||||
target_link_libraries(test_import_xml PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_import_xml COMMAND test_import_xml)
|
||||
|
||||
add_executable(test_new_features tests/test_new_features.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp)
|
||||
target_include_directories(test_new_features PRIVATE src)
|
||||
target_link_libraries(test_new_features PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
add_test(NAME test_new_features COMMAND test_new_features)
|
||||
add_executable(test_import_source tests/test_import_source.cpp
|
||||
src/imports/import_source.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_import_source PRIVATE src)
|
||||
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_import_source COMMAND test_import_source)
|
||||
|
||||
add_executable(test_export_xml tests/test_export_xml.cpp
|
||||
src/imports/export_reclass_xml.cpp src/imports/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_export_xml PRIVATE src)
|
||||
target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_export_xml COMMAND test_export_xml)
|
||||
|
||||
add_executable(test_disasm tests/test_disasm.cpp
|
||||
src/disasm.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
|
||||
third_party/fadec/decode.c third_party/fadec/format.c)
|
||||
target_include_directories(test_disasm PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_disasm PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_disasm COMMAND test_disasm)
|
||||
|
||||
add_executable(test_addressparser tests/test_addressparser.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_addressparser PRIVATE src)
|
||||
target_link_libraries(test_addressparser PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_addressparser COMMAND test_addressparser)
|
||||
|
||||
add_executable(test_static_fields tests/test_static_fields.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_static_fields PRIVATE src)
|
||||
target_link_libraries(test_static_fields PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_static_fields COMMAND test_static_fields)
|
||||
|
||||
add_executable(test_scanner tests/test_scanner.cpp src/scanner.cpp)
|
||||
target_include_directories(test_scanner PRIVATE src)
|
||||
target_link_libraries(test_scanner PRIVATE ${QT}::Core ${QT}::Concurrent ${QT}::Test)
|
||||
add_test(NAME test_scanner COMMAND test_scanner)
|
||||
|
||||
add_executable(test_32bit_support tests/test_32bit_support.cpp
|
||||
src/generator.cpp src/imports/import_source.cpp src/imports/import_reclass_xml.cpp
|
||||
src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_32bit_support PRIVATE src
|
||||
${CMAKE_SOURCE_DIR}/plugins/RemoteProcessMemory)
|
||||
target_link_libraries(test_32bit_support PRIVATE ${QT}::Core ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_32bit_support COMMAND test_32bit_support)
|
||||
set_tests_properties(test_32bit_support PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_import_pdb tests/test_import_pdb.cpp
|
||||
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_import_pdb PRIVATE src)
|
||||
target_link_libraries(test_import_pdb PRIVATE
|
||||
${QT}::Core ${QT}::Test raw_pdb)
|
||||
add_test(NAME test_import_pdb COMMAND test_import_pdb)
|
||||
|
||||
add_executable(bench_import_pdb tests/bench_import_pdb.cpp
|
||||
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
|
||||
target_include_directories(bench_import_pdb PRIVATE src)
|
||||
target_link_libraries(bench_import_pdb PRIVATE
|
||||
${QT}::Core ${QT}::Test raw_pdb)
|
||||
add_test(NAME bench_import_pdb COMMAND bench_import_pdb)
|
||||
endif()
|
||||
|
||||
# ── UI tests (require Qt::Widgets / QScintilla / display — skip on headless CI) ──
|
||||
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
|
||||
if(BUILD_UI_TESTS)
|
||||
|
||||
add_executable(test_controller tests/test_controller.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_controller PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_controller PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
|
||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_context_menu PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_context_menu COMMAND test_context_menu)
|
||||
|
||||
add_executable(test_source_management tests/test_source_management.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_source_management PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_management PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_management PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_management COMMAND test_source_management)
|
||||
|
||||
add_executable(test_editor tests/test_editor.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
|
||||
src/providerregistry.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_editor PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_editor PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_editor COMMAND test_editor)
|
||||
|
||||
add_executable(test_rendered_view tests/test_rendered_view.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_rendered_view PRIVATE src)
|
||||
target_link_libraries(test_rendered_view PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||
|
||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_selector PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_selector COMMAND test_type_selector)
|
||||
|
||||
add_executable(test_type_visibility tests/test_type_visibility.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(test_type_visibility PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_type_visibility PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_type_visibility COMMAND test_type_visibility)
|
||||
|
||||
add_executable(test_options_dialog tests/test_options_dialog.cpp
|
||||
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_options_dialog PRIVATE src)
|
||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||
|
||||
add_executable(test_source_provider tests/test_source_provider.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
|
||||
src/resources.qrc)
|
||||
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||
|
||||
add_executable(test_scanner_ui tests/test_scanner_ui.cpp
|
||||
src/scanner.cpp src/scannerpanel.cpp src/addressparser.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
||||
target_include_directories(test_scanner_ui PRIVATE src)
|
||||
target_link_libraries(test_scanner_ui PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
|
||||
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
|
||||
|
||||
add_executable(test_mcp tests/test_mcp.cpp)
|
||||
target_include_directories(test_mcp PRIVATE src)
|
||||
target_link_libraries(test_mcp PRIVATE ${QT}::Core ${QT}::Network ${QT}::Test)
|
||||
add_test(NAME test_mcp COMMAND test_mcp)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
|
||||
src/scanner.cpp)
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
|
||||
add_executable(test_kernel_provider tests/test_kernel_provider.cpp
|
||||
plugins/KernelMemory/KernelMemoryPlugin.cpp
|
||||
src/processpicker.cpp src/processpicker.ui
|
||||
src/scanner.cpp)
|
||||
target_include_directories(test_kernel_provider PRIVATE
|
||||
src plugins/KernelMemory)
|
||||
target_link_libraries(test_kernel_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test
|
||||
psapi shell32 advapi32 ${_QT_WINEXTRAS})
|
||||
add_test(NAME test_kernel_provider COMMAND test_kernel_provider)
|
||||
endif()
|
||||
|
||||
add_executable(bench_large_class tests/bench_large_class.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
|
||||
src/providerregistry.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||
target_include_directories(bench_large_class PRIVATE src third_party/fadec)
|
||||
target_link_libraries(bench_large_class PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(bench_large_class PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME bench_large_class COMMAND bench_large_class)
|
||||
|
||||
add_executable(bench_project tests/bench_project.cpp)
|
||||
target_include_directories(bench_project PRIVATE src)
|
||||
target_link_libraries(bench_project PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
if(WIN32)
|
||||
target_link_libraries(bench_project PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME bench_project COMMAND bench_project)
|
||||
|
||||
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
||||
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
||||
if(TARGET ${QT}::windeployqt)
|
||||
add_custom_target(deploy_tests ALL
|
||||
COMMAND $<TARGET_FILE:${QT}::windeployqt>
|
||||
--no-compiler-runtime --no-translations
|
||||
--no-opengl-sw --no-system-d3d-compiler
|
||||
$<TARGET_FILE:test_controller>
|
||||
DEPENDS test_controller
|
||||
COMMENT "Deploying Qt runtime DLLs for tests..."
|
||||
)
|
||||
endif()
|
||||
|
||||
endif() # BUILD_UI_TESTS
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
if(NOT APPLE)
|
||||
add_subdirectory(plugins/RemoteProcessMemory)
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_subdirectory(plugins/KernelMemory)
|
||||
add_subdirectory(plugins/WinDbgMemory)
|
||||
add_subdirectory(plugins/RcNetPluginCompatLayer)
|
||||
endif()
|
||||
|
||||
195
README.md
@@ -1,16 +1,197 @@
|
||||
# ReclassX
|
||||
<div align="center">
|
||||
|
||||
An improvement over other reclass like editors.
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="docs/RECLASS_LIGHTMODE.svg" height="170">
|
||||
<img src="docs/RECLASS_DARKMODE.svg" alt="Reclass" height="170" />
|
||||
</picture>
|
||||
|
||||
https://github.com/IChooseYou/ReclassX/raw/main/video.mp4
|
||||
**A structured binary editor for reverse engineering — inspect raw bytes as typed structs, arrays, and pointers.<p>Built from scratch as a modern replacement for ReClass.NET and ReClassEx**
|
||||
|
||||

|
||||
[Download](https://github.com/IChooseYou/Reclass/releases) · [Build Instructions](#build) · [MCP Integration](#mcp-integration) · [Alternatives](#alternatives)
|
||||
|
||||
[](https://github.com/IChooseYou/Reclass/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/IChooseYou/Reclass/releases)
|
||||
[]()
|
||||
|
||||
</div>
|
||||
|
||||
Reclass helps you inspect raw bytes and interpret them as types (structs, arrays, primitives, pointers, padding) instead of just hex. It is a debugging tool for figuring out unknown data structures — either at runtime from a live process, or from a static source like a binary file or crash dump.
|
||||
|
||||
Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor surface is rendered as formatted plain text with inline editing, fold markers, and hex/ASCII previews.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
### Editor
|
||||
|
||||
- **Structured binary view** — render raw bytes as typed fields with columnar alignment
|
||||
- **Inline editing** — click to edit type names, field names, values, base addresses, array metadata, pointer targets, enum members, bitfield members, static expressions, and comments — all with real-time validation
|
||||
- **Tab-cycling** — tab through editable fields within a line
|
||||
- **Type autocomplete** — cached popup type picker with search/filter for struct targets
|
||||
- **Multi-select** — Ctrl+click individual nodes or Shift+click for range selection
|
||||
- **Split views** — multiple synchronized editor panes over the same document
|
||||
- **Find bar** — Ctrl+F in-editor search with indicator highlighting
|
||||
- **Fold/collapse** — expand and collapse structs, arrays, and pointer expansions with embedded fold indicators
|
||||
- **Hex + ASCII columns** — raw byte previews alongside the structured view with per-byte change highlighting
|
||||
|
||||
### Live Memory Analysis
|
||||
|
||||
- **Auto-refresh** — configurable interval (default 660ms) with async page-based reads for non-blocking UI
|
||||
- **Value history & heatmap** — per-node ring buffer (10 samples with timestamps), color-coded heat indicators (static/cold/warm/hot) based on change frequency
|
||||
- **Changed-byte highlighting** — per-byte change indicators within hex preview lines
|
||||
- **Memory write-back** — edit values inline, writes propagate through the provider to live process memory
|
||||
- **Pointer chasing** — automatic reads of dereferenced memory regions across pointer chains
|
||||
- **Address parser** — formula expressions like `<module.exe>+0x1A0`, pointer dereference chains, symbol resolution
|
||||
|
||||
### Undo / Redo
|
||||
|
||||
Full command stack with 15 undoable operations: ChangeKind, Rename, Collapse, Insert, Remove, ChangeBase, WriteBytes, ChangeArrayMeta, ChangePointerRef, ChangeStructTypeName, ChangeClassKeyword, ChangeOffset, ChangeEnumMembers, ChangeOffsetExpr, ToggleStatic. Batch macro support for multi-node operations.
|
||||
|
||||
### Import / Export
|
||||
|
||||
| Format | Import | Export |
|
||||
|--------|:------:|:------:|
|
||||
| **Native JSON (.rcx)** | Full tree + metadata | Full tree + metadata |
|
||||
| **C/C++ source** | Struct/class/union/enum parsing with offset comments | Header generation with optional static asserts |
|
||||
| **ReClass XML** | Full compatibility with ReClass Classic | Full compatibility |
|
||||
| **PDB symbols (Windows)** | UDT enumeration with selective recursive import via raw_pdb — no DIA SDK dependency | |
|
||||
|
||||
### Workspace & Navigation
|
||||
|
||||
- **Multi-document tabs** — MDI interface, one document per tab
|
||||
- **Workspace dock** — project explorer tree with struct/enum/union icons, sorted by field count, quick navigation to members
|
||||
- **Scanner dock** — integrated memory search panel
|
||||
- **Dual view mode** — switch between ReClass tree view and rendered C/C++ output per tab
|
||||
- **View root** — focus on a specific struct, hiding all others
|
||||
- **Scroll to node** — programmatic navigation to any node by ID
|
||||
|
||||
## Data Sources
|
||||
|
||||
- **File** — open any binary file and inspect its contents as structured data
|
||||
- **Process** — attach to a live process and read its memory in real time (Windows/Linux)
|
||||
- **Kernel driver** — Windows kernel driver (IOCTL) for process memory, physical memory, page table walking, and CR3/VTOP translation
|
||||
- **Remote Process** — read another process's memory over TCP with cross-architecture 32/64-bit support
|
||||
- **WinDbg** — connect to live WinDbg debugging sessions or load crash dumps
|
||||
- **Saved sources** — quick-switch between recently used data sources per tab
|
||||
|
||||
## Plugin System
|
||||
|
||||
DLL plugins loaded from a `Plugins` folder, auto or manual.
|
||||
|
||||
**Bundled plugins:**
|
||||
|
||||
| Plugin | Description |
|
||||
|--------|-------------|
|
||||
| **Process memory** | Attach to local processes on Windows and Linux — PID-based, with symbol resolution and module/region enumeration |
|
||||
| **Kernel memory** | Windows kernel driver (IOCTL) for reading/writing process and physical memory, CR3 queries, virtual-to-physical translation, and full 4-level page table walking — supports 4KB, 2MB, and 1GB pages |
|
||||
| **WinDbg** | Access data from live WinDbg debugging sessions |
|
||||
| **Remote process memory** | TCP RPC-based remote process access with cross-architecture support |
|
||||
| **ReClass.NET compatibility** | Load existing ReClass.NET native DLL plugins directly; optional .NET CLR hosting for managed plugins |
|
||||
|
||||
## MCP Integration
|
||||
|
||||
Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `ReclassMcpBridge` — the first reverse engineering tool with native AI/LLM integration. The server uses JSON-RPC 2.0 over named pipes and can be toggled from the Tools menu or auto-started on launch.
|
||||
|
||||
**Available tools:**
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `projectState` | Read current tree structure, base address, tab state |
|
||||
| `treeApply` | Apply structural command deltas to the node tree |
|
||||
| `sourceSwitch` | Switch the active data source |
|
||||
| `hexRead` | Read bytes at an address |
|
||||
| `hexWrite` | Write bytes at an address |
|
||||
| `statusSet` | Update the status bar text |
|
||||
| `uiAction` | Trigger menu actions programmatically |
|
||||
| `treeSearch` | Search nodes by name or type |
|
||||
| `nodeHistory` | Query value change history for a node |
|
||||
|
||||
**Notifications:** `notifyTreeChanged`, `notifyDataChanged`
|
||||
|
||||
A standalone stdio-to-pipe bridge binary is built alongside the main application. To connect, add this to your MCP client config (e.g. `.mcp.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ReclassMcpBridge": {
|
||||
"command": "path/to/build/ReclassMcpBridge",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Requires Qt 6, QScintilla, and MinGW on Windows.
|
||||
### Prerequisites
|
||||
|
||||
- **Qt 6** (or Qt 5) with MinGW — [Qt Online Installer](https://doc.qt.io/qt-6/qt-online-installation.html) (select MinGW kit + CMake/Ninja from the Tools section)
|
||||
- **CMake 3.20+** — [cmake.org](https://cmake.org/download/) (bundled with Qt)
|
||||
- **Ninja** — bundled with the Qt installer
|
||||
|
||||
### Quick Build
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
|
||||
cd Reclass
|
||||
.\scripts\build_qscintilla.ps1
|
||||
.\scripts\build.ps1
|
||||
```
|
||||
cmake -B build -G Ninja
|
||||
cmake --build build
|
||||
|
||||
The build script auto-detects your Qt install location.
|
||||
|
||||
### macOS Build
|
||||
|
||||
```bash
|
||||
./scripts/build_macos.sh --qt-dir /opt/homebrew/opt/qt --build-type Release --package
|
||||
```
|
||||
|
||||
If you installed Qt via Homebrew, `--qt-dir /opt/homebrew/opt/qt` is typical on Apple Silicon. You can also set `QTDIR` or `Qt6_DIR` instead of passing `--qt-dir`.
|
||||
|
||||
Note: macOS Gatekeeper may block unsigned apps. If the app won't open, go to **System Settings > Privacy & Security** and click **Open Anyway**.
|
||||
|
||||
### Manual Build (MinGW)
|
||||
|
||||
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
|
||||
2. Build QScintilla: `qmake` + `mingw32-make` in `third_party/qscintilla/src`
|
||||
3. Configure and build:
|
||||
```bash
|
||||
cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/mingw_64
|
||||
cmake --build build
|
||||
```
|
||||
4. Optionally run `windeployqt` on the output executable
|
||||
|
||||
### Visual Studio 2022+
|
||||
|
||||
The `msvc/` folder contains a ready-made solution (`Reclass.slnx`) with projects for the main application, all plugins, and third-party libraries. Requires the [Qt Visual Studio Tools](https://marketplace.visualstudio.com/items?itemName=TheQtCompany.QtVisualStudioTools2022) extension with a Qt 6 MSVC kit configured.
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
30 tests covering composition, serialization, undo/redo, import/export, provider switching, type visibility, validation, scanning, and rendering.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- [ReClass.NET](https://github.com/ReClassNET/ReClass.NET)
|
||||
- [ReClassEx](https://github.com/ajkhoury/ReClassEx)
|
||||
|
||||
<div align="center">
|
||||
<sub>MIT License</sub>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
set(_QSCI_ROOT "${CMAKE_SOURCE_DIR}/third_party/qscintilla")
|
||||
|
||||
# Try to find a pre-built library first
|
||||
find_path(QScintilla_INCLUDE_DIR
|
||||
NAMES Qsci/qsciscintilla.h
|
||||
PATHS "${_QSCI_ROOT}/src" "${_QSCI_ROOT}/include"
|
||||
@@ -7,7 +8,10 @@ find_path(QScintilla_INCLUDE_DIR
|
||||
)
|
||||
|
||||
find_library(QScintilla_LIBRARY
|
||||
NAMES qscintilla2_qt6 libqscintilla2_qt6
|
||||
NAMES
|
||||
qscintilla2_qt${QT_VERSION_MAJOR} libqscintilla2_qt${QT_VERSION_MAJOR}
|
||||
qscintilla2_qt6 libqscintilla2_qt6
|
||||
qscintilla2_qt5 libqscintilla2_qt5
|
||||
PATHS
|
||||
"${_QSCI_ROOT}/src/release"
|
||||
"${_QSCI_ROOT}/src"
|
||||
@@ -15,13 +19,11 @@ find_library(QScintilla_LIBRARY
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(QScintilla DEFAULT_MSG
|
||||
QScintilla_LIBRARY QScintilla_INCLUDE_DIR)
|
||||
|
||||
if(QScintilla_FOUND)
|
||||
set(QScintilla_INCLUDE_DIRS ${QScintilla_INCLUDE_DIR})
|
||||
set(QScintilla_LIBRARIES ${QScintilla_LIBRARY})
|
||||
if(QScintilla_LIBRARY AND QScintilla_INCLUDE_DIR)
|
||||
# Use pre-built library
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(QScintilla DEFAULT_MSG
|
||||
QScintilla_LIBRARY QScintilla_INCLUDE_DIR)
|
||||
if(NOT TARGET QScintilla::QScintilla)
|
||||
add_library(QScintilla::QScintilla STATIC IMPORTED)
|
||||
set_target_properties(QScintilla::QScintilla PROPERTIES
|
||||
@@ -29,4 +31,118 @@ if(QScintilla_FOUND)
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${QScintilla_INCLUDE_DIR}"
|
||||
)
|
||||
endif()
|
||||
elseif(EXISTS "${_QSCI_ROOT}/src/qsciscintilla.cpp")
|
||||
# Build from source
|
||||
message(STATUS "Building QScintilla from source")
|
||||
|
||||
file(GLOB _QSCI_LEXER_SOURCES "${_QSCI_ROOT}/scintilla/lexers/*.cpp")
|
||||
file(GLOB _QSCI_LEXLIB_SOURCES "${_QSCI_ROOT}/scintilla/lexlib/*.cpp")
|
||||
file(GLOB _QSCI_SCI_SOURCES "${_QSCI_ROOT}/scintilla/src/*.cpp")
|
||||
file(GLOB _QSCI_HEADERS "${_QSCI_ROOT}/src/Qsci/*.h")
|
||||
|
||||
set(_QSCI_QT_SOURCES
|
||||
"${_QSCI_ROOT}/src/qsciscintilla.cpp"
|
||||
"${_QSCI_ROOT}/src/qsciscintillabase.cpp"
|
||||
"${_QSCI_ROOT}/src/qsciabstractapis.cpp"
|
||||
"${_QSCI_ROOT}/src/qsciapis.cpp"
|
||||
"${_QSCI_ROOT}/src/qscicommand.cpp"
|
||||
"${_QSCI_ROOT}/src/qscicommandset.cpp"
|
||||
"${_QSCI_ROOT}/src/qscidocument.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexer.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerasm.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexeravs.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerbash.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerbatch.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexercmake.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexercoffeescript.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexercpp.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexercsharp.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexercss.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexercustom.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerd.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerdiff.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexeredifact.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerfortran.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerfortran77.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerhex.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerhtml.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexeridl.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerintelhex.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerjava.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerjavascript.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerjson.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerlua.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexermakefile.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexermarkdown.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexermasm.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexermatlab.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexernasm.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexeroctave.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerpascal.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerperl.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerpostscript.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerpo.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerpov.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerproperties.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerpython.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerruby.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerspice.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexersql.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexersrec.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexertcl.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexertekhex.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexertex.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerverilog.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexervhdl.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexerxml.cpp"
|
||||
"${_QSCI_ROOT}/src/qscilexeryaml.cpp"
|
||||
"${_QSCI_ROOT}/src/qscimacro.cpp"
|
||||
"${_QSCI_ROOT}/src/qsciprinter.cpp"
|
||||
"${_QSCI_ROOT}/src/qscistyle.cpp"
|
||||
"${_QSCI_ROOT}/src/qscistyledtext.cpp"
|
||||
"${_QSCI_ROOT}/src/InputMethod.cpp"
|
||||
"${_QSCI_ROOT}/src/ListBoxQt.cpp"
|
||||
"${_QSCI_ROOT}/src/PlatQt.cpp"
|
||||
"${_QSCI_ROOT}/src/SciAccessibility.cpp"
|
||||
"${_QSCI_ROOT}/src/SciClasses.cpp"
|
||||
"${_QSCI_ROOT}/src/ScintillaQt.cpp"
|
||||
)
|
||||
|
||||
add_library(qscintilla2 STATIC
|
||||
${_QSCI_QT_SOURCES}
|
||||
${_QSCI_HEADERS}
|
||||
${_QSCI_LEXER_SOURCES}
|
||||
${_QSCI_LEXLIB_SOURCES}
|
||||
${_QSCI_SCI_SOURCES}
|
||||
)
|
||||
|
||||
target_include_directories(qscintilla2 PUBLIC
|
||||
"${_QSCI_ROOT}/src"
|
||||
)
|
||||
target_include_directories(qscintilla2 PRIVATE
|
||||
"${_QSCI_ROOT}/scintilla/include"
|
||||
"${_QSCI_ROOT}/scintilla/lexlib"
|
||||
"${_QSCI_ROOT}/scintilla/src"
|
||||
)
|
||||
|
||||
target_compile_definitions(qscintilla2 PRIVATE
|
||||
SCINTILLA_QT
|
||||
SCI_LEXER
|
||||
INCLUDE_DEPRECATED_FEATURES
|
||||
)
|
||||
|
||||
target_link_libraries(qscintilla2 PUBLIC
|
||||
${QT}::Widgets
|
||||
${QT}::PrintSupport
|
||||
)
|
||||
|
||||
set_target_properties(qscintilla2 PROPERTIES AUTOMOC ON)
|
||||
|
||||
add_library(QScintilla::QScintilla ALIAS qscintilla2)
|
||||
set(QScintilla_FOUND TRUE)
|
||||
else()
|
||||
set(QScintilla_FOUND FALSE)
|
||||
if(QScintilla_FIND_REQUIRED)
|
||||
message(FATAL_ERROR "Could NOT find QScintilla (missing source and pre-built library)")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
36
cmake/FindQt5.cmake
Normal file
@@ -0,0 +1,36 @@
|
||||
# Documentation: https://cmake.org/cmake/help/latest/manual/cmake-developer.7.html#find-modules
|
||||
|
||||
# Always try config mode for the requested components (handles repeated calls)
|
||||
find_package(Qt5 COMPONENTS ${Qt5_FIND_COMPONENTS} QUIET CONFIG)
|
||||
|
||||
if(Qt5_FOUND)
|
||||
if(NOT Qt5_FIND_QUIETLY)
|
||||
message(STATUS "Qt5 found: ${Qt5_DIR}")
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(Qt5_FIND_REQUIRED AND WIN32)
|
||||
message(STATUS "Downloading Qt5...")
|
||||
# Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP
|
||||
if(POLICY CMP0135)
|
||||
cmake_policy(SET CMP0135 NEW)
|
||||
endif()
|
||||
include(FetchContent)
|
||||
set(FETCHCONTENT_QUIET OFF)
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
FetchContent_Declare(Qt5
|
||||
URL "https://github.com/x64dbg/deps/releases/download/2025.07.02/qt5.12.12-msvc2017_64.7z"
|
||||
URL_HASH SHA256=770490bf09514982c8192ebde9a1fac8821108ba42b021f167bac54e85ada48a
|
||||
)
|
||||
else()
|
||||
FetchContent_Declare(Qt5
|
||||
URL "https://github.com/x64dbg/deps/releases/download/2025.07.02/qt5.12.12-msvc2017.7z"
|
||||
URL_HASH SHA256=3ff2a58e5ed772be475643cd7bb2df3e5499d7169d794ddf1ed5df5c5e862cb6
|
||||
)
|
||||
endif()
|
||||
FetchContent_MakeAvailable(Qt5)
|
||||
unset(FETCHCONTENT_QUIET)
|
||||
set(Qt5_ROOT ${qt5_SOURCE_DIR})
|
||||
find_package(Qt5 COMPONENTS ${Qt5_FIND_COMPONENTS} CONFIG REQUIRED)
|
||||
endif()
|
||||
91
cmake/deploy.cmake
Normal file
@@ -0,0 +1,91 @@
|
||||
# cmake/deploy.cmake - Dual-mode script for deploying Qt runtime DLLs
|
||||
#
|
||||
# Script mode: cmake -P deploy.cmake <target_exe> <windeployqt>
|
||||
# Include mode: include(deploy) from CMakeLists.txt (creates "deploy" target + post-build)
|
||||
|
||||
if(CMAKE_SCRIPT_MODE_FILE)
|
||||
set(TARGET_EXE ${CMAKE_ARGV3})
|
||||
set(WINDEPLOYQT ${CMAKE_ARGV4})
|
||||
get_filename_component(TARGET_DIR ${TARGET_EXE} DIRECTORY)
|
||||
|
||||
# Skip if already deployed for this build
|
||||
if(EXISTS "${TARGET_DIR}/.qt_deployed")
|
||||
return()
|
||||
endif()
|
||||
|
||||
message(STATUS "Running windeployqt on ${TARGET_EXE}")
|
||||
|
||||
execute_process(
|
||||
COMMAND ${WINDEPLOYQT}
|
||||
--no-compiler-runtime
|
||||
--no-translations
|
||||
--no-opengl-sw
|
||||
--no-system-d3d-compiler
|
||||
--force
|
||||
${TARGET_EXE}
|
||||
RESULT_VARIABLE _result
|
||||
)
|
||||
|
||||
if(_result EQUAL 0)
|
||||
file(WRITE "${TARGET_DIR}/.qt_deployed" "")
|
||||
message(STATUS "windeployqt completed successfully")
|
||||
else()
|
||||
message(WARNING "windeployqt failed with exit code ${_result}")
|
||||
endif()
|
||||
|
||||
return()
|
||||
endif()
|
||||
|
||||
# ── Include mode: configure the deploy target ──
|
||||
|
||||
if(NOT WIN32)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Discover windeployqt from qmake
|
||||
if(NOT TARGET ${QT}::windeployqt AND TARGET ${QT}::qmake)
|
||||
get_target_property(_qt_qmake_location ${QT}::qmake IMPORTED_LOCATION)
|
||||
|
||||
execute_process(
|
||||
COMMAND "${_qt_qmake_location}" -query QT_INSTALL_PREFIX
|
||||
RESULT_VARIABLE _return_code
|
||||
OUTPUT_VARIABLE _qt_install_prefix
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
set(_windeployqt "${_qt_install_prefix}/bin/windeployqt.exe")
|
||||
if(EXISTS ${_windeployqt})
|
||||
add_executable(${QT}::windeployqt IMPORTED)
|
||||
set_target_properties(${QT}::windeployqt PROPERTIES
|
||||
IMPORTED_LOCATION ${_windeployqt}
|
||||
)
|
||||
message(STATUS "Found windeployqt: ${_windeployqt}")
|
||||
else()
|
||||
message(WARNING "windeployqt not found at ${_windeployqt}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(TARGET ${QT}::windeployqt)
|
||||
# Standalone "deploy" target (can still be invoked manually)
|
||||
add_custom_target(deploy
|
||||
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
|
||||
$<TARGET_FILE:Reclass>
|
||||
$<TARGET_FILE:${QT}::windeployqt>
|
||||
DEPENDS Reclass
|
||||
COMMENT "Deploying Qt runtime DLLs..."
|
||||
)
|
||||
|
||||
# Force re-deploy on rebuild
|
||||
set_target_properties(deploy PROPERTIES
|
||||
ADDITIONAL_CLEAN_FILES $<TARGET_FILE_DIR:Reclass>/.qt_deployed
|
||||
)
|
||||
|
||||
# Auto-deploy as post-build step so the correct Qt DLLs are always next
|
||||
# to the exe. Without this, MSVC builds load whatever Qt DLLs happen to
|
||||
# be in PATH (often MinGW ones), causing instant ABI-mismatch crashes.
|
||||
add_custom_command(TARGET Reclass POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
|
||||
$<TARGET_FILE:Reclass>
|
||||
$<TARGET_FILE:${QT}::windeployqt>
|
||||
COMMENT "Auto-deploying Qt runtime DLLs...")
|
||||
endif()
|
||||
29
cmake/raw_pdb_prefix.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// Force-included before every raw_pdb translation unit (and consumers).
|
||||
// PDB_CRT.h forward-declares printf/memcmp/etc with extern "C" __cdecl,
|
||||
// which conflicts with MinGW's CRT headers (C++ linkage, no __cdecl).
|
||||
//
|
||||
// Fix: include the real CRT headers, then include PDB_CRT.h with function
|
||||
// names macro-renamed to harmless dummies. This triggers #pragma once so
|
||||
// no raw_pdb source file ever processes PDB_CRT.h's conflicting declarations.
|
||||
//
|
||||
// Guarded with __cplusplus because PUBLIC propagation applies this to C
|
||||
// sources (fadec) where PDB_CRT.h is irrelevant and <cstdio> doesn't exist.
|
||||
#ifdef __cplusplus
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#undef __cdecl
|
||||
#define __cdecl
|
||||
|
||||
#define printf _pdb_crt_unused_printf
|
||||
#define memcmp _pdb_crt_unused_memcmp
|
||||
#define memcpy _pdb_crt_unused_memcpy
|
||||
#define strlen _pdb_crt_unused_strlen
|
||||
#define strcmp _pdb_crt_unused_strcmp
|
||||
#include "Foundation/PDB_CRT.h"
|
||||
#undef printf
|
||||
#undef memcmp
|
||||
#undef memcpy
|
||||
#undef strlen
|
||||
#undef strcmp
|
||||
#endif
|
||||
8
deploy/Reclass.desktop
Normal file
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Reclass
|
||||
Comment=Memory structure reverse engineering tool
|
||||
Exec=Reclass
|
||||
Icon=reclass
|
||||
Categories=Development;Debugger;
|
||||
Terminal=false
|
||||
BIN
docs/README_PIC1.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
docs/README_PIC2.png
Normal file
|
After Width: | Height: | Size: 403 KiB |
BIN
docs/README_PIC3.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
docs/README_PIC4.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/README_PIC5.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
docs/README_PIC6.png
Normal file
|
After Width: | Height: | Size: 312 KiB |
160
docs/RECLASS_DARKMODE.svg
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 186.01 52.79">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#Unbenannter_Verlauf_130-2);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#Unbenannter_Verlauf_236-2);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#Unbenannter_Verlauf_225-2);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #1f2939;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #5d9bd4;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #1e3e88;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: #6e809a;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: url(#Unbenannter_Verlauf_225);
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
fill: url(#Unbenannter_Verlauf_236);
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
fill: url(#Unbenannter_Verlauf_130);
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
fill: url(#Unbenannter_Verlauf_170);
|
||||
}
|
||||
|
||||
.cls-12 {
|
||||
fill: url(#Unbenannter_Verlauf_161);
|
||||
}
|
||||
|
||||
.cls-13 {
|
||||
fill: url(#Unbenannter_Verlauf_183);
|
||||
}
|
||||
|
||||
.cls-14 {
|
||||
fill: #b06ba9;
|
||||
}
|
||||
|
||||
.cls-15 {
|
||||
fill: #826415;
|
||||
}
|
||||
|
||||
.cls-16 {
|
||||
fill: #e2aa11;
|
||||
}
|
||||
|
||||
.cls-17 {
|
||||
fill: #893089;
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.33" y1="8.33" x2="18.11" y2="18.11" gradientTransform="translate(13.22 -5.47) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f3db78"/>
|
||||
<stop offset=".19" stop-color="#f4e188"/>
|
||||
<stop offset=".34" stop-color="#f4e38d"/>
|
||||
<stop offset=".38" stop-color="#f4df81"/>
|
||||
<stop offset=".47" stop-color="#f5d86f"/>
|
||||
<stop offset=".57" stop-color="#f5d463"/>
|
||||
<stop offset=".67" stop-color="#f6d360"/>
|
||||
<stop offset=".89" stop-color="#f1cc53"/>
|
||||
<stop offset="1" stop-color="#efbe33"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.46" x2="10.98" y2="26.03" gradientTransform="translate(-4.95 39.45) rotate(-135)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".18" stop-color="#e2aa11"/>
|
||||
<stop offset=".91" stop-color="#826415"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.46" y1=".41" x2="26.03" y2="10.98" gradientTransform="translate(31.39 24.39) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.97" y1="15.65" x2="42.34" y2="23.02" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#deb0d3"/>
|
||||
<stop offset=".15" stop-color="#e1b5d6"/>
|
||||
<stop offset=".3" stop-color="#e3b8d7"/>
|
||||
<stop offset=".4" stop-color="#d7a8cd"/>
|
||||
<stop offset=".53" stop-color="#cf9cc7"/>
|
||||
<stop offset=".67" stop-color="#cd99c5"/>
|
||||
<stop offset=".89" stop-color="#c68abc"/>
|
||||
<stop offset="1" stop-color="#bb7db4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.78" y1="20.14" x2="36.87" y2="28.24" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#b06ba9"/>
|
||||
<stop offset=".87" stop-color="#893089"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.45" y1="9.43" x2="47.55" y2="17.53" xlink:href="#Unbenannter_Verlauf_225"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.88" y1="39.45" x2="42.29" y2="46.86" gradientTransform="translate(41.82 -14.64) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#91c4eb"/>
|
||||
<stop offset=".2" stop-color="#9dc9ed"/>
|
||||
<stop offset=".33" stop-color="#96c6ec"/>
|
||||
<stop offset=".35" stop-color="#91c3ea"/>
|
||||
<stop offset=".45" stop-color="#7fb8e5"/>
|
||||
<stop offset=".56" stop-color="#73b2e2"/>
|
||||
<stop offset=".67" stop-color="#70b0e1"/>
|
||||
<stop offset=".89" stop-color="#60a7dc"/>
|
||||
<stop offset="1" stop-color="#4d9bd5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.83" y1="43.9" x2="36.92" y2="51.99" gradientTransform="translate(22.68 105.31) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#5d9bd4"/>
|
||||
<stop offset=".87" stop-color="#1e3e88"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.51" y1="33.23" x2="47.59" y2="41.32" gradientTransform="translate(48.45 94.63) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
|
||||
</defs>
|
||||
<g>
|
||||
<rect class="cls-7" x="22.48" y="16.85" width="1.76" height="25.1"/>
|
||||
<rect class="cls-7" x="17.08" y="16.85" width="19.7" height="1.82"/>
|
||||
<rect class="cls-7" x="22.48" y="40.19" width="11.9" height="1.76"/>
|
||||
<g>
|
||||
<rect class="cls-12" x="2.56" y="6.31" width="21.31" height="13.82" transform="translate(-5.48 13.22) rotate(-45)"/>
|
||||
<rect class="cls-15" x="17.52" y="6.88" width="1.15" height="22.44" transform="translate(18.1 -7.49) rotate(45)"/>
|
||||
<g>
|
||||
<rect class="cls-16" x="7.76" y="-2.88" width="1.15" height="22.44" transform="translate(8.34 -3.45) rotate(45)"/>
|
||||
<rect class="cls-10" x="5.12" y="13.27" width="1.15" height="14.95" transform="translate(24.39 31.39) rotate(135)"/>
|
||||
<rect class="cls-1" x="20.17" y="-1.78" width="1.15" height="14.95" transform="translate(39.45 -4.95) rotate(135)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="cls-11" points="40.33 10.29 29.64 21.02 36.98 28.38 47.67 17.66 40.33 10.29"/>
|
||||
<polygon class="cls-17" points="37.01 29.1 36.29 28.38 47.68 16.96 48.39 17.68 37.01 29.1"/>
|
||||
<polygon class="cls-14" points="29.67 21.74 28.95 21.02 40.34 9.6 41.05 10.31 29.67 21.74"/>
|
||||
<polygon class="cls-8" points="28.95 21.02 29.67 20.3 37.72 28.38 37 29.1 28.95 21.02"/>
|
||||
<polygon class="cls-3" points="39.63 10.31 40.34 9.6 48.39 17.67 47.68 18.39 39.63 10.31"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect class="cls-13" x="30.96" y="37.92" width="15.26" height="10.48" transform="translate(-19.21 39.92) rotate(-45)"/>
|
||||
<g>
|
||||
<rect class="cls-9" x="32.83" y="42.71" width="1.01" height="11.38" transform="translate(91.13 59.06) rotate(135)"/>
|
||||
<rect class="cls-2" x="43.5" y="32.04" width="1.01" height="11.38" transform="translate(101.81 33.29) rotate(135)"/>
|
||||
<rect class="cls-6" x="41.84" y="38.69" width="1.01" height="16.1" transform="translate(45.45 -16.25) rotate(45)"/>
|
||||
<rect class="cls-5" x="34.5" y="31.35" width="1.01" height="16.1" transform="translate(38.11 -13.21) rotate(45)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-4" d="M53.66,30.51v11.46h-2.57v-25.13h9.36c5.04,0,7.72,2.72,7.72,6.65,0,3.22-1.89,5.26-4.51,5.89,2.34.58,4.09,2.2,4.09,6.5v1.02c0,1.74-.11,4.07.33,5.07h-2.54c-.46-1.08-.39-3.1-.39-5.34v-.6c0-3.87-1.12-5.52-5.79-5.52h-5.7ZM53.66,28.26h5.79c4.15,0,6.03-1.56,6.03-4.64,0-2.9-1.89-4.54-5.57-4.54h-6.25v9.18Z"/>
|
||||
<path class="cls-4" d="M86.55,29.87h-12.65v9.79h13.88l-.35,2.3h-16.06v-25.12h15.81v2.27h-13.28v8.49h12.65v2.27Z"/>
|
||||
<path class="cls-4" d="M109.12,35.04c-1.15,4.11-4.2,7.19-9.68,7.19-7.34,0-11.13-5.72-11.13-12.79s3.76-12.96,11.21-12.96c5.64,0,8.84,3.18,9.63,7.37h-2.56c-1.04-3.02-3.01-5.13-7.18-5.13-5.92,0-8.38,5.4-8.38,10.66s2.39,10.62,8.52,10.62c3.99,0,5.89-2.16,7.01-4.95h2.57Z"/>
|
||||
<path class="cls-4" d="M111.62,16.84h2.56v22.82h13.3l-.41,2.27h-15.46v-25.09Z"/>
|
||||
<path class="cls-4" d="M133.03,33.77l-2.97,8.16h-2.58l9.09-25.09h3.11l9.48,25.09h-2.76l-3.05-8.16h-10.32ZM142.61,31.5c-2.61-7.07-3.99-10.62-4.51-12.4h-.04c-.61,2-2.16,6.36-4.27,12.4h8.82Z"/>
|
||||
<path class="cls-4" d="M151.68,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.24,4.56,8.24,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
|
||||
<path class="cls-4" d="M170.59,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.25,4.56,8.25,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
160
docs/RECLASS_LIGHTMODE.svg
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.55 52.66">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#Unbenannter_Verlauf_130-2);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#Unbenannter_Verlauf_236-2);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#Unbenannter_Verlauf_225-2);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #5d9bd4;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #e3e8f0;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #1e3e88;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: #6e809a;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: url(#Unbenannter_Verlauf_225);
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
fill: url(#Unbenannter_Verlauf_236);
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
fill: url(#Unbenannter_Verlauf_130);
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
fill: url(#Unbenannter_Verlauf_170);
|
||||
}
|
||||
|
||||
.cls-12 {
|
||||
fill: url(#Unbenannter_Verlauf_161);
|
||||
}
|
||||
|
||||
.cls-13 {
|
||||
fill: url(#Unbenannter_Verlauf_183);
|
||||
}
|
||||
|
||||
.cls-14 {
|
||||
fill: #b06ba9;
|
||||
}
|
||||
|
||||
.cls-15 {
|
||||
fill: #826415;
|
||||
}
|
||||
|
||||
.cls-16 {
|
||||
fill: #e2aa11;
|
||||
}
|
||||
|
||||
.cls-17 {
|
||||
fill: #893089;
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.31" y1="8.31" x2="18.06" y2="18.06" gradientTransform="translate(13.19 -5.46) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f3db78"/>
|
||||
<stop offset=".19" stop-color="#f4e188"/>
|
||||
<stop offset=".34" stop-color="#f4e38d"/>
|
||||
<stop offset=".38" stop-color="#f4df81"/>
|
||||
<stop offset=".47" stop-color="#f5d86f"/>
|
||||
<stop offset=".57" stop-color="#f5d463"/>
|
||||
<stop offset=".67" stop-color="#f6d360"/>
|
||||
<stop offset=".89" stop-color="#f1cc53"/>
|
||||
<stop offset="1" stop-color="#efbe33"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.42" x2="10.95" y2="25.97" gradientTransform="translate(-4.94 39.35) rotate(-135)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".18" stop-color="#e2aa11"/>
|
||||
<stop offset=".91" stop-color="#826415"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.42" y1=".41" x2="25.97" y2="10.95" gradientTransform="translate(31.32 24.33) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.88" y1="15.61" x2="42.24" y2="22.97" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#deb0d3"/>
|
||||
<stop offset=".15" stop-color="#e1b5d6"/>
|
||||
<stop offset=".3" stop-color="#e3b8d7"/>
|
||||
<stop offset=".4" stop-color="#d7a8cd"/>
|
||||
<stop offset=".53" stop-color="#cf9cc7"/>
|
||||
<stop offset=".67" stop-color="#cd99c5"/>
|
||||
<stop offset=".89" stop-color="#c68abc"/>
|
||||
<stop offset="1" stop-color="#bb7db4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.7" y1="20.09" x2="36.78" y2="28.17" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#b06ba9"/>
|
||||
<stop offset=".87" stop-color="#893089"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.35" y1="9.41" x2="47.43" y2="17.49" xlink:href="#Unbenannter_Verlauf_225"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.79" y1="39.35" x2="42.18" y2="46.74" gradientTransform="translate(41.71 -14.61) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#91c4eb"/>
|
||||
<stop offset=".2" stop-color="#9dc9ed"/>
|
||||
<stop offset=".33" stop-color="#96c6ec"/>
|
||||
<stop offset=".35" stop-color="#91c3ea"/>
|
||||
<stop offset=".45" stop-color="#7fb8e5"/>
|
||||
<stop offset=".56" stop-color="#73b2e2"/>
|
||||
<stop offset=".67" stop-color="#70b0e1"/>
|
||||
<stop offset=".89" stop-color="#60a7dc"/>
|
||||
<stop offset="1" stop-color="#4d9bd5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.76" y1="43.79" x2="36.83" y2="51.86" gradientTransform="translate(22.62 105.05) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#5d9bd4"/>
|
||||
<stop offset=".87" stop-color="#1e3e88"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.41" y1="33.15" x2="47.47" y2="41.21" gradientTransform="translate(48.33 94.4) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
|
||||
</defs>
|
||||
<g>
|
||||
<rect class="cls-7" x="22.43" y="16.81" width="1.75" height="25.04"/>
|
||||
<rect class="cls-7" x="17.04" y="16.81" width="19.66" height="1.82"/>
|
||||
<rect class="cls-7" x="22.43" y="40.09" width="11.87" height="1.75"/>
|
||||
<g>
|
||||
<rect class="cls-12" x="2.56" y="6.29" width="21.26" height="13.79" transform="translate(-5.46 13.19) rotate(-45)"/>
|
||||
<rect class="cls-15" x="17.48" y="6.87" width="1.15" height="22.38" transform="translate(18.06 -7.47) rotate(45)"/>
|
||||
<g>
|
||||
<rect class="cls-16" x="7.74" y="-2.87" width="1.15" height="22.38" transform="translate(8.32 -3.45) rotate(45)"/>
|
||||
<rect class="cls-10" x="5.1" y="13.24" width="1.15" height="14.92" transform="translate(24.33 31.32) rotate(135)"/>
|
||||
<rect class="cls-1" x="20.12" y="-1.78" width="1.15" height="14.92" transform="translate(39.35 -4.94) rotate(135)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="cls-11" points="40.23 10.26 29.56 20.96 36.89 28.31 47.56 17.61 40.23 10.26"/>
|
||||
<polygon class="cls-17" points="36.91 29.03 36.2 28.31 47.56 16.92 48.27 17.63 36.91 29.03"/>
|
||||
<polygon class="cls-14" points="29.59 21.68 28.88 20.97 40.24 9.57 40.95 10.29 29.59 21.68"/>
|
||||
<polygon class="cls-8" points="28.88 20.97 29.59 20.25 37.62 28.31 36.91 29.02 28.88 20.97"/>
|
||||
<polygon class="cls-3" points="39.53 10.29 40.24 9.57 48.27 17.63 47.56 18.34 39.53 10.29"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect class="cls-13" x="30.88" y="37.82" width="15.22" height="10.45" transform="translate(-19.17 39.82) rotate(-45)"/>
|
||||
<g>
|
||||
<rect class="cls-9" x="32.75" y="42.61" width="1.01" height="11.36" transform="translate(90.91 58.91) rotate(135)"/>
|
||||
<rect class="cls-2" x="43.4" y="31.96" width="1.01" height="11.36" transform="translate(101.56 33.21) rotate(135)"/>
|
||||
<rect class="cls-6" x="41.73" y="38.59" width="1.01" height="16.06" transform="translate(45.34 -16.21) rotate(45)"/>
|
||||
<rect class="cls-4" x="34.41" y="31.27" width="1.01" height="16.06" transform="translate(38.02 -13.18) rotate(45)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-5" d="M53.53,30.44v11.43h-2.56v-25.07h9.34c5.03,0,7.7,2.71,7.7,6.63,0,3.21-1.88,5.24-4.5,5.87,2.33.58,4.08,2.19,4.08,6.49v1.01c0,1.74-.11,4.06.33,5.06h-2.54c-.46-1.08-.39-3.09-.39-5.33v-.59c0-3.87-1.12-5.51-5.78-5.51h-5.68ZM53.53,28.19h5.77c4.14,0,6.02-1.55,6.02-4.63,0-2.89-1.88-4.53-5.55-4.53h-6.24v9.16Z"/>
|
||||
<path class="cls-5" d="M86.34,29.8h-12.62v9.77h13.84l-.35,2.3h-16.02v-25.06h15.77v2.26h-13.25v8.47h12.62v2.26Z"/>
|
||||
<path class="cls-5" d="M108.85,34.96c-1.15,4.09-4.19,7.17-9.65,7.17-7.32,0-11.11-5.7-11.11-12.76s3.75-12.93,11.18-12.93c5.63,0,8.82,3.17,9.6,7.35h-2.56c-1.03-3.02-3-5.12-7.16-5.12-5.91,0-8.36,5.39-8.36,10.63s2.38,10.6,8.5,10.6c3.98,0,5.88-2.16,6.99-4.94h2.56Z"/>
|
||||
<path class="cls-5" d="M111.34,16.8h2.56v22.77h13.27l-.4,2.26h-15.42v-25.03Z"/>
|
||||
<path class="cls-5" d="M132.69,33.69l-2.96,8.14h-2.57l9.07-25.03h3.1l9.45,25.03h-2.75l-3.04-8.14h-10.3ZM142.25,31.43c-2.61-7.05-3.98-10.59-4.5-12.37h-.04c-.61,2-2.15,6.34-4.26,12.37h8.8Z"/>
|
||||
<path class="cls-5" d="M151.31,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
|
||||
<path class="cls-5" d="M170.17,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
117
msvc/ProcessMemoryPlugin.vcxproj
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{8792F51B-4951-4BAD-B130-2F0EFDEFF64B}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<RootNamespace>ProcessMemoryPlugin</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\src\processpicker.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="..\src\processpicker.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\plugins\ProcessMemory\ProcessMemoryPlugin.h" />
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\providers\provider.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\plugins\ProcessMemory\ProcessMemoryPlugin.cpp" />
|
||||
<ClCompile Include="..\src\processpicker.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
124
msvc/RcNetCompatPlugin.vcxproj
Normal file
@@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{6B775E9C-9CB6-45FD-86A0-BE948A778969}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<RootNamespace>RcNetCompatPlugin</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<PreprocessorDefinitions>HAS_CLR_BRIDGE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>HAS_CLR_BRIDGE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\src\processpicker.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="..\src\processpicker.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatPlugin.h" />
|
||||
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatProvider.h" />
|
||||
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\ReClassNET_Plugin.hpp" />
|
||||
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\ClrHost.h" />
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\providers\provider.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatPlugin.cpp" />
|
||||
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatProvider.cpp" />
|
||||
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\ClrHost.cpp" />
|
||||
<ClCompile Include="..\src\processpicker.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
17
msvc/Reclass.slnx
Normal file
@@ -0,0 +1,17 @@
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<Platform Name="x64" />
|
||||
</Configurations>
|
||||
<Folder Name="/plugins/">
|
||||
<Project Path="ProcessMemoryPlugin.vcxproj" Id="8792f51b-4951-4bad-b130-2f0efdeff64b" />
|
||||
<Project Path="WinDbgMemoryPlugin.vcxproj" Id="e25d358e-20f0-448b-bb2f-55e9d1f8e7ca" />
|
||||
<Project Path="RemoteProcessMemoryPlugin.vcxproj" Id="39e2ddf6-cb76-4063-b957-66ecf1252010" />
|
||||
<Project Path="RcNetCompatPlugin.vcxproj" Id="6b775e9c-9cb6-45fd-86a0-be948a778969" />
|
||||
</Folder>
|
||||
<Folder Name="/third_party/">
|
||||
<Project Path="../third_party/raw_pdb/build/RawPDB.vcxproj" Id="fbe3dbfa-20a7-4f99-9326-ed82c8b7b910" />
|
||||
<Project Path="fadec.vcxproj" Id="6a30a4f0-1a8d-4c6e-82d4-0a0d9693aa40" />
|
||||
<Project Path="qscintilla.vcxproj" Id="f7124b57-7682-4702-b725-4d844dc41ada" />
|
||||
</Folder>
|
||||
<Project Path="Reclass.vcxproj" Id="c369f1fe-37c2-4c66-ac6d-ecb2b2b4ad5e" />
|
||||
</Solution>
|
||||
211
msvc/Reclass.vcxproj
Normal file
@@ -0,0 +1,211 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="18.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{C369F1FE-37C2-4C66-AC6D-ECB2B2B4AD5E}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">10.0</WindowsTargetPlatformVersion>
|
||||
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets;concurrent;network;svg</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets;concurrent;network;svg</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<PostBuildEvent>
|
||||
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe
|
||||
xcopy /Y /I "$(SolutionDir)..\src\examples\*.rcx" "$(SolutionDir)$(Platform)\$(Configuration)\examples\"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Link>
|
||||
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
|
||||
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<PostBuildEvent>
|
||||
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe
|
||||
xcopy /Y /I "$(SolutionDir)..\src\examples\*.rcx" "$(SolutionDir)$(Platform)\$(Configuration)\examples\"</Command>
|
||||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ClCompile>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ClCompile>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<QtRcc Include="..\src\resources.qrc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="..\src\processpicker.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\src\controller.h" />
|
||||
<QtMoc Include="..\src\editor.h" />
|
||||
<QtMoc Include="..\src\mainwindow.h" />
|
||||
<QtMoc Include="..\src\optionsdialog.h" />
|
||||
<QtMoc Include="..\src\processpicker.h" />
|
||||
<QtMoc Include="..\src\scanner.h" />
|
||||
<QtMoc Include="..\src\scannerpanel.h" />
|
||||
<QtMoc Include="..\src\titlebar.h" />
|
||||
<QtMoc Include="..\src\typeselectorpopup.h" />
|
||||
<QtMoc Include="..\src\imports\import_pdb_dialog.h" />
|
||||
<QtMoc Include="..\src\mcp\mcp_bridge.h" />
|
||||
<QtMoc Include="..\src\themes\themeeditor.h" />
|
||||
<QtMoc Include="..\src\themes\thememanager.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\src\addressparser.h" />
|
||||
<ClInclude Include="..\src\core.h" />
|
||||
<ClInclude Include="..\src\disasm.h" />
|
||||
<QtMoc Include="..\src\dock_tab_buttons.h" />
|
||||
<ClInclude Include="..\src\generator.h" />
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\pluginmanager.h" />
|
||||
<ClInclude Include="..\src\providerregistry.h" />
|
||||
<QtMoc Include="..\src\startpage.h" />
|
||||
<ClInclude Include="..\src\workspace_model.h" />
|
||||
<ClInclude Include="..\src\imports\export_reclass_xml.h" />
|
||||
<ClInclude Include="..\src\imports\import_pdb.h" />
|
||||
<ClInclude Include="..\src\imports\import_reclass_xml.h" />
|
||||
<ClInclude Include="..\src\imports\import_source.h" />
|
||||
<ClInclude Include="..\src\providers\buffer_provider.h" />
|
||||
<ClInclude Include="..\src\providers\null_provider.h" />
|
||||
<ClInclude Include="..\src\providers\provider.h" />
|
||||
<ClInclude Include="..\src\providers\snapshot_provider.h" />
|
||||
<ClInclude Include="..\src\themes\theme.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\src\addressparser.cpp" />
|
||||
<ClCompile Include="..\src\compose.cpp" />
|
||||
<ClCompile Include="..\src\controller.cpp" />
|
||||
<ClCompile Include="..\src\disasm.cpp" />
|
||||
<ClCompile Include="..\src\editor.cpp" />
|
||||
<ClCompile Include="..\src\format.cpp" />
|
||||
<ClCompile Include="..\src\generator.cpp" />
|
||||
<ClCompile Include="..\src\main.cpp">
|
||||
<DynamicSource Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">input</DynamicSource>
|
||||
<QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(Filename).moc</QtMocFileName>
|
||||
<DynamicSource Condition="'$(Configuration)|$(Platform)'=='Release|x64'">input</DynamicSource>
|
||||
<QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(Filename).moc</QtMocFileName>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\optionsdialog.cpp" />
|
||||
<ClCompile Include="..\src\pluginmanager.cpp" />
|
||||
<ClCompile Include="..\src\processpicker.cpp" />
|
||||
<ClCompile Include="..\src\providerregistry.cpp" />
|
||||
<ClCompile Include="..\src\scanner.cpp" />
|
||||
<ClCompile Include="..\src\scannerpanel.cpp" />
|
||||
<ClCompile Include="..\src\titlebar.cpp" />
|
||||
<ClCompile Include="..\src\typeselectorpopup.cpp" />
|
||||
<ClCompile Include="..\src\imports\export_reclass_xml.cpp" />
|
||||
<ClCompile Include="..\src\imports\import_pdb.cpp" />
|
||||
<ClCompile Include="..\src\imports\import_pdb_dialog.cpp" />
|
||||
<ClCompile Include="..\src\imports\import_reclass_xml.cpp" />
|
||||
<ClCompile Include="..\src\imports\import_source.cpp" />
|
||||
<ClCompile Include="..\src\mcp\mcp_bridge.cpp" />
|
||||
<ClCompile Include="..\src\themes\theme.cpp" />
|
||||
<ClCompile Include="..\src\themes\themeeditor.cpp" />
|
||||
<ClCompile Include="..\src\themes\thememanager.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="fadec.vcxproj">
|
||||
<Project>{6A30A4F0-1A8D-4C6E-82D4-0A0D9693AA40}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="qscintilla.vcxproj">
|
||||
<Project>{F7124B57-7682-4702-B725-4D844DC41ADA}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\third_party\raw_pdb\build\RawPDB.vcxproj">
|
||||
<Project>{fbe3dbfa-20a7-4f99-9326-ed82c8b7b910}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
229
msvc/Reclass.vcxproj.filters
Normal file
@@ -0,0 +1,229 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\imports">
|
||||
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000001}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\mcp">
|
||||
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000002}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Source Files\themes">
|
||||
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000003}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files\imports">
|
||||
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000001}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Header Files\mcp">
|
||||
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000002}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Header Files\providers">
|
||||
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000003}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Header Files\themes">
|
||||
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000004}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Form Files">
|
||||
<UniqueIdentifier>{99349809-55BA-4b9d-BF79-8FDBB0286EB3}</UniqueIdentifier>
|
||||
<Extensions>ui</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtRcc Include="..\src\resources.qrc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</QtRcc>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="..\src\processpicker.ui">
|
||||
<Filter>Form Files</Filter>
|
||||
</QtUic>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\src\controller.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\editor.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\mainwindow.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\optionsdialog.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\processpicker.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\scanner.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\scannerpanel.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\titlebar.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\typeselectorpopup.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\imports\import_pdb_dialog.h">
|
||||
<Filter>Header Files\imports</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\mcp\mcp_bridge.h">
|
||||
<Filter>Header Files\mcp</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\themes\themeeditor.h">
|
||||
<Filter>Header Files\themes</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\themes\thememanager.h">
|
||||
<Filter>Header Files\themes</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\dock_tab_buttons.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
<QtMoc Include="..\src\startpage.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</QtMoc>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\src\addressparser.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\core.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\disasm.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\generator.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\iplugin.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\pluginmanager.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\providerregistry.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\workspace_model.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\imports\export_reclass_xml.h">
|
||||
<Filter>Header Files\imports</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\imports\import_pdb.h">
|
||||
<Filter>Header Files\imports</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\imports\import_reclass_xml.h">
|
||||
<Filter>Header Files\imports</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\imports\import_source.h">
|
||||
<Filter>Header Files\imports</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\providers\buffer_provider.h">
|
||||
<Filter>Header Files\providers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\providers\null_provider.h">
|
||||
<Filter>Header Files\providers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\providers\provider.h">
|
||||
<Filter>Header Files\providers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\providers\snapshot_provider.h">
|
||||
<Filter>Header Files\providers</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\src\themes\theme.h">
|
||||
<Filter>Header Files\themes</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\src\addressparser.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\compose.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\controller.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\disasm.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\editor.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\format.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\generator.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\optionsdialog.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\pluginmanager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\processpicker.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\providerregistry.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\scanner.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\scannerpanel.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\titlebar.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\typeselectorpopup.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\imports\export_reclass_xml.cpp">
|
||||
<Filter>Source Files\imports</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\imports\import_pdb.cpp">
|
||||
<Filter>Source Files\imports</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\imports\import_pdb_dialog.cpp">
|
||||
<Filter>Source Files\imports</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\imports\import_reclass_xml.cpp">
|
||||
<Filter>Source Files\imports</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\imports\import_source.cpp">
|
||||
<Filter>Source Files\imports</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\mcp\mcp_bridge.cpp">
|
||||
<Filter>Source Files\mcp</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\themes\theme.cpp">
|
||||
<Filter>Source Files\themes</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\themes\themeeditor.cpp">
|
||||
<Filter>Source Files\themes</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\themes\thememanager.cpp">
|
||||
<Filter>Source Files\themes</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\src\main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
118
msvc/RemoteProcessMemoryPlugin.vcxproj
Normal file
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{39E2DDF6-CB76-4063-B957-66ECF1252010}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<RootNamespace>RemoteProcessMemoryPlugin</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;..\plugins\RemoteProcessMemory;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;..\plugins\RemoteProcessMemory;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\src\processpicker.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<QtUic Include="..\src\processpicker.ui" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\plugins\RemoteProcessMemory\RemoteProcessMemoryPlugin.h" />
|
||||
<ClInclude Include="..\plugins\RemoteProcessMemory\rcx_rpc_protocol.h" />
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\providers\provider.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\plugins\RemoteProcessMemory\RemoteProcessMemoryPlugin.cpp" />
|
||||
<ClCompile Include="..\src\processpicker.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
112
msvc/WinDbgMemoryPlugin.vcxproj
Normal file
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{E25D358E-20F0-448B-BB2F-55E9D1F8E7CA}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<RootNamespace>WinDbgMemoryPlugin</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>dbgeng.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<AdditionalDependencies>dbgeng.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\plugins\WinDbgMemory\WinDbgMemoryPlugin.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\src\iplugin.h" />
|
||||
<ClInclude Include="..\src\providers\provider.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\plugins\WinDbgMemory\WinDbgMemoryPlugin.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
81
msvc/fadec.vcxproj
Normal file
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{6A30A4F0-1A8D-4C6E-82D4-0A0D9693AA40}</ProjectGuid>
|
||||
<RootNamespace>fadec</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<FadecDir>..\third_party\fadec\</FadecDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(FadecDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<DisableSpecificWarnings>4018;4146;4244;4245;4267;4310</DisableSpecificWarnings>
|
||||
<LanguageStandard_C>stdc11</LanguageStandard_C>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(FadecDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<DisableSpecificWarnings>4018;4146;4244;4245;4267;4310</DisableSpecificWarnings>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<LanguageStandard_C>stdc11</LanguageStandard_C>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<Target Name="GenerateFadecTables" BeforeTargets="ClCompile"
|
||||
Inputs="$(FadecDir)instrs.txt;$(FadecDir)parseinstrs.py"
|
||||
Outputs="$(FadecDir)fadec-decode-public.inc;$(FadecDir)fadec-decode-private.inc">
|
||||
<Exec Command="python "$(FadecDir)parseinstrs.py" decode "$(FadecDir)instrs.txt" "$(FadecDir)fadec-decode-public.inc" "$(FadecDir)fadec-decode-private.inc" --32 --64" />
|
||||
</Target>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\third_party\fadec\decode.c" />
|
||||
<ClCompile Include="..\third_party\fadec\format.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\third_party\fadec\fadec.h" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
445
msvc/qscintilla.vcxproj
Normal file
@@ -0,0 +1,445 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{F7124B57-7682-4702-B725-4D844DC41ADA}</ProjectGuid>
|
||||
<Keyword>QtVS_v304</Keyword>
|
||||
<RootNamespace>qscintilla</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>StaticLibrary</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
|
||||
<Import Project="$(QtMsBuild)\qt_defaults.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets;printsupport</QtModules>
|
||||
<QtBuildConfig>debug</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
|
||||
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
|
||||
<QtModules>core;gui;widgets;printsupport</QtModules>
|
||||
<QtBuildConfig>release</QtBuildConfig>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
|
||||
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
|
||||
</Target>
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
<Import Project="$(QtMsBuild)\Qt.props" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<QScintillaDir>..\third_party\qscintilla\</QScintillaDir>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(QScintillaDir)src;$(QScintillaDir)scintilla\include;$(QScintillaDir)scintilla\lexlib;$(QScintillaDir)scintilla\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>TurnOffAllWarnings</WarningLevel>
|
||||
<PreprocessorDefinitions>SCINTILLA_QT;SCI_LEXER;INCLUDE_DEPRECATED_FEATURES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ExceptionHandling>Sync</ExceptionHandling>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>$(QScintillaDir)src;$(QScintillaDir)scintilla\include;$(QScintillaDir)scintilla\lexlib;$(QScintillaDir)scintilla\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<WarningLevel>TurnOffAllWarnings</WarningLevel>
|
||||
<PreprocessorDefinitions>SCINTILLA_QT;SCI_LEXER;INCLUDE_DEPRECATED_FEATURES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ExceptionHandling>Sync</ExceptionHandling>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<!-- QtMoc headers (Q_OBJECT) — QScintilla Qt wrapper -->
|
||||
<ItemGroup>
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciabstractapis.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciapis.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexer.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerasm.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeravs.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerbash.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerbatch.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercmake.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercoffeescript.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercpp.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercsharp.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercss.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercustom.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerd.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerdiff.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeredifact.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerfortran.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerfortran77.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerhex.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerhtml.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeridl.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerintelhex.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjava.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjavascript.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjson.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerlua.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermakefile.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermarkdown.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermasm.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermatlab.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexernasm.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeroctave.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpascal.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerperl.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpostscript.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpo.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpov.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerproperties.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpython.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerruby.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerspice.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexersql.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexersrec.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertcl.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertekhex.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertex.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerverilog.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexervhdl.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerxml.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeryaml.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscimacro.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciscintilla.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciscintillabase.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\SciClasses.h" />
|
||||
<QtMoc Include="..\third_party\qscintilla\src\ScintillaQt.h" />
|
||||
</ItemGroup>
|
||||
<!-- ClInclude headers (no Q_OBJECT) -->
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscicommand.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscicommandset.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscidocument.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qsciglobal.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qsciprinter.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscistyle.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscistyledtext.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\ListBoxQt.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\src\SciAccessibility.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ILexer.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ILoader.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Platform.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Sci_Position.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\SciLexer.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Scintilla.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ScintillaWidget.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\Accessor.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\CharacterCategory.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\CharacterSet.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\DefaultLexer.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexAccessor.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerBase.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerModule.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerNoExceptions.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerSimple.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\OptionSet.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\PropSetSimple.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\SparseState.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\StringCopy.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\StyleContext.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\SubStyles.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\WordList.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\AutoComplete.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CallTip.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CaseConvert.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CaseFolder.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Catalogue.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CellBuffer.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CharClassify.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ContractionState.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\DBCS.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Decoration.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Document.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\EditModel.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Editor.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\EditView.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ElapsedPeriod.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ExternalLexer.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\FontQuality.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Indicator.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\IntegerRectangle.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\KeyMap.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\LineMarker.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\MarginView.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Partitioning.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\PerLine.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Position.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\PositionCache.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\RESearch.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\RunStyles.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ScintillaBase.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Selection.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\SparseVector.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\SplitVector.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Style.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\UniConversion.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\UniqueString.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ViewStyle.h" />
|
||||
<ClInclude Include="..\third_party\qscintilla\scintilla\src\XPM.h" />
|
||||
</ItemGroup>
|
||||
<!-- QScintilla Qt wrapper sources -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qsciscintilla.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qsciscintillabase.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qsciabstractapis.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qsciapis.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscicommand.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscicommandset.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscidocument.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexer.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerasm.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexeravs.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerbash.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerbatch.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercmake.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercoffeescript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercpp.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercsharp.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercss.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexercustom.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerd.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerdiff.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexeredifact.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerfortran.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerfortran77.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerhex.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerhtml.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexeridl.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerintelhex.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjava.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjavascript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjson.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerlua.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexermakefile.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexermarkdown.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexermasm.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexermatlab.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexernasm.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexeroctave.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpascal.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerperl.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpostscript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpo.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpov.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerproperties.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpython.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerruby.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerspice.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexersql.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexersrec.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexertcl.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexertekhex.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexertex.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerverilog.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexervhdl.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexerxml.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscilexeryaml.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscimacro.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qsciprinter.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscistyle.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\qscistyledtext.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\InputMethod.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\ListBoxQt.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\PlatQt.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\SciAccessibility.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\SciClasses.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\src\ScintillaQt.cpp" />
|
||||
</ItemGroup>
|
||||
<!-- Scintilla lexers -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexA68K.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAPDL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexASY.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAU3.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAVE.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAVS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAbaqus.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAda.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAsm.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAsn1.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBaan.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBash.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBasic.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBatch.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBibTeX.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBullant.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCLW.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCOBOL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCPP.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCSS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCaml.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCmake.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCoffeeScript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexConf.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCrontab.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCsound.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexD.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDMAP.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDMIS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDiff.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexECL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEDIFACT.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEScript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEiffel.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexErlang.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexErrorList.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexFlagship.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexForth.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexFortran.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexGAP.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexGui4Cli.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHTML.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHaskell.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHex.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexIndent.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexInno.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexJSON.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexKVIrc.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexKix.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLaTeX.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLisp.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLout.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLua.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMMIXAL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMPT.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMSSQL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMagik.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMake.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMarkdown.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMatlab.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMaxima.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMetapost.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexModula.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMySQL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNimrod.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNsis.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNull.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexOScript.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexOpal.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPB.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPLM.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPO.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPOV.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPascal.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPerl.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPowerPro.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPowerShell.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexProgress.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexProps.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPython.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexR.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRebol.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRegistry.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRuby.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRust.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSAS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSML.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSQL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSTTXT.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexScriptol.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSmalltalk.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSorcus.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSpecman.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSpice.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexStata.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTACL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTADS3.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTAL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTCL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTCMD.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTeX.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTxt2tags.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVB.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVHDL.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVerilog.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVisualProlog.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexYAML.cpp" />
|
||||
</ItemGroup>
|
||||
<!-- Scintilla lexlib -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\Accessor.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\CharacterCategory.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\CharacterSet.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\DefaultLexer.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerBase.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerModule.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerNoExceptions.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerSimple.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\PropSetSimple.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\StyleContext.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\WordList.cpp" />
|
||||
</ItemGroup>
|
||||
<!-- Scintilla core engine -->
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\AutoComplete.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CallTip.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CaseConvert.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CaseFolder.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Catalogue.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CellBuffer.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CharClassify.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ContractionState.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\DBCS.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Decoration.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Document.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\EditModel.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Editor.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\EditView.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ExternalLexer.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Indicator.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\KeyMap.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\LineMarker.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\MarginView.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\PerLine.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\PositionCache.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\RESearch.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\RunStyles.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ScintillaBase.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Selection.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Style.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\UniConversion.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ViewStyle.cpp" />
|
||||
<ClCompile Include="..\third_party\qscintilla\scintilla\src\XPM.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
|
||||
<Import Project="$(QtMsBuild)\qt.targets" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
63
plugins/KernelMemory/CMakeLists.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(KernelMemoryPlugin LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC OFF) # run uic manually to avoid dupbuild with ProcessMemoryPlugin
|
||||
|
||||
# ─── Generate ui_processpicker.h in our own build dir ────────────────
|
||||
set(_UI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui")
|
||||
set(_UI_HDR "${CMAKE_CURRENT_BINARY_DIR}/ui_processpicker.h")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_UI_HDR}"
|
||||
COMMAND ${QT}::uic -o "${_UI_HDR}" "${_UI_SRC}"
|
||||
DEPENDS "${_UI_SRC}"
|
||||
COMMENT "UIC processpicker.ui (KernelMemoryPlugin)"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
# ─── Plugin DLL ──────────────────────────────────────────────────────
|
||||
set(PLUGIN_SOURCES
|
||||
KernelMemoryPlugin.h
|
||||
KernelMemoryPlugin.cpp
|
||||
rcx_drv_protocol.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
|
||||
"${_UI_HDR}"
|
||||
)
|
||||
|
||||
add_library(KernelMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
target_link_libraries(KernelMemoryPlugin PRIVATE
|
||||
${QT}::Widgets
|
||||
${_QT_WINEXTRAS}
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(KernelMemoryPlugin PRIVATE psapi shell32 advapi32)
|
||||
endif()
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_compile_options(KernelMemoryPlugin PRIVATE -fvisibility=hidden)
|
||||
endif()
|
||||
|
||||
target_include_directories(KernelMemoryPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR} # for ui_processpicker.h
|
||||
)
|
||||
|
||||
set_target_properties(KernelMemoryPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
|
||||
install(TARGETS KernelMemoryPlugin
|
||||
LIBRARY DESTINATION Plugins
|
||||
RUNTIME DESTINATION Plugins
|
||||
)
|
||||
751
plugins/KernelMemory/KernelMemoryPlugin.cpp
Normal file
@@ -0,0 +1,751 @@
|
||||
#include "KernelMemoryPlugin.h"
|
||||
#include "../../src/processpicker.h"
|
||||
|
||||
#include <QStyle>
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QLibrary>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <psapi.h>
|
||||
#include <shellapi.h>
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
#include <QtWin>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Helper: DeviceIoControl wrapper
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
static bool ioctlCall(HANDLE h, DWORD code,
|
||||
const void* in, DWORD inLen,
|
||||
void* out, DWORD outLen,
|
||||
DWORD* bytesReturned = nullptr)
|
||||
{
|
||||
DWORD br = 0;
|
||||
BOOL ok = DeviceIoControl(h, code, const_cast<LPVOID>(in), inLen,
|
||||
out, outLen, &br, nullptr);
|
||||
if (bytesReturned) *bytesReturned = br;
|
||||
return ok != FALSE;
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// KernelProcessProvider
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
KernelProcessProvider::KernelProcessProvider(void* driverHandle, uint32_t pid, const QString& processName)
|
||||
: m_driverHandle(driverHandle)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
{
|
||||
if (m_driverHandle) {
|
||||
queryPeb();
|
||||
cacheModules();
|
||||
}
|
||||
}
|
||||
|
||||
bool KernelProcessProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_driverHandle || len <= 0) return false;
|
||||
if (len > RCX_DRV_MAX_VIRTUAL) len = RCX_DRV_MAX_VIRTUAL;
|
||||
|
||||
RcxDrvReadRequest req{};
|
||||
req.pid = m_pid;
|
||||
req.address = addr;
|
||||
req.length = (uint32_t)len;
|
||||
|
||||
DWORD br = 0;
|
||||
BOOL ok = DeviceIoControl((HANDLE)m_driverHandle,
|
||||
IOCTL_RCX_READ_MEMORY,
|
||||
&req, sizeof(req),
|
||||
buf, (DWORD)len, &br, nullptr);
|
||||
// Zero unread portion (partial copy)
|
||||
if ((int)br < len)
|
||||
memset((char*)buf + br, 0, len - br);
|
||||
return ok || br > 0;
|
||||
#else
|
||||
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int KernelProcessProvider::size() const
|
||||
{
|
||||
return m_driverHandle ? 0x10000 : 0;
|
||||
}
|
||||
|
||||
bool KernelProcessProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_driverHandle || len <= 0) return false;
|
||||
if (len > RCX_DRV_MAX_VIRTUAL) return false;
|
||||
|
||||
// Build request: header + inline data
|
||||
QByteArray packet(sizeof(RcxDrvWriteRequest) + len, Qt::Uninitialized);
|
||||
auto* req = reinterpret_cast<RcxDrvWriteRequest*>(packet.data());
|
||||
req->pid = m_pid;
|
||||
req->_pad0 = 0;
|
||||
req->address = addr;
|
||||
req->length = (uint32_t)len;
|
||||
req->_pad1 = 0;
|
||||
memcpy(packet.data() + sizeof(RcxDrvWriteRequest), buf, len);
|
||||
|
||||
return ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_WRITE_MEMORY,
|
||||
packet.constData(), (DWORD)packet.size(),
|
||||
nullptr, 0);
|
||||
#else
|
||||
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
QString KernelProcessProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (addr >= mod.base && addr < mod.base + mod.size) {
|
||||
uint64_t offset = addr - mod.base;
|
||||
return QStringLiteral("%1+0x%2")
|
||||
.arg(mod.name)
|
||||
.arg(offset, 0, 16, QChar('0'));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
uint64_t KernelProcessProvider::symbolToAddress(const QString& name) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
|
||||
return mod.base;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> KernelProcessProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
#ifdef _WIN32
|
||||
if (!m_driverHandle) return regions;
|
||||
|
||||
RcxDrvQueryRegionsRequest req{};
|
||||
req.pid = m_pid;
|
||||
|
||||
// Allocate generous output buffer for region entries
|
||||
constexpr int kMaxEntries = 8192;
|
||||
QByteArray outBuf(kMaxEntries * sizeof(RcxDrvRegionEntry), Qt::Uninitialized);
|
||||
|
||||
DWORD br = 0;
|
||||
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_REGIONS,
|
||||
&req, sizeof(req),
|
||||
outBuf.data(), (DWORD)outBuf.size(), &br))
|
||||
return regions;
|
||||
|
||||
int count = (int)(br / sizeof(RcxDrvRegionEntry));
|
||||
auto* entries = reinterpret_cast<const RcxDrvRegionEntry*>(outBuf.constData());
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const auto& e = entries[i];
|
||||
// Only include committed, accessible regions
|
||||
if (!(e.state & 0x1000)) continue; // MEM_COMMIT = 0x1000
|
||||
uint32_t p = e.protect;
|
||||
if (p & 0x01) continue; // PAGE_NOACCESS
|
||||
if (p & 0x100) continue; // PAGE_GUARD
|
||||
|
||||
rcx::MemoryRegion region;
|
||||
region.base = e.base;
|
||||
region.size = e.size;
|
||||
region.readable = true;
|
||||
region.writable = (p & 0x04) || (p & 0x08) || (p & 0x40) || (p & 0x80);
|
||||
region.executable = (p & 0x10) || (p & 0x20) || (p & 0x40) || (p & 0x80);
|
||||
|
||||
// Match module name
|
||||
for (const auto& mod : m_modules) {
|
||||
if (region.base >= mod.base && region.base < mod.base + mod.size) {
|
||||
region.moduleName = mod.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
regions.append(region);
|
||||
}
|
||||
#endif
|
||||
return regions;
|
||||
}
|
||||
|
||||
void KernelProcessProvider::queryPeb()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
RcxDrvQueryPebRequest req{};
|
||||
req.pid = m_pid;
|
||||
|
||||
RcxDrvQueryPebResponse resp{};
|
||||
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_PEB,
|
||||
&req, sizeof(req), &resp, sizeof(resp))) {
|
||||
m_peb = resp.pebAddress;
|
||||
if (resp.pointerSize == 4)
|
||||
m_pointerSize = 4;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
QVector<rcx::Provider::ThreadInfo> KernelProcessProvider::tebs() const
|
||||
{
|
||||
QVector<ThreadInfo> result;
|
||||
#ifdef _WIN32
|
||||
if (!m_driverHandle) return result;
|
||||
|
||||
RcxDrvQueryTebsRequest req{};
|
||||
req.pid = m_pid;
|
||||
|
||||
constexpr int kMaxThreads = 4096;
|
||||
QByteArray outBuf(kMaxThreads * sizeof(RcxDrvTebEntry), Qt::Uninitialized);
|
||||
|
||||
DWORD br = 0;
|
||||
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_TEBS,
|
||||
&req, sizeof(req),
|
||||
outBuf.data(), (DWORD)outBuf.size(), &br))
|
||||
return result;
|
||||
|
||||
int count = (int)(br / sizeof(RcxDrvTebEntry));
|
||||
auto* entries = reinterpret_cast<const RcxDrvTebEntry*>(outBuf.constData());
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
result.push_back(ThreadInfo{entries[i].tebAddress, entries[i].threadId});
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
void KernelProcessProvider::cacheModules()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_driverHandle) return;
|
||||
|
||||
RcxDrvQueryModulesRequest req{};
|
||||
req.pid = m_pid;
|
||||
|
||||
constexpr int kMaxModules = 1024;
|
||||
QByteArray outBuf(kMaxModules * sizeof(RcxDrvModuleEntry), Qt::Uninitialized);
|
||||
|
||||
DWORD br = 0;
|
||||
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_MODULES,
|
||||
&req, sizeof(req),
|
||||
outBuf.data(), (DWORD)outBuf.size(), &br))
|
||||
return;
|
||||
|
||||
int count = (int)(br / sizeof(RcxDrvModuleEntry));
|
||||
auto* entries = reinterpret_cast<const RcxDrvModuleEntry*>(outBuf.constData());
|
||||
|
||||
m_modules.reserve(count);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
QString modName = QString::fromUtf16(reinterpret_cast<const char16_t*>(entries[i].name));
|
||||
if (i == 0)
|
||||
m_base = entries[i].base;
|
||||
|
||||
m_modules.push_back(ModuleInfo{modName, entries[i].base, entries[i].size});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// KernelProcessProvider — paging / address translation
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
uint64_t KernelProcessProvider::getCr3() const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_cr3Cache) return m_cr3Cache;
|
||||
if (!m_driverHandle) return 0;
|
||||
|
||||
RcxDrvReadCr3Request req{};
|
||||
req.pid = m_pid;
|
||||
|
||||
RcxDrvReadCr3Response resp{};
|
||||
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_READ_CR3,
|
||||
&req, sizeof(req), &resp, sizeof(resp))) {
|
||||
m_cr3Cache = resp.cr3;
|
||||
return m_cr3Cache;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
rcx::VtopResult KernelProcessProvider::translateAddress(uint64_t va) const
|
||||
{
|
||||
rcx::VtopResult result{};
|
||||
#ifdef _WIN32
|
||||
if (!m_driverHandle) return result;
|
||||
|
||||
RcxDrvVtopRequest req{};
|
||||
req.pid = m_pid;
|
||||
req.virtualAddress = va;
|
||||
|
||||
RcxDrvVtopResponse resp{};
|
||||
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_VTOP,
|
||||
&req, sizeof(req), &resp, sizeof(resp))) {
|
||||
result.physical = resp.physicalAddress;
|
||||
result.pml4e = resp.pml4e;
|
||||
result.pdpte = resp.pdpte;
|
||||
result.pde = resp.pde;
|
||||
result.pte = resp.pte;
|
||||
result.pageSize = resp.pageSize;
|
||||
result.valid = resp.valid != 0;
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(va);
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
QVector<uint64_t> KernelProcessProvider::readPageTable(uint64_t physAddr, int startIdx, int count) const
|
||||
{
|
||||
QVector<uint64_t> entries;
|
||||
#ifdef _WIN32
|
||||
if (!m_driverHandle) return entries;
|
||||
if (startIdx < 0 || startIdx >= 512) return entries;
|
||||
if (count <= 0) return entries;
|
||||
if (startIdx + count > 512) count = 512 - startIdx;
|
||||
|
||||
// Read the full 4KB page table via physical read
|
||||
int byteOffset = startIdx * 8;
|
||||
int byteLen = count * 8;
|
||||
QByteArray buf(byteLen, 0);
|
||||
|
||||
RcxDrvPhysReadRequest req{};
|
||||
req.physAddress = physAddr + byteOffset;
|
||||
req.length = (uint32_t)byteLen;
|
||||
req.width = 0; // memcpy mode
|
||||
|
||||
DWORD br = 0;
|
||||
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_READ_PHYS,
|
||||
&req, sizeof(req), buf.data(), (DWORD)byteLen, &br)) {
|
||||
entries.resize(count);
|
||||
memcpy(entries.data(), buf.constData(), byteLen);
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(physAddr); Q_UNUSED(startIdx); Q_UNUSED(count);
|
||||
#endif
|
||||
return entries;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// KernelPhysProvider
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
KernelPhysProvider::KernelPhysProvider(void* driverHandle, uint64_t baseAddr)
|
||||
: m_driverHandle(driverHandle)
|
||||
, m_baseAddr(baseAddr)
|
||||
{
|
||||
}
|
||||
|
||||
bool KernelPhysProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_driverHandle || len <= 0) return false;
|
||||
|
||||
// Read in 4KB chunks (driver cap)
|
||||
int offset = 0;
|
||||
while (offset < len) {
|
||||
int chunk = qMin(len - offset, (int)RCX_DRV_MAX_PHYSICAL);
|
||||
|
||||
RcxDrvPhysReadRequest req{};
|
||||
req.physAddress = addr + offset;
|
||||
req.length = (uint32_t)chunk;
|
||||
req.width = 0; // memcpy mode
|
||||
|
||||
DWORD br = 0;
|
||||
BOOL ok = DeviceIoControl((HANDLE)m_driverHandle,
|
||||
IOCTL_RCX_READ_PHYS,
|
||||
&req, sizeof(req),
|
||||
(char*)buf + offset, (DWORD)chunk, &br, nullptr);
|
||||
if (!ok && br == 0) {
|
||||
memset((char*)buf + offset, 0, len - offset);
|
||||
return offset > 0;
|
||||
}
|
||||
if ((int)br < chunk)
|
||||
memset((char*)buf + offset + br, 0, chunk - br);
|
||||
offset += chunk;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool KernelPhysProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_driverHandle || len <= 0) return false;
|
||||
|
||||
int offset = 0;
|
||||
while (offset < len) {
|
||||
int chunk = qMin(len - offset, (int)RCX_DRV_MAX_PHYSICAL);
|
||||
|
||||
QByteArray packet(sizeof(RcxDrvPhysWriteRequest) + chunk, Qt::Uninitialized);
|
||||
auto* req = reinterpret_cast<RcxDrvPhysWriteRequest*>(packet.data());
|
||||
req->physAddress = addr + offset;
|
||||
req->length = (uint32_t)chunk;
|
||||
req->width = 0;
|
||||
memcpy(packet.data() + sizeof(RcxDrvPhysWriteRequest), (const char*)buf + offset, chunk);
|
||||
|
||||
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_WRITE_PHYS,
|
||||
packet.constData(), (DWORD)packet.size(),
|
||||
nullptr, 0))
|
||||
return false;
|
||||
offset += chunk;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// KernelMemoryPlugin
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
KernelMemoryPlugin::KernelMemoryPlugin()
|
||||
{
|
||||
}
|
||||
|
||||
KernelMemoryPlugin::~KernelMemoryPlugin()
|
||||
{
|
||||
stopDriver();
|
||||
}
|
||||
|
||||
QIcon KernelMemoryPlugin::Icon() const
|
||||
{
|
||||
return qApp->style()->standardIcon(QStyle::SP_DriveHDIcon);
|
||||
}
|
||||
|
||||
bool KernelMemoryPlugin::canHandle(const QString& target) const
|
||||
{
|
||||
return target.startsWith(QStringLiteral("km:"))
|
||||
|| target.startsWith(QStringLiteral("phys:"));
|
||||
}
|
||||
|
||||
std::unique_ptr<rcx::Provider> KernelMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
{
|
||||
if (!ensureDriverLoaded(errorMsg))
|
||||
return nullptr;
|
||||
|
||||
#ifdef _WIN32
|
||||
if (target.startsWith(QStringLiteral("km:"))) {
|
||||
// km:{pid}:{name}
|
||||
QStringList parts = target.mid(3).split(':');
|
||||
bool ok = false;
|
||||
uint32_t pid = parts[0].toUInt(&ok);
|
||||
if (!ok || pid == 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target: ") + target;
|
||||
return nullptr;
|
||||
}
|
||||
QString name = parts.size() > 1 ? parts[1] : QStringLiteral("PID %1").arg(pid);
|
||||
auto prov = std::make_unique<KernelProcessProvider>((void*)m_driverHandle, pid, name);
|
||||
if (!prov->isValid()) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Failed to read process %1 (PID: %2) via kernel driver.")
|
||||
.arg(name).arg(pid);
|
||||
return nullptr;
|
||||
}
|
||||
return prov;
|
||||
}
|
||||
|
||||
if (target.startsWith(QStringLiteral("phys:"))) {
|
||||
// phys:{baseAddr}
|
||||
bool ok = false;
|
||||
uint64_t baseAddr = target.mid(5).toULongLong(&ok, 16);
|
||||
if (!ok) baseAddr = 0;
|
||||
return std::make_unique<KernelPhysProvider>((void*)m_driverHandle, baseAddr);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Unknown target format: ") + target;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint64_t KernelMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||
{
|
||||
if (target.startsWith(QStringLiteral("phys:"))) {
|
||||
bool ok = false;
|
||||
uint64_t addr = target.mid(5).toULongLong(&ok, 16);
|
||||
return ok ? addr : 0;
|
||||
}
|
||||
// For process mode, the provider discovers base via modules
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool KernelMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
// Show process picker directly (physical memory is accessed via
|
||||
// context menu "Browse Page Tables" / "Follow Physical Frame" on an
|
||||
// attached kernel process).
|
||||
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
|
||||
QList<ProcessInfo> processes;
|
||||
for (const auto& pinfo : pluginProcesses) {
|
||||
ProcessInfo info;
|
||||
info.pid = pinfo.pid;
|
||||
info.name = pinfo.name;
|
||||
info.path = pinfo.path;
|
||||
info.icon = pinfo.icon;
|
||||
info.is32Bit = pinfo.is32Bit;
|
||||
processes.append(info);
|
||||
}
|
||||
|
||||
ProcessPicker picker(processes, parent);
|
||||
if (picker.exec() == QDialog::Accepted) {
|
||||
uint32_t pid = picker.selectedProcessId();
|
||||
QString name = picker.selectedProcessName();
|
||||
*target = QStringLiteral("km:%1:%2").arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<PluginProcessInfo> KernelMemoryPlugin::enumerateProcesses()
|
||||
{
|
||||
QVector<PluginProcessInfo> processes;
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) return processes;
|
||||
|
||||
PROCESSENTRY32W entry;
|
||||
entry.dwSize = sizeof(entry);
|
||||
|
||||
if (Process32FirstW(snapshot, &entry)) {
|
||||
do {
|
||||
PluginProcessInfo info;
|
||||
info.pid = entry.th32ProcessID;
|
||||
info.name = QString::fromWCharArray(entry.szExeFile);
|
||||
|
||||
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID);
|
||||
if (hProcess) {
|
||||
wchar_t path[MAX_PATH * 2];
|
||||
DWORD pathLen = sizeof(path) / sizeof(wchar_t);
|
||||
|
||||
if (QueryFullProcessImageNameW(hProcess, 0, path, &pathLen)) {
|
||||
info.path = QString::fromWCharArray(path);
|
||||
|
||||
SHFILEINFOW sfi = {};
|
||||
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) {
|
||||
if (sfi.hIcon) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QPixmap pixmap = QPixmap::fromImage(QImage::fromHICON(sfi.hIcon));
|
||||
#else
|
||||
QPixmap pixmap = QtWin::fromHICON(sfi.hIcon);
|
||||
#endif
|
||||
info.icon = QIcon(pixmap);
|
||||
DestroyIcon(sfi.hIcon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOL isWow64 = FALSE;
|
||||
if (IsWow64Process(hProcess, &isWow64) && isWow64)
|
||||
info.is32Bit = true;
|
||||
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
|
||||
processes.append(info);
|
||||
} while (Process32NextW(snapshot, &entry));
|
||||
}
|
||||
|
||||
CloseHandle(snapshot);
|
||||
#endif
|
||||
|
||||
return processes;
|
||||
}
|
||||
|
||||
void KernelMemoryPlugin::populatePluginMenu(QMenu* menu)
|
||||
{
|
||||
if (!m_driverLoaded) return;
|
||||
menu->addAction(QStringLiteral("Unload Kernel Driver"), [this]() { unloadDriver(); });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Driver service management
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
QString KernelMemoryPlugin::driverPath() const
|
||||
{
|
||||
// Resolve rcxdrv.sys next to the plugin DLL
|
||||
QString pluginDir = QCoreApplication::applicationDirPath() + QStringLiteral("/Plugins");
|
||||
return pluginDir + QStringLiteral("/rcxdrv.sys");
|
||||
}
|
||||
|
||||
bool KernelMemoryPlugin::ensureDriverLoaded(QString* errorMsg)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Already connected?
|
||||
if (m_driverLoaded && m_driverHandle != INVALID_HANDLE_VALUE) {
|
||||
RcxDrvPingResponse ping{};
|
||||
if (ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping)))
|
||||
return true;
|
||||
// Handle went stale — close it and try to reconnect
|
||||
CloseHandle(m_driverHandle);
|
||||
m_driverHandle = INVALID_HANDLE_VALUE;
|
||||
m_driverLoaded = false;
|
||||
}
|
||||
|
||||
// Show wait cursor (SCM + StartService can take seconds on first load)
|
||||
struct WaitCursorGuard {
|
||||
WaitCursorGuard() { QGuiApplication::setOverrideCursor(Qt::WaitCursor); }
|
||||
~WaitCursorGuard() { QGuiApplication::restoreOverrideCursor(); }
|
||||
} waitCursor;
|
||||
|
||||
// Fast path: driver may already be running (previous session, or after disconnect).
|
||||
// Just try to open the device handle directly.
|
||||
m_driverHandle = CreateFileA(RCX_DRV_USERMODE_PATH,
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
0, nullptr, OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (m_driverHandle != INVALID_HANDLE_VALUE) {
|
||||
RcxDrvPingResponse ping{};
|
||||
if (ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping))) {
|
||||
m_driverLoaded = true;
|
||||
return true;
|
||||
}
|
||||
CloseHandle(m_driverHandle);
|
||||
m_driverHandle = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
|
||||
// Slow path: need to install/start the service.
|
||||
QString sysPath = driverPath();
|
||||
if (!QFileInfo::exists(sysPath)) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Driver not found: %1\n\n"
|
||||
"Place rcxdrv.sys in the Plugins folder next to the plugin DLL.").arg(sysPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
|
||||
if (!scm) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Failed to open Service Control Manager.\n"
|
||||
"Run Reclass as Administrator to load the kernel driver.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to open existing service first
|
||||
SC_HANDLE svc = OpenServiceW(scm, L"RcxDrv", SERVICE_ALL_ACCESS);
|
||||
if (!svc) {
|
||||
// Service doesn't exist — create it
|
||||
std::wstring wPath = sysPath.toStdWString();
|
||||
svc = CreateServiceW(scm, L"RcxDrv", L"RcxDrv",
|
||||
SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER,
|
||||
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
|
||||
wPath.c_str(),
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr);
|
||||
if (!svc) {
|
||||
DWORD err = GetLastError();
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Failed to create driver service (error %1).\n"
|
||||
"Ensure test signing is enabled: bcdedit /set testsigning on").arg(err);
|
||||
CloseServiceHandle(scm);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Start service (ERROR_SERVICE_ALREADY_RUNNING is fine — means it's already up)
|
||||
if (!StartServiceW(svc, 0, nullptr)) {
|
||||
DWORD err = GetLastError();
|
||||
if (err != ERROR_SERVICE_ALREADY_RUNNING) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Failed to start driver (error %1).\n"
|
||||
"Ensure test signing is enabled and the driver is properly signed.").arg(err);
|
||||
CloseServiceHandle(svc);
|
||||
CloseServiceHandle(scm);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Done with SCM — don't hold handles open
|
||||
CloseServiceHandle(svc);
|
||||
CloseServiceHandle(scm);
|
||||
|
||||
// Open device handle
|
||||
m_driverHandle = CreateFileA(RCX_DRV_USERMODE_PATH,
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
0, nullptr, OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (m_driverHandle == INVALID_HANDLE_VALUE) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Driver started but could not open device handle.\n"
|
||||
"Device path: %1").arg(QString::fromLatin1(RCX_DRV_USERMODE_PATH));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify with ping
|
||||
RcxDrvPingResponse ping{};
|
||||
if (!ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping))) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Driver opened but ping failed.");
|
||||
CloseHandle(m_driverHandle);
|
||||
m_driverHandle = INVALID_HANDLE_VALUE;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_driverLoaded = true;
|
||||
return true;
|
||||
#else
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Kernel driver is only supported on Windows.");
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void KernelMemoryPlugin::unloadDriver()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Close device handle only — service stays running so we can reconnect
|
||||
if (m_driverHandle != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(m_driverHandle);
|
||||
m_driverHandle = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
m_driverLoaded = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void KernelMemoryPlugin::stopDriver()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
unloadDriver();
|
||||
|
||||
// Full cleanup: stop + delete the service
|
||||
SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
|
||||
if (scm) {
|
||||
SC_HANDLE svc = OpenServiceW(scm, L"RcxDrv", SERVICE_ALL_ACCESS);
|
||||
if (svc) {
|
||||
SERVICE_STATUS ss;
|
||||
ControlService(svc, SERVICE_CONTROL_STOP, &ss);
|
||||
DeleteService(svc);
|
||||
CloseServiceHandle(svc);
|
||||
}
|
||||
CloseServiceHandle(scm);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Plugin factory
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new KernelMemoryPlugin();
|
||||
}
|
||||
142
plugins/KernelMemory/KernelMemoryPlugin.h
Normal file
@@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
#include "../../src/iplugin.h"
|
||||
#include "../../src/core.h"
|
||||
#include "rcx_drv_protocol.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Provider variants
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Kernel-mode process memory provider.
|
||||
* Reads/writes target process virtual memory via IOCTL_RCX_READ/WRITE_MEMORY.
|
||||
*/
|
||||
class KernelProcessProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
KernelProcessProvider(void* driverHandle, uint32_t pid, const QString& processName);
|
||||
~KernelProcessProvider() override = default;
|
||||
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
int size() const override;
|
||||
|
||||
bool write(uint64_t addr, const void* buf, int len) override;
|
||||
bool isWritable() const override { return true; }
|
||||
QString name() const override { return m_processName; }
|
||||
QString kind() const override { return QStringLiteral("KernelProcess"); }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
uint64_t symbolToAddress(const QString& name) const override;
|
||||
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
int pointerSize() const override { return m_pointerSize; }
|
||||
QVector<rcx::MemoryRegion> enumerateRegions() const override;
|
||||
bool isReadable(uint64_t, int len) const override { return m_driverHandle && len >= 0; }
|
||||
|
||||
uint32_t pid() const { return m_pid; }
|
||||
uint64_t peb() const override { return m_peb; }
|
||||
QVector<ThreadInfo> tebs() const override;
|
||||
|
||||
// ── Paging / address translation ──
|
||||
bool hasKernelPaging() const override { return true; }
|
||||
uint64_t getCr3() const override;
|
||||
rcx::VtopResult translateAddress(uint64_t va) const override;
|
||||
QVector<uint64_t> readPageTable(uint64_t physAddr, int startIdx = 0, int count = 512) const override;
|
||||
void* driverHandle() const { return m_driverHandle; }
|
||||
|
||||
private:
|
||||
void queryPeb();
|
||||
void cacheModules();
|
||||
|
||||
void* m_driverHandle;
|
||||
uint32_t m_pid;
|
||||
QString m_processName;
|
||||
uint64_t m_base = 0;
|
||||
int m_pointerSize = 8;
|
||||
uint64_t m_peb = 0;
|
||||
mutable uint64_t m_cr3Cache = 0;
|
||||
|
||||
struct ModuleInfo {
|
||||
QString name;
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
};
|
||||
QVector<ModuleInfo> m_modules;
|
||||
};
|
||||
|
||||
/**
|
||||
* Kernel-mode physical memory provider.
|
||||
* Reads/writes raw physical addresses via IOCTL_RCX_READ/WRITE_PHYS.
|
||||
*/
|
||||
class KernelPhysProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
KernelPhysProvider(void* driverHandle, uint64_t baseAddr);
|
||||
~KernelPhysProvider() override = default;
|
||||
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
int size() const override { return m_driverHandle ? 0x10000 : 0; }
|
||||
|
||||
bool write(uint64_t addr, const void* buf, int len) override;
|
||||
bool isWritable() const override { return true; }
|
||||
QString name() const override { return QStringLiteral("Physical Memory"); }
|
||||
QString kind() const override { return QStringLiteral("Physical"); }
|
||||
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_baseAddr; }
|
||||
bool isReadable(uint64_t, int len) const override { return m_driverHandle && len >= 0; }
|
||||
|
||||
void setBaseAddr(uint64_t addr) { m_baseAddr = addr; }
|
||||
void* driverHandle() const { return m_driverHandle; }
|
||||
|
||||
private:
|
||||
void* m_driverHandle;
|
||||
uint64_t m_baseAddr;
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Plugin
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class KernelMemoryPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
KernelMemoryPlugin();
|
||||
~KernelMemoryPlugin() override;
|
||||
|
||||
std::string Name() const override { return "Kernel Memory"; }
|
||||
std::string Version() const override { return "1.0.0"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
std::string Description() const override { return "Read and write memory via kernel driver (IOCTL)"; }
|
||||
k_ELoadType LoadType() const override { return k_ELoadTypeManual; }
|
||||
QIcon Icon() const override;
|
||||
|
||||
bool canHandle(const QString& target) const override;
|
||||
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
|
||||
uint64_t getInitialBaseAddress(const QString& target) const override;
|
||||
bool selectTarget(QWidget* parent, QString* target) override;
|
||||
|
||||
bool providesProcessList() const override { return true; }
|
||||
QVector<PluginProcessInfo> enumerateProcesses() override;
|
||||
void populatePluginMenu(QMenu* menu) override;
|
||||
|
||||
private:
|
||||
bool ensureDriverLoaded(QString* errorMsg = nullptr);
|
||||
void unloadDriver(); // close handle only — service stays running
|
||||
void stopDriver(); // full cleanup: close handle + stop + delete service
|
||||
QString driverPath() const;
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE m_driverHandle = INVALID_HANDLE_VALUE;
|
||||
#endif
|
||||
bool m_driverLoaded = false;
|
||||
};
|
||||
|
||||
// Plugin export
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||
99
plugins/KernelMemory/driver/build_driver.bat
Normal file
@@ -0,0 +1,99 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: ── Auto-detect MSVC (override with MSVC env var) ──
|
||||
if not defined MSVC (
|
||||
set "VSBASE=C:\Program Files\Microsoft Visual Studio\2022"
|
||||
for %%E in (Enterprise Professional Community BuildTools) do (
|
||||
if exist "!VSBASE!\%%E\VC\Tools\MSVC" (
|
||||
for /f "delims=" %%V in ('dir /b /ad /o-n "!VSBASE!\%%E\VC\Tools\MSVC" 2^>nul') do (
|
||||
if not defined MSVC set "MSVC=!VSBASE!\%%E\VC\Tools\MSVC\%%V"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
if not defined MSVC (
|
||||
echo ERROR: Could not find MSVC toolchain
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: ── Auto-detect WDK (override with WDK_INC_ROOT and WDK_LIB_ROOT env vars) ──
|
||||
:: SDK_INC_ROOT is optional; when WDK is installed traditionally, SDK shared
|
||||
:: headers live alongside WDK headers. NuGet splits them into a separate package.
|
||||
if not defined WDK_INC_ROOT (
|
||||
set "WDK=C:\Program Files (x86)\Windows Kits\10"
|
||||
set WDKVER=
|
||||
for /f "delims=" %%V in ('dir /b /ad /o-n "!WDK!\Include" 2^>nul') do (
|
||||
if exist "!WDK!\Include\%%V\km\ntddk.h" (
|
||||
if not defined WDKVER set "WDKVER=%%V"
|
||||
)
|
||||
)
|
||||
if not defined WDKVER (
|
||||
echo ERROR: Could not find WDK headers under !WDK!\Include
|
||||
echo Set WDK_INC_ROOT and WDK_LIB_ROOT environment variables to override.
|
||||
exit /b 1
|
||||
)
|
||||
set "WDK_INC_ROOT=!WDK!\Include\!WDKVER!"
|
||||
set "WDK_LIB_ROOT=!WDK!\Lib\!WDKVER!"
|
||||
set "SDK_INC_ROOT=!WDK!\Include\!WDKVER!"
|
||||
)
|
||||
|
||||
:: If SDK_INC_ROOT not set, default to WDK_INC_ROOT (traditional install has both)
|
||||
if not defined SDK_INC_ROOT set "SDK_INC_ROOT=%WDK_INC_ROOT%"
|
||||
|
||||
echo Using MSVC: %MSVC%
|
||||
echo Using WDK inc: %WDK_INC_ROOT%
|
||||
echo Using SDK inc: %SDK_INC_ROOT%
|
||||
echo Using WDK lib: %WDK_LIB_ROOT%
|
||||
|
||||
set "CL_EXE=%MSVC%\bin\Hostx64\x64\cl.exe"
|
||||
set "LINK_EXE=%MSVC%\bin\Hostx64\x64\link.exe"
|
||||
|
||||
set "SRCDIR=%~dp0"
|
||||
set "OUTDIR=%SRCDIR%build"
|
||||
|
||||
if not exist "%OUTDIR%" mkdir "%OUTDIR%"
|
||||
|
||||
echo === Compiling rcxdrv.c ===
|
||||
"%CL_EXE%" /nologo /c /Zi /W4 /WX- /O2 /GS- ^
|
||||
/D "NDEBUG" /D "_AMD64_" /D "AMD64" /D "_WIN64" /D "KERNEL" ^
|
||||
/D "NTDDI_VERSION=0x0A000000" ^
|
||||
/I "%WDK_INC_ROOT%\km" ^
|
||||
/I "%WDK_INC_ROOT%\km\crt" ^
|
||||
/I "%WDK_INC_ROOT%\shared" ^
|
||||
/I "%SDK_INC_ROOT%\shared" ^
|
||||
/I "%SDK_INC_ROOT%\ucrt" ^
|
||||
/kernel ^
|
||||
/Fo"%OUTDIR%\rcxdrv.obj" ^
|
||||
"%SRCDIR%rcxdrv.c"
|
||||
if errorlevel 1 goto :fail
|
||||
|
||||
echo === Linking rcxdrv.sys ===
|
||||
"%LINK_EXE%" /nologo ^
|
||||
/OUT:"%OUTDIR%\rcxdrv.sys" ^
|
||||
/DRIVER:WDM ^
|
||||
/SUBSYSTEM:NATIVE ^
|
||||
/ENTRY:DriverEntry ^
|
||||
/MACHINE:X64 ^
|
||||
/NODEFAULTLIB ^
|
||||
/RELEASE ^
|
||||
/MERGE:.rdata=.text ^
|
||||
/INTEGRITYCHECK ^
|
||||
/PDBALTPATH:rcxdrv.pdb ^
|
||||
/PDB:"%OUTDIR%\rcxdrv.pdb" ^
|
||||
"%OUTDIR%\rcxdrv.obj" ^
|
||||
"%WDK_LIB_ROOT%\km\x64\ntoskrnl.lib" ^
|
||||
"%WDK_LIB_ROOT%\km\x64\hal.lib" ^
|
||||
"%WDK_LIB_ROOT%\km\x64\BufferOverflowK.lib" ^
|
||||
"%MSVC%\lib\x64\libcmt.lib"
|
||||
if errorlevel 1 goto :fail
|
||||
|
||||
echo.
|
||||
echo === SUCCESS ===
|
||||
echo Output: %OUTDIR%\rcxdrv.sys
|
||||
goto :eof
|
||||
|
||||
:fail
|
||||
echo.
|
||||
echo === BUILD FAILED ===
|
||||
exit /b 1
|
||||
808
plugins/KernelMemory/driver/rcxdrv.c
Normal file
@@ -0,0 +1,808 @@
|
||||
/*
|
||||
* rcxdrv.c -- Minimal kernel-mode memory driver for Reclass.
|
||||
*
|
||||
* Provides: virtual memory R/W (per-process), physical memory R/W,
|
||||
* region/PEB/module/TEB query, CR3 read, virtual-to-physical translation.
|
||||
*
|
||||
* Safety: all inputs validated, SEH around privileged instructions,
|
||||
* MmCopyVirtualMemory for cross-process reads (no attach deadlock),
|
||||
* METHOD_BUFFERED (no raw user pointers).
|
||||
*/
|
||||
#include <ntifs.h>
|
||||
#include "../rcx_drv_protocol.h"
|
||||
|
||||
/* ── Undocumented but stable kernel exports (Vista+) ────────────── */
|
||||
|
||||
NTSTATUS NTAPI MmCopyVirtualMemory(
|
||||
PEPROCESS SourceProcess, PVOID SourceAddress,
|
||||
PEPROCESS TargetProcess, PVOID TargetAddress,
|
||||
SIZE_T BufferSize, KPROCESSOR_MODE PreviousMode,
|
||||
PSIZE_T ReturnSize);
|
||||
|
||||
PPEB NTAPI PsGetProcessPeb(PEPROCESS Process);
|
||||
PVOID NTAPI PsGetProcessWow64Process(PEPROCESS Process);
|
||||
PVOID NTAPI PsGetThreadTeb(PETHREAD Thread);
|
||||
|
||||
/*
|
||||
* PsGetNextProcessThread is undocumented (not in any .lib).
|
||||
* We resolve it dynamically via MmGetSystemRoutineAddress.
|
||||
*/
|
||||
typedef PETHREAD (NTAPI *PsGetNextProcessThread_t)(PEPROCESS Process, PETHREAD Thread);
|
||||
static PsGetNextProcessThread_t g_PsGetNextProcessThread = NULL;
|
||||
|
||||
/* ── Manual structure definitions (kernel-mode) ─────────────────── */
|
||||
/* These are partially opaque in WDK headers; define just the offsets we need. */
|
||||
|
||||
typedef struct _MEMORY_BASIC_INFORMATION_KM {
|
||||
PVOID BaseAddress;
|
||||
PVOID AllocationBase;
|
||||
ULONG AllocationProtect;
|
||||
SIZE_T RegionSize;
|
||||
ULONG State;
|
||||
ULONG Protect;
|
||||
ULONG Type;
|
||||
} MEMORY_BASIC_INFORMATION_KM;
|
||||
|
||||
#define MEM_COMMIT_KM 0x1000
|
||||
|
||||
/* PEB.Ldr minimal definition for module enumeration */
|
||||
typedef struct _PEB_LDR_DATA_KM {
|
||||
UCHAR Reserved1[8];
|
||||
PVOID Reserved2[3];
|
||||
LIST_ENTRY InLoadOrderModuleList;
|
||||
} PEB_LDR_DATA_KM;
|
||||
|
||||
/* PEB minimal: only need Ldr at offset 0x18 (x64) */
|
||||
typedef struct _PEB_KM {
|
||||
UCHAR Reserved1[2];
|
||||
UCHAR BeingDebugged;
|
||||
UCHAR Reserved2[0x15];
|
||||
PEB_LDR_DATA_KM* Ldr; /* offset 0x18 on x64 */
|
||||
} PEB_KM;
|
||||
|
||||
/* LDR_DATA_TABLE_ENTRY minimal for walking InLoadOrderModuleList */
|
||||
typedef struct _LDR_DATA_TABLE_ENTRY_KM {
|
||||
LIST_ENTRY InLoadOrderLinks; /* offset 0x00 */
|
||||
LIST_ENTRY InMemoryOrderLinks; /* offset 0x10 */
|
||||
LIST_ENTRY InInitializationOrderLinks; /* offset 0x20 */
|
||||
PVOID DllBase; /* offset 0x30 */
|
||||
PVOID EntryPoint; /* offset 0x38 */
|
||||
ULONG SizeOfImage; /* offset 0x40 */
|
||||
ULONG _pad;
|
||||
UNICODE_STRING FullDllName; /* offset 0x48 */
|
||||
UNICODE_STRING BaseDllName; /* offset 0x58 */
|
||||
} LDR_DATA_TABLE_ENTRY_KM;
|
||||
|
||||
/* ── Forward declarations ────────────────────────────────────────── */
|
||||
|
||||
static NTSTATUS DispatchCreateClose(PDEVICE_OBJECT dev, PIRP irp);
|
||||
static NTSTATUS DispatchIoctl(PDEVICE_OBJECT dev, PIRP irp);
|
||||
DRIVER_UNLOAD DriverUnload;
|
||||
|
||||
/* ZwCurrentProcess() macro for ZwQueryVirtualMemory */
|
||||
#ifndef ZwCurrentProcess
|
||||
#define ZwCurrentProcess() ((HANDLE)(LONG_PTR)-1)
|
||||
#endif
|
||||
|
||||
/* ── Helpers ─────────────────────────────────────────────────────── */
|
||||
|
||||
#define VALIDATE_INPUT(irp, stk, T) \
|
||||
do { \
|
||||
if ((stk)->Parameters.DeviceIoControl.InputBufferLength < sizeof(T)) { \
|
||||
(irp)->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; \
|
||||
(irp)->IoStatus.Information = 0; \
|
||||
IoCompleteRequest((irp), IO_NO_INCREMENT); \
|
||||
return STATUS_BUFFER_TOO_SMALL; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define VALIDATE_OUTPUT(irp, stk, minSize) \
|
||||
do { \
|
||||
if ((stk)->Parameters.DeviceIoControl.OutputBufferLength < (ULONG)(minSize)) { \
|
||||
(irp)->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; \
|
||||
(irp)->IoStatus.Information = 0; \
|
||||
IoCompleteRequest((irp), IO_NO_INCREMENT); \
|
||||
return STATUS_BUFFER_TOO_SMALL; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static NTSTATUS LookupProcess(ULONG pid, PEPROCESS* proc)
|
||||
{
|
||||
return PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)pid, proc);
|
||||
}
|
||||
|
||||
/* ── Safe physical mapping (MDL-based, avoids MmMapIoSpace BSOD) ── */
|
||||
/*
|
||||
* MmMapIoSpace/MmUnmapIoSpace BSODs (bugcheck 0x50 in
|
||||
* MiClearMappingAndDereferenceIoSpace) when used on RAM-backed physical
|
||||
* addresses. MDL-based mapping is safe for both RAM and MMIO.
|
||||
*
|
||||
* CRITICAL: cacheType must match the existing kernel mapping of the page.
|
||||
* Use MmCached for RAM pages (already mapped cached by the kernel).
|
||||
* Use MmNonCached ONLY for MMIO/device registers.
|
||||
* Mismatched cache attributes (e.g. MmNonCached on RAM) cause silent
|
||||
* kernel memory corruption via CPU cache coherency conflicts.
|
||||
*/
|
||||
|
||||
typedef struct { PMDL mdl; PVOID base; } PHYS_MAP_CTX;
|
||||
|
||||
static PVOID MapPhysical(uint64_t physAddr, SIZE_T size,
|
||||
MEMORY_CACHING_TYPE cacheType, PHYS_MAP_CTX* ctx)
|
||||
{
|
||||
ctx->mdl = NULL;
|
||||
ctx->base = NULL;
|
||||
|
||||
ULONG_PTR pageOff = (ULONG_PTR)(physAddr & (PAGE_SIZE - 1));
|
||||
SIZE_T totalSize = pageOff + size;
|
||||
ULONG pages = (ULONG)((totalSize + PAGE_SIZE - 1) / PAGE_SIZE);
|
||||
|
||||
PMDL mdl = IoAllocateMdl(NULL, (ULONG)totalSize, FALSE, FALSE, NULL);
|
||||
if (!mdl) return NULL;
|
||||
|
||||
PPFN_NUMBER pfn = MmGetMdlPfnArray(mdl);
|
||||
PFN_NUMBER startPfn = (PFN_NUMBER)(physAddr / PAGE_SIZE);
|
||||
for (ULONG i = 0; i < pages; i++)
|
||||
pfn[i] = startPfn + i;
|
||||
mdl->MdlFlags |= MDL_PAGES_LOCKED;
|
||||
|
||||
__try {
|
||||
ctx->base = MmMapLockedPagesSpecifyCache(
|
||||
mdl, KernelMode, cacheType, NULL, FALSE, NormalPagePriority);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
IoFreeMdl(mdl);
|
||||
return NULL;
|
||||
}
|
||||
if (!ctx->base) { IoFreeMdl(mdl); return NULL; }
|
||||
|
||||
ctx->mdl = mdl;
|
||||
return (PUCHAR)ctx->base + pageOff;
|
||||
}
|
||||
|
||||
static void UnmapPhysical(PHYS_MAP_CTX* ctx)
|
||||
{
|
||||
if (ctx->base) MmUnmapLockedPages(ctx->base, ctx->mdl);
|
||||
if (ctx->mdl) IoFreeMdl(ctx->mdl);
|
||||
ctx->base = NULL;
|
||||
ctx->mdl = NULL;
|
||||
}
|
||||
|
||||
/* ── Virtual memory read ─────────────────────────────────────────── */
|
||||
|
||||
static NTSTATUS HandleReadMemory(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
VALIDATE_INPUT(irp, stk, struct RcxDrvReadRequest);
|
||||
|
||||
struct RcxDrvReadRequest* req = (struct RcxDrvReadRequest*)irp->AssociatedIrp.SystemBuffer;
|
||||
if (req->length == 0 || req->length > RCX_DRV_MAX_VIRTUAL)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
|
||||
VALIDATE_OUTPUT(irp, stk, req->length);
|
||||
|
||||
/* Save request fields before MmCopyVirtualMemory overwrites SystemBuffer.
|
||||
* METHOD_BUFFERED aliases input and output to the same buffer, so the
|
||||
* copy destination (SystemBuffer) clobbers req->* fields. */
|
||||
ULONG pid = req->pid;
|
||||
uint64_t address = req->address;
|
||||
ULONG length = req->length;
|
||||
|
||||
PEPROCESS proc = NULL;
|
||||
NTSTATUS st = LookupProcess(pid, &proc);
|
||||
if (!NT_SUCCESS(st)) return st;
|
||||
|
||||
SIZE_T bytesRead = 0;
|
||||
st = MmCopyVirtualMemory(
|
||||
proc, (PVOID)address,
|
||||
PsGetCurrentProcess(), irp->AssociatedIrp.SystemBuffer,
|
||||
(SIZE_T)length, KernelMode, &bytesRead);
|
||||
|
||||
ObDereferenceObject(proc);
|
||||
|
||||
/* Partial reads: zero remainder, report success */
|
||||
if (st == STATUS_PARTIAL_COPY) {
|
||||
RtlZeroMemory((PUCHAR)irp->AssociatedIrp.SystemBuffer + bytesRead,
|
||||
length - bytesRead);
|
||||
irp->IoStatus.Information = length;
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
irp->IoStatus.Information = NT_SUCCESS(st) ? length : 0;
|
||||
return st;
|
||||
}
|
||||
|
||||
/* ── Virtual memory write ────────────────────────────────────────── */
|
||||
|
||||
static NTSTATUS HandleWriteMemory(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
ULONG inputLen = stk->Parameters.DeviceIoControl.InputBufferLength;
|
||||
if (inputLen < sizeof(struct RcxDrvWriteRequest))
|
||||
return STATUS_BUFFER_TOO_SMALL;
|
||||
|
||||
struct RcxDrvWriteRequest* req = (struct RcxDrvWriteRequest*)irp->AssociatedIrp.SystemBuffer;
|
||||
if (req->length == 0 || req->length > RCX_DRV_MAX_VIRTUAL)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
if (inputLen < sizeof(struct RcxDrvWriteRequest) + req->length)
|
||||
return STATUS_BUFFER_TOO_SMALL;
|
||||
|
||||
PEPROCESS proc = NULL;
|
||||
NTSTATUS st = LookupProcess(req->pid, &proc);
|
||||
if (!NT_SUCCESS(st)) return st;
|
||||
|
||||
PUCHAR data = (PUCHAR)req + sizeof(struct RcxDrvWriteRequest);
|
||||
SIZE_T bytesWritten = 0;
|
||||
st = MmCopyVirtualMemory(
|
||||
PsGetCurrentProcess(), data,
|
||||
proc, (PVOID)req->address,
|
||||
(SIZE_T)req->length, KernelMode, &bytesWritten);
|
||||
|
||||
ObDereferenceObject(proc);
|
||||
irp->IoStatus.Information = 0;
|
||||
return st;
|
||||
}
|
||||
|
||||
/* ── Physical memory read ────────────────────────────────────────── */
|
||||
|
||||
static NTSTATUS HandleReadPhys(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
VALIDATE_INPUT(irp, stk, struct RcxDrvPhysReadRequest);
|
||||
|
||||
struct RcxDrvPhysReadRequest* req = (struct RcxDrvPhysReadRequest*)irp->AssociatedIrp.SystemBuffer;
|
||||
if (req->length == 0 || req->length > RCX_DRV_MAX_PHYSICAL)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
if (req->width != 0 && req->width != 1 && req->width != 2 && req->width != 4)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
|
||||
VALIDATE_OUTPUT(irp, stk, req->length);
|
||||
|
||||
/* Save request fields before SystemBuffer is overwritten (METHOD_BUFFERED
|
||||
* aliases input and output to the same buffer). */
|
||||
uint64_t physAddress = req->physAddress;
|
||||
ULONG length = req->length;
|
||||
ULONG width = req->width;
|
||||
|
||||
PUCHAR dst = (PUCHAR)irp->AssociatedIrp.SystemBuffer;
|
||||
|
||||
if (width == 0) {
|
||||
/* Byte copy -- use MmCopyMemory (safe for both RAM and MMIO) */
|
||||
MM_COPY_ADDRESS srcAddr;
|
||||
srcAddr.PhysicalAddress.QuadPart = (LONGLONG)physAddress;
|
||||
SIZE_T bytesCopied = 0;
|
||||
NTSTATUS st = MmCopyMemory(dst, srcAddr, (SIZE_T)length,
|
||||
MM_COPY_MEMORY_PHYSICAL, &bytesCopied);
|
||||
if (!NT_SUCCESS(st)) return st;
|
||||
if (bytesCopied < length)
|
||||
RtlZeroMemory(dst + bytesCopied, length - bytesCopied);
|
||||
irp->IoStatus.Information = length;
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* Width-aware MMIO reads -- map via MDL (safe for all physical addresses).
|
||||
* Use MmNonCached: width>0 implies MMIO register access where uncached
|
||||
* semantics are required for correct device interaction. */
|
||||
PHYS_MAP_CTX mapCtx;
|
||||
PUCHAR src = (PUCHAR)MapPhysical(physAddress, (SIZE_T)length, MmNonCached, &mapCtx);
|
||||
if (!src) return STATUS_UNSUCCESSFUL;
|
||||
|
||||
__try {
|
||||
ULONG off = 0;
|
||||
while (off + width <= length) {
|
||||
if (width == 1)
|
||||
dst[off] = READ_REGISTER_UCHAR(&src[off]);
|
||||
else if (width == 2)
|
||||
*(USHORT*)(dst + off) = READ_REGISTER_USHORT((PUSHORT)(src + off));
|
||||
else
|
||||
*(ULONG*)(dst + off) = READ_REGISTER_ULONG((PULONG)(src + off));
|
||||
off += width;
|
||||
}
|
||||
if (off < length)
|
||||
RtlZeroMemory(dst + off, length - off);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
UnmapPhysical(&mapCtx);
|
||||
return STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
UnmapPhysical(&mapCtx);
|
||||
irp->IoStatus.Information = length;
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* ── Physical memory write ───────────────────────────────────────── */
|
||||
|
||||
static NTSTATUS HandleWritePhys(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
ULONG inputLen = stk->Parameters.DeviceIoControl.InputBufferLength;
|
||||
if (inputLen < sizeof(struct RcxDrvPhysWriteRequest))
|
||||
return STATUS_BUFFER_TOO_SMALL;
|
||||
|
||||
struct RcxDrvPhysWriteRequest* req = (struct RcxDrvPhysWriteRequest*)irp->AssociatedIrp.SystemBuffer;
|
||||
if (req->length == 0 || req->length > RCX_DRV_MAX_PHYSICAL)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
if (req->width != 0 && req->width != 1 && req->width != 2 && req->width != 4)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
if (inputLen < sizeof(struct RcxDrvPhysWriteRequest) + req->length)
|
||||
return STATUS_BUFFER_TOO_SMALL;
|
||||
|
||||
PUCHAR src = (PUCHAR)req + sizeof(struct RcxDrvPhysWriteRequest);
|
||||
|
||||
/* Map via MDL (safe for both RAM and MMIO).
|
||||
* width==0 → RAM byte write (MmCached to avoid cache attribute conflict).
|
||||
* width>0 → MMIO register write (MmNonCached for correct device semantics). */
|
||||
MEMORY_CACHING_TYPE ct = (req->width == 0) ? MmCached : MmNonCached;
|
||||
PHYS_MAP_CTX mapCtx;
|
||||
PUCHAR dst = (PUCHAR)MapPhysical(req->physAddress, (SIZE_T)req->length, ct, &mapCtx);
|
||||
if (!dst) return STATUS_UNSUCCESSFUL;
|
||||
|
||||
__try {
|
||||
if (req->width == 0) {
|
||||
RtlCopyMemory(dst, src, req->length);
|
||||
} else {
|
||||
ULONG off = 0;
|
||||
while (off + req->width <= req->length) {
|
||||
if (req->width == 1)
|
||||
WRITE_REGISTER_UCHAR(&dst[off], src[off]);
|
||||
else if (req->width == 2)
|
||||
WRITE_REGISTER_USHORT((PUSHORT)(dst + off), *(USHORT*)(src + off));
|
||||
else
|
||||
WRITE_REGISTER_ULONG((PULONG)(dst + off), *(ULONG*)(src + off));
|
||||
off += req->width;
|
||||
}
|
||||
}
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
UnmapPhysical(&mapCtx);
|
||||
return STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
UnmapPhysical(&mapCtx);
|
||||
irp->IoStatus.Information = 0;
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* ── Ping ────────────────────────────────────────────────────────── */
|
||||
|
||||
static NTSTATUS HandlePing(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvPingResponse));
|
||||
|
||||
struct RcxDrvPingResponse* rsp = (struct RcxDrvPingResponse*)irp->AssociatedIrp.SystemBuffer;
|
||||
rsp->version = RCX_DRV_VERSION;
|
||||
rsp->driverBuild = __LINE__;
|
||||
irp->IoStatus.Information = sizeof(struct RcxDrvPingResponse);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* ── Query PEB ───────────────────────────────────────────────────── */
|
||||
|
||||
static NTSTATUS HandleQueryPeb(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryPebRequest);
|
||||
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvQueryPebResponse));
|
||||
|
||||
struct RcxDrvQueryPebRequest* req = (struct RcxDrvQueryPebRequest*)irp->AssociatedIrp.SystemBuffer;
|
||||
struct RcxDrvQueryPebResponse* rsp = (struct RcxDrvQueryPebResponse*)irp->AssociatedIrp.SystemBuffer;
|
||||
|
||||
PEPROCESS proc = NULL;
|
||||
NTSTATUS st = LookupProcess(req->pid, &proc);
|
||||
if (!NT_SUCCESS(st)) return st;
|
||||
|
||||
rsp->pebAddress = (uint64_t)(ULONG_PTR)PsGetProcessPeb(proc);
|
||||
rsp->pointerSize = 8;
|
||||
rsp->_pad = 0;
|
||||
|
||||
/* Detect WoW64 (32-bit process on 64-bit OS) */
|
||||
PVOID wow64 = PsGetProcessWow64Process(proc);
|
||||
if (wow64) {
|
||||
rsp->pebAddress = (uint64_t)(ULONG_PTR)wow64;
|
||||
rsp->pointerSize = 4;
|
||||
}
|
||||
|
||||
ObDereferenceObject(proc);
|
||||
irp->IoStatus.Information = sizeof(struct RcxDrvQueryPebResponse);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* ── Query Regions ───────────────────────────────────────────────── */
|
||||
|
||||
static NTSTATUS HandleQueryRegions(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryRegionsRequest);
|
||||
|
||||
struct RcxDrvQueryRegionsRequest* req = (struct RcxDrvQueryRegionsRequest*)irp->AssociatedIrp.SystemBuffer;
|
||||
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
|
||||
ULONG maxEntries = outputLen / sizeof(struct RcxDrvRegionEntry);
|
||||
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
|
||||
|
||||
PEPROCESS proc = NULL;
|
||||
NTSTATUS st = LookupProcess(req->pid, &proc);
|
||||
if (!NT_SUCCESS(st)) return st;
|
||||
|
||||
/* Attach to target process to query its address space.
|
||||
* IOCTLs arrive at PASSIVE_LEVEL; KeStackAttachProcess requires <= APC_LEVEL.
|
||||
* ZwQueryVirtualMemory with ZwCurrentProcess() while attached queries the
|
||||
* attached process's address space (correct). */
|
||||
KAPC_STATE apcState;
|
||||
KeStackAttachProcess(proc, &apcState);
|
||||
|
||||
struct RcxDrvRegionEntry* entries = (struct RcxDrvRegionEntry*)irp->AssociatedIrp.SystemBuffer;
|
||||
ULONG count = 0;
|
||||
PVOID addr = NULL;
|
||||
MEMORY_BASIC_INFORMATION_KM mbi;
|
||||
|
||||
while (count < maxEntries) {
|
||||
SIZE_T retLen = 0;
|
||||
st = ZwQueryVirtualMemory(ZwCurrentProcess(), addr, 0 /*MemoryBasicInformation*/,
|
||||
&mbi, sizeof(mbi), &retLen);
|
||||
if (!NT_SUCCESS(st)) break;
|
||||
|
||||
if (mbi.State == MEM_COMMIT_KM) {
|
||||
entries[count].base = (uint64_t)(ULONG_PTR)mbi.BaseAddress;
|
||||
entries[count].size = (uint64_t)mbi.RegionSize;
|
||||
entries[count].protect = mbi.Protect;
|
||||
entries[count].state = mbi.State;
|
||||
count++;
|
||||
}
|
||||
|
||||
ULONG_PTR next = (ULONG_PTR)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (next <= (ULONG_PTR)addr) break; /* overflow */
|
||||
addr = (PVOID)next;
|
||||
}
|
||||
|
||||
KeUnstackDetachProcess(&apcState);
|
||||
ObDereferenceObject(proc);
|
||||
|
||||
irp->IoStatus.Information = count * sizeof(struct RcxDrvRegionEntry);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* ── Query Modules ───────────────────────────────────────────────── */
|
||||
|
||||
static NTSTATUS HandleQueryModules(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryModulesRequest);
|
||||
|
||||
struct RcxDrvQueryModulesRequest* req = (struct RcxDrvQueryModulesRequest*)irp->AssociatedIrp.SystemBuffer;
|
||||
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
|
||||
ULONG maxEntries = outputLen / sizeof(struct RcxDrvModuleEntry);
|
||||
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
|
||||
|
||||
PEPROCESS proc = NULL;
|
||||
NTSTATUS st = LookupProcess(req->pid, &proc);
|
||||
if (!NT_SUCCESS(st)) return st;
|
||||
|
||||
/* Attach to target process to read PEB->Ldr */
|
||||
KAPC_STATE apcState;
|
||||
KeStackAttachProcess(proc, &apcState);
|
||||
|
||||
struct RcxDrvModuleEntry* entries = (struct RcxDrvModuleEntry*)irp->AssociatedIrp.SystemBuffer;
|
||||
ULONG count = 0;
|
||||
|
||||
__try {
|
||||
/* Read PEB address */
|
||||
PEB_KM* peb = (PEB_KM*)PsGetProcessPeb(proc);
|
||||
if (!peb) goto done;
|
||||
ProbeForRead(peb, sizeof(PEB_KM), 1);
|
||||
|
||||
/* PEB->Ldr at offset 0x18 (x64) */
|
||||
PEB_LDR_DATA_KM* ldr = peb->Ldr;
|
||||
if (!ldr) goto done;
|
||||
ProbeForRead(ldr, sizeof(PEB_LDR_DATA_KM), 1);
|
||||
|
||||
/* Walk InLoadOrderModuleList */
|
||||
LIST_ENTRY* head = &ldr->InLoadOrderModuleList;
|
||||
LIST_ENTRY* cur = head->Flink;
|
||||
|
||||
while (cur != head && count < maxEntries) {
|
||||
LDR_DATA_TABLE_ENTRY_KM* entry = CONTAINING_RECORD(cur, LDR_DATA_TABLE_ENTRY_KM, InLoadOrderLinks);
|
||||
|
||||
entries[count].base = (uint64_t)(ULONG_PTR)entry->DllBase;
|
||||
entries[count].size = (uint64_t)entry->SizeOfImage;
|
||||
|
||||
/* Copy wide-char name (truncate to 259 chars + null) */
|
||||
USHORT nameLen = entry->BaseDllName.Length / sizeof(WCHAR);
|
||||
if (nameLen > 259) nameLen = 259;
|
||||
if (entry->BaseDllName.Buffer) {
|
||||
RtlCopyMemory(entries[count].name, entry->BaseDllName.Buffer,
|
||||
nameLen * sizeof(uint16_t));
|
||||
}
|
||||
entries[count].name[nameLen] = 0;
|
||||
|
||||
count++;
|
||||
cur = cur->Flink;
|
||||
}
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
/* Partial results are fine */
|
||||
}
|
||||
|
||||
done:
|
||||
KeUnstackDetachProcess(&apcState);
|
||||
ObDereferenceObject(proc);
|
||||
|
||||
irp->IoStatus.Information = count * sizeof(struct RcxDrvModuleEntry);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* ── Query TEBs ──────────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* Walk the target process's thread list to collect TEB addresses.
|
||||
* Uses PsGetNextProcessThread (undocumented but stable since Vista).
|
||||
*/
|
||||
static NTSTATUS HandleQueryTebs(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryTebsRequest);
|
||||
|
||||
struct RcxDrvQueryTebsRequest* req = (struct RcxDrvQueryTebsRequest*)irp->AssociatedIrp.SystemBuffer;
|
||||
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
|
||||
ULONG maxEntries = outputLen / sizeof(struct RcxDrvTebEntry);
|
||||
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
|
||||
|
||||
PEPROCESS proc = NULL;
|
||||
NTSTATUS st = LookupProcess(req->pid, &proc);
|
||||
if (!NT_SUCCESS(st)) return st;
|
||||
|
||||
struct RcxDrvTebEntry* entries = (struct RcxDrvTebEntry*)irp->AssociatedIrp.SystemBuffer;
|
||||
ULONG count = 0;
|
||||
|
||||
if (!g_PsGetNextProcessThread) {
|
||||
ObDereferenceObject(proc);
|
||||
return STATUS_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/* PsGetNextProcessThread increments the ref on the returned PETHREAD and
|
||||
* dereferences the previous one. We must release the last thread if we
|
||||
* exit the loop early (exception or maxEntries hit). */
|
||||
{
|
||||
PETHREAD thread = NULL;
|
||||
__try {
|
||||
while ((thread = g_PsGetNextProcessThread(proc, thread)) != NULL) {
|
||||
if (count >= maxEntries) {
|
||||
/* Hit limit — release the thread PsGetNextProcessThread just returned */
|
||||
ObDereferenceObject(thread);
|
||||
break;
|
||||
}
|
||||
PVOID teb = PsGetThreadTeb(thread);
|
||||
if (teb) {
|
||||
entries[count].tebAddress = (uint64_t)(ULONG_PTR)teb;
|
||||
entries[count].threadId = (uint32_t)(ULONG_PTR)PsGetThreadId(thread);
|
||||
entries[count]._pad = 0;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
/* Exception mid-iteration: thread holds a referenced PETHREAD — release it */
|
||||
if (thread)
|
||||
ObDereferenceObject(thread);
|
||||
}
|
||||
}
|
||||
|
||||
ObDereferenceObject(proc);
|
||||
|
||||
irp->IoStatus.Information = count * sizeof(struct RcxDrvTebEntry);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* ── Read CR3 (DirectoryTableBase) ────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* EPROCESS.DirectoryTableBase offset. Stable across Win10/11 x64.
|
||||
* Verified: 0x028 on 1507-22H2+ (KPROCESS is at offset 0 of EPROCESS).
|
||||
*/
|
||||
#define KPROCESS_DIRECTORY_TABLE_BASE 0x028
|
||||
|
||||
static NTSTATUS HandleReadCr3(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
VALIDATE_INPUT(irp, stk, struct RcxDrvReadCr3Request);
|
||||
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvReadCr3Response));
|
||||
|
||||
struct RcxDrvReadCr3Request* req = (struct RcxDrvReadCr3Request*)irp->AssociatedIrp.SystemBuffer;
|
||||
struct RcxDrvReadCr3Response* rsp = (struct RcxDrvReadCr3Response*)irp->AssociatedIrp.SystemBuffer;
|
||||
|
||||
PEPROCESS proc = NULL;
|
||||
NTSTATUS st = LookupProcess(req->pid, &proc);
|
||||
if (!NT_SUCCESS(st)) return st;
|
||||
|
||||
__try {
|
||||
rsp->cr3 = *(uint64_t*)((PUCHAR)proc + KPROCESS_DIRECTORY_TABLE_BASE);
|
||||
/* Mask off PCID bits (bits 0-11) to get the PML4 physical address */
|
||||
rsp->cr3 &= ~0xFFFULL;
|
||||
rsp->kernelCr3 = rsp->cr3; /* same on non-KPTI; KPTI shadow is not easily accessible */
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
ObDereferenceObject(proc);
|
||||
return STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
ObDereferenceObject(proc);
|
||||
irp->IoStatus.Information = sizeof(struct RcxDrvReadCr3Response);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* ── Virtual-to-Physical address translation ─────────────────────── */
|
||||
|
||||
/* NOTE: This walks the page table non-atomically via 4 sequential physical reads.
|
||||
* The page table can be modified between reads (e.g., page-out, remap). This is
|
||||
* an inherent limitation shared by WinDbg's !vtop and similar tools. For a
|
||||
* debugging/reversing tool this tradeoff is acceptable. */
|
||||
|
||||
/* Extract physical frame address from a page table entry (bits 51:12) */
|
||||
#define PTE_FRAME(pte) ((pte) & 0x000FFFFFFFFFF000ULL)
|
||||
/* Check Present bit (bit 0) */
|
||||
#define PTE_PRESENT(pte) ((pte) & 1ULL)
|
||||
/* Check Page Size bit (bit 7) -- indicates large/huge page */
|
||||
#define PTE_PS(pte) ((pte) & (1ULL << 7))
|
||||
|
||||
static NTSTATUS HandleVtop(PIRP irp, PIO_STACK_LOCATION stk)
|
||||
{
|
||||
VALIDATE_INPUT(irp, stk, struct RcxDrvVtopRequest);
|
||||
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvVtopResponse));
|
||||
|
||||
struct RcxDrvVtopRequest* req = (struct RcxDrvVtopRequest*)irp->AssociatedIrp.SystemBuffer;
|
||||
struct RcxDrvVtopResponse* rsp = (struct RcxDrvVtopResponse*)irp->AssociatedIrp.SystemBuffer;
|
||||
|
||||
PEPROCESS proc = NULL;
|
||||
NTSTATUS st = LookupProcess(req->pid, &proc);
|
||||
if (!NT_SUCCESS(st)) return st;
|
||||
|
||||
/* Read CR3 */
|
||||
uint64_t cr3;
|
||||
__try {
|
||||
cr3 = *(uint64_t*)((PUCHAR)proc + KPROCESS_DIRECTORY_TABLE_BASE);
|
||||
cr3 &= ~0xFFFULL;
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
ObDereferenceObject(proc);
|
||||
return STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
ObDereferenceObject(proc);
|
||||
|
||||
uint64_t va = req->virtualAddress;
|
||||
RtlZeroMemory(rsp, sizeof(*rsp));
|
||||
|
||||
/* Extract indices from virtual address:
|
||||
* [47:39] = PML4 index, [38:30] = PDPT index,
|
||||
* [29:21] = PD index, [20:12] = PT index,
|
||||
* [11:0] = page offset */
|
||||
ULONG pml4Idx = (ULONG)((va >> 39) & 0x1FF);
|
||||
ULONG pdptIdx = (ULONG)((va >> 30) & 0x1FF);
|
||||
ULONG pdIdx = (ULONG)((va >> 21) & 0x1FF);
|
||||
ULONG ptIdx = (ULONG)((va >> 12) & 0x1FF);
|
||||
|
||||
MM_COPY_ADDRESS ca;
|
||||
SIZE_T copied;
|
||||
uint64_t entry;
|
||||
|
||||
/* Level 4: PML4 -- use MmCopyMemory (safe for RAM, unlike MmMapIoSpace) */
|
||||
ca.PhysicalAddress.QuadPart = (LONGLONG)(cr3 + pml4Idx * 8);
|
||||
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
|
||||
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
|
||||
rsp->pml4e = entry;
|
||||
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
|
||||
|
||||
/* Level 3: PDPT */
|
||||
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + pdptIdx * 8);
|
||||
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
|
||||
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
|
||||
rsp->pdpte = entry;
|
||||
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
|
||||
if (PTE_PS(entry)) {
|
||||
/* 1GB huge page: physical = frame[51:30] | va[29:0] */
|
||||
rsp->physicalAddress = (entry & 0x000FFFFFC0000000ULL) | (va & 0x3FFFFFFFULL);
|
||||
rsp->pageSize = 2;
|
||||
rsp->valid = 1;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Level 2: PD */
|
||||
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + pdIdx * 8);
|
||||
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
|
||||
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
|
||||
rsp->pde = entry;
|
||||
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
|
||||
if (PTE_PS(entry)) {
|
||||
/* 2MB large page: physical = frame[51:21] | va[20:0] */
|
||||
rsp->physicalAddress = (entry & 0x000FFFFFFFE00000ULL) | (va & 0x1FFFFFULL);
|
||||
rsp->pageSize = 1;
|
||||
rsp->valid = 1;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Level 1: PT */
|
||||
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + ptIdx * 8);
|
||||
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
|
||||
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
|
||||
rsp->pte = entry;
|
||||
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
|
||||
|
||||
/* 4KB page: physical = frame[51:12] | va[11:0] */
|
||||
rsp->physicalAddress = PTE_FRAME(entry) | (va & 0xFFFULL);
|
||||
rsp->pageSize = 0;
|
||||
rsp->valid = 1;
|
||||
|
||||
done:
|
||||
irp->IoStatus.Information = sizeof(struct RcxDrvVtopResponse);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* ── IOCTL dispatch ──────────────────────────────────────────────── */
|
||||
|
||||
static NTSTATUS DispatchIoctl(PDEVICE_OBJECT dev, PIRP irp)
|
||||
{
|
||||
UNREFERENCED_PARAMETER(dev);
|
||||
|
||||
PIO_STACK_LOCATION stk = IoGetCurrentIrpStackLocation(irp);
|
||||
NTSTATUS st;
|
||||
|
||||
switch (stk->Parameters.DeviceIoControl.IoControlCode) {
|
||||
case IOCTL_RCX_READ_MEMORY: st = HandleReadMemory(irp, stk); break;
|
||||
case IOCTL_RCX_WRITE_MEMORY: st = HandleWriteMemory(irp, stk); break;
|
||||
case IOCTL_RCX_QUERY_REGIONS: st = HandleQueryRegions(irp, stk); break;
|
||||
case IOCTL_RCX_QUERY_PEB: st = HandleQueryPeb(irp, stk); break;
|
||||
case IOCTL_RCX_QUERY_MODULES: st = HandleQueryModules(irp, stk); break;
|
||||
case IOCTL_RCX_QUERY_TEBS: st = HandleQueryTebs(irp, stk); break;
|
||||
case IOCTL_RCX_PING: st = HandlePing(irp, stk); break;
|
||||
case IOCTL_RCX_READ_PHYS: st = HandleReadPhys(irp, stk); break;
|
||||
case IOCTL_RCX_WRITE_PHYS: st = HandleWritePhys(irp, stk); break;
|
||||
case IOCTL_RCX_READ_CR3: st = HandleReadCr3(irp, stk); break;
|
||||
case IOCTL_RCX_VTOP: st = HandleVtop(irp, stk); break;
|
||||
default:
|
||||
st = STATUS_INVALID_DEVICE_REQUEST;
|
||||
irp->IoStatus.Information = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
irp->IoStatus.Status = st;
|
||||
IoCompleteRequest(irp, IO_NO_INCREMENT);
|
||||
return st;
|
||||
}
|
||||
|
||||
/* ── Create / Close (permit open/close) ──────────────────────────── */
|
||||
|
||||
static NTSTATUS DispatchCreateClose(PDEVICE_OBJECT dev, PIRP irp)
|
||||
{
|
||||
UNREFERENCED_PARAMETER(dev);
|
||||
irp->IoStatus.Status = STATUS_SUCCESS;
|
||||
irp->IoStatus.Information = 0;
|
||||
IoCompleteRequest(irp, IO_NO_INCREMENT);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/* ── Unload ──────────────────────────────────────────────────────── */
|
||||
|
||||
void DriverUnload(PDRIVER_OBJECT drv)
|
||||
{
|
||||
UNICODE_STRING symlink = RTL_CONSTANT_STRING(L"\\DosDevices\\RcxDrv");
|
||||
IoDeleteSymbolicLink(&symlink);
|
||||
if (drv->DeviceObject)
|
||||
IoDeleteDevice(drv->DeviceObject);
|
||||
}
|
||||
|
||||
/* ── Entry point ─────────────────────────────────────────────────── */
|
||||
|
||||
NTSTATUS DriverEntry(PDRIVER_OBJECT drv, PUNICODE_STRING regPath)
|
||||
{
|
||||
UNREFERENCED_PARAMETER(regPath);
|
||||
|
||||
/* Resolve undocumented APIs */
|
||||
UNICODE_STRING fnName = RTL_CONSTANT_STRING(L"PsGetNextProcessThread");
|
||||
g_PsGetNextProcessThread = (PsGetNextProcessThread_t)MmGetSystemRoutineAddress(&fnName);
|
||||
|
||||
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\RcxDrv");
|
||||
UNICODE_STRING symlink = RTL_CONSTANT_STRING(L"\\DosDevices\\RcxDrv");
|
||||
|
||||
PDEVICE_OBJECT devObj = NULL;
|
||||
NTSTATUS st = IoCreateDevice(drv, 0, &devName, FILE_DEVICE_UNKNOWN,
|
||||
FILE_DEVICE_SECURE_OPEN, FALSE, &devObj);
|
||||
if (!NT_SUCCESS(st)) return st;
|
||||
|
||||
st = IoCreateSymbolicLink(&symlink, &devName);
|
||||
if (!NT_SUCCESS(st)) {
|
||||
IoDeleteDevice(devObj);
|
||||
return st;
|
||||
}
|
||||
|
||||
drv->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose;
|
||||
drv->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
|
||||
drv->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctl;
|
||||
drv->DriverUnload = DriverUnload;
|
||||
|
||||
devObj->Flags |= DO_BUFFERED_IO;
|
||||
devObj->Flags &= ~DO_DEVICE_INITIALIZING;
|
||||
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
17
plugins/KernelMemory/linux/Makefile
Normal file
@@ -0,0 +1,17 @@
|
||||
obj-m += rcxkm.o
|
||||
|
||||
KDIR ?= /lib/modules/$(shell uname -r)/build
|
||||
|
||||
all:
|
||||
$(MAKE) -C $(KDIR) M=$(PWD) modules
|
||||
|
||||
clean:
|
||||
$(MAKE) -C $(KDIR) M=$(PWD) clean
|
||||
|
||||
install:
|
||||
insmod rcxkm.ko
|
||||
|
||||
uninstall:
|
||||
rmmod rcxkm
|
||||
|
||||
.PHONY: all clean install uninstall
|
||||
132
plugins/KernelMemory/linux/rcxkm.c
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* rcxkm.c -- Linux kernel module stub for Reclass kernel memory provider.
|
||||
*
|
||||
* Provides /dev/rcxkm char device with ioctl() dispatch using the same
|
||||
* protocol structs as the Windows driver (rcx_drv_protocol.h).
|
||||
*
|
||||
* Build: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
|
||||
*
|
||||
* TODO: implement handlers (currently returns -ENOSYS for all IOCTLs).
|
||||
*/
|
||||
|
||||
#include <linux/module.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/miscdevice.h>
|
||||
#include <linux/uaccess.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/sched.h>
|
||||
#include <linux/pid.h>
|
||||
#include <linux/mm.h>
|
||||
|
||||
#include "../rcx_drv_protocol.h"
|
||||
|
||||
#define DEVICE_NAME "rcxkm"
|
||||
|
||||
MODULE_LICENSE("GPL");
|
||||
MODULE_AUTHOR("Reclass");
|
||||
MODULE_DESCRIPTION("Reclass kernel memory provider (stub)");
|
||||
|
||||
/* ── IOCTL dispatch ─────────────────────────────────────────────────── */
|
||||
|
||||
static long rcxkm_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
|
||||
{
|
||||
(void)filp;
|
||||
(void)arg;
|
||||
|
||||
switch (cmd) {
|
||||
case IOCTL_RCX_READ_MEMORY:
|
||||
/* TODO: find_get_pid(pid) -> get_task_struct -> access_process_vm() */
|
||||
return -ENOSYS;
|
||||
|
||||
case IOCTL_RCX_WRITE_MEMORY:
|
||||
/* TODO: access_process_vm() with FOLL_WRITE */
|
||||
return -ENOSYS;
|
||||
|
||||
case IOCTL_RCX_QUERY_REGIONS:
|
||||
/* TODO: walk target mm->mmap via VMA iteration */
|
||||
return -ENOSYS;
|
||||
|
||||
case IOCTL_RCX_QUERY_PEB:
|
||||
/* N/A on Linux (no PEB); could return mm->start_brk or similar */
|
||||
return -ENOSYS;
|
||||
|
||||
case IOCTL_RCX_QUERY_MODULES:
|
||||
/* TODO: walk target /proc/pid/maps or mm VMAs */
|
||||
return -ENOSYS;
|
||||
|
||||
case IOCTL_RCX_QUERY_TEBS:
|
||||
/* N/A on Linux (no TEB) */
|
||||
return -ENOSYS;
|
||||
|
||||
case IOCTL_RCX_PING: {
|
||||
struct RcxDrvPingResponse resp = {
|
||||
.version = RCX_DRV_VERSION,
|
||||
.driverBuild = 1,
|
||||
};
|
||||
if (copy_to_user((void __user *)arg, &resp, sizeof(resp)))
|
||||
return -EFAULT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
case IOCTL_RCX_READ_PHYS:
|
||||
/* TODO: ioremap() + memcpy_fromio() */
|
||||
return -ENOSYS;
|
||||
|
||||
case IOCTL_RCX_WRITE_PHYS:
|
||||
/* TODO: ioremap() + memcpy_toio() */
|
||||
return -ENOSYS;
|
||||
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── File operations ────────────────────────────────────────────────── */
|
||||
|
||||
static int rcxkm_open(struct inode *inode, struct file *filp)
|
||||
{
|
||||
(void)inode; (void)filp;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rcxkm_release(struct inode *inode, struct file *filp)
|
||||
{
|
||||
(void)inode; (void)filp;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct file_operations rcxkm_fops = {
|
||||
.owner = THIS_MODULE,
|
||||
.unlocked_ioctl = rcxkm_ioctl,
|
||||
.open = rcxkm_open,
|
||||
.release = rcxkm_release,
|
||||
};
|
||||
|
||||
static struct miscdevice rcxkm_device = {
|
||||
.minor = MISC_DYNAMIC_MINOR,
|
||||
.name = DEVICE_NAME,
|
||||
.fops = &rcxkm_fops,
|
||||
};
|
||||
|
||||
/* ── Module init/exit ───────────────────────────────────────────────── */
|
||||
|
||||
static int __init rcxkm_init(void)
|
||||
{
|
||||
int ret = misc_register(&rcxkm_device);
|
||||
if (ret) {
|
||||
pr_err("rcxkm: failed to register misc device (err=%d)\n", ret);
|
||||
return ret;
|
||||
}
|
||||
pr_info("rcxkm: loaded, device /dev/%s\n", DEVICE_NAME);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void __exit rcxkm_exit(void)
|
||||
{
|
||||
misc_deregister(&rcxkm_device);
|
||||
pr_info("rcxkm: unloaded\n");
|
||||
}
|
||||
|
||||
module_init(rcxkm_init);
|
||||
module_exit(rcxkm_exit);
|
||||
189
plugins/KernelMemory/rcx_drv_protocol.h
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* RCX Driver Protocol -- shared between kernel driver and usermode plugin.
|
||||
* No dependencies beyond standard C headers. Pure C, no Windows types.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#ifdef KERNEL
|
||||
/* Kernel mode build: avoid stdint.h (not in WDK km/crt) */
|
||||
typedef unsigned char uint8_t;
|
||||
typedef unsigned short uint16_t;
|
||||
typedef unsigned int uint32_t;
|
||||
typedef unsigned __int64 uint64_t;
|
||||
typedef signed __int64 int64_t;
|
||||
#else
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
/* ── Device / service names ───────────────────────────────────────── */
|
||||
#define RCX_DRV_DEVICE_NAME L"\\Device\\RcxDrv"
|
||||
#define RCX_DRV_SYMLINK_NAME L"\\DosDevices\\RcxDrv"
|
||||
#define RCX_DRV_USERMODE_PATH "\\\\.\\RcxDrv"
|
||||
#define RCX_DRV_SERVICE_NAME "RcxDrv"
|
||||
|
||||
/* ── Protocol version ─────────────────────────────────────────────── */
|
||||
#define RCX_DRV_VERSION 1
|
||||
|
||||
/* ── Size limits ──────────────────────────────────────────────────── */
|
||||
#define RCX_DRV_MAX_VIRTUAL (1024 * 1024) /* 1 MB per virtual read/write */
|
||||
#define RCX_DRV_MAX_PHYSICAL 4096 /* 4 KB per physical read/write */
|
||||
|
||||
/* ── IOCTL codes ──────────────────────────────────────────────────── */
|
||||
/* CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, function, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0) */
|
||||
|
||||
/* Virtual memory (per-process) */
|
||||
#define IOCTL_RCX_READ_MEMORY 0x222000 /* function 0x800 */
|
||||
#define IOCTL_RCX_WRITE_MEMORY 0x222004 /* function 0x801 */
|
||||
#define IOCTL_RCX_QUERY_REGIONS 0x222008 /* function 0x802 */
|
||||
#define IOCTL_RCX_QUERY_PEB 0x22200C /* function 0x803 */
|
||||
#define IOCTL_RCX_QUERY_MODULES 0x222010 /* function 0x804 */
|
||||
#define IOCTL_RCX_QUERY_TEBS 0x222014 /* function 0x805 */
|
||||
#define IOCTL_RCX_PING 0x222018 /* function 0x806 */
|
||||
|
||||
/* Physical memory (MMIO) */
|
||||
#define IOCTL_RCX_READ_PHYS 0x22201C /* function 0x807 */
|
||||
#define IOCTL_RCX_WRITE_PHYS 0x222020 /* function 0x808 */
|
||||
|
||||
/* Paging / address translation */
|
||||
#define IOCTL_RCX_READ_CR3 0x222044 /* function 0x811 */
|
||||
#define IOCTL_RCX_VTOP 0x222048 /* function 0x812 */
|
||||
|
||||
/* ── Request / Response structures ────────────────────────────────── */
|
||||
/* All structs are naturally aligned. Padding fields are explicit. */
|
||||
|
||||
/* -- Virtual memory -- */
|
||||
|
||||
struct RcxDrvReadRequest {
|
||||
uint32_t pid;
|
||||
uint32_t _pad0;
|
||||
uint64_t address;
|
||||
uint32_t length; /* max RCX_DRV_MAX_VIRTUAL */
|
||||
uint32_t _pad1;
|
||||
};
|
||||
|
||||
/* Write: input = header + inline data bytes */
|
||||
struct RcxDrvWriteRequest {
|
||||
uint32_t pid;
|
||||
uint32_t _pad0;
|
||||
uint64_t address;
|
||||
uint32_t length; /* max RCX_DRV_MAX_VIRTUAL */
|
||||
uint32_t _pad1;
|
||||
/* uint8_t data[length] follows */
|
||||
};
|
||||
|
||||
/* -- Region enumeration -- */
|
||||
|
||||
struct RcxDrvQueryRegionsRequest {
|
||||
uint32_t pid;
|
||||
uint32_t _pad;
|
||||
};
|
||||
|
||||
struct RcxDrvRegionEntry {
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
uint32_t protect; /* raw PAGE_* flags */
|
||||
uint32_t state; /* MEM_COMMIT etc. */
|
||||
};
|
||||
|
||||
/* -- PEB -- */
|
||||
|
||||
struct RcxDrvQueryPebRequest {
|
||||
uint32_t pid;
|
||||
uint32_t _pad;
|
||||
};
|
||||
|
||||
struct RcxDrvQueryPebResponse {
|
||||
uint64_t pebAddress;
|
||||
uint32_t pointerSize; /* 4 or 8 */
|
||||
uint32_t _pad;
|
||||
};
|
||||
|
||||
/* -- Modules -- */
|
||||
|
||||
struct RcxDrvQueryModulesRequest {
|
||||
uint32_t pid;
|
||||
uint32_t _pad;
|
||||
};
|
||||
|
||||
struct RcxDrvModuleEntry {
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
uint16_t name[260]; /* wide-char, null-terminated */
|
||||
};
|
||||
|
||||
/* -- TEBs -- */
|
||||
|
||||
struct RcxDrvQueryTebsRequest {
|
||||
uint32_t pid;
|
||||
uint32_t _pad;
|
||||
};
|
||||
|
||||
struct RcxDrvTebEntry {
|
||||
uint64_t tebAddress;
|
||||
uint32_t threadId;
|
||||
uint32_t _pad;
|
||||
};
|
||||
|
||||
/* -- Ping -- */
|
||||
|
||||
struct RcxDrvPingResponse {
|
||||
uint32_t version;
|
||||
uint32_t driverBuild;
|
||||
};
|
||||
|
||||
/* -- Physical memory -- */
|
||||
|
||||
struct RcxDrvPhysReadRequest {
|
||||
uint64_t physAddress;
|
||||
uint32_t length; /* max RCX_DRV_MAX_PHYSICAL */
|
||||
uint32_t width; /* access width: 1, 2, or 4 (0 = memcpy) */
|
||||
};
|
||||
|
||||
struct RcxDrvPhysWriteRequest {
|
||||
uint64_t physAddress;
|
||||
uint32_t length; /* max RCX_DRV_MAX_PHYSICAL */
|
||||
uint32_t width; /* access width: 1, 2, or 4 (0 = memcpy) */
|
||||
/* uint8_t data[length] follows */
|
||||
};
|
||||
|
||||
/* -- Paging / address translation -- */
|
||||
|
||||
struct RcxDrvReadCr3Request {
|
||||
uint32_t pid;
|
||||
uint32_t _pad;
|
||||
};
|
||||
|
||||
struct RcxDrvReadCr3Response {
|
||||
uint64_t cr3; /* DirectoryTableBase (PML4 physical address) */
|
||||
uint64_t kernelCr3; /* KernelDirectoryTableBase (KPTI shadow) */
|
||||
};
|
||||
|
||||
struct RcxDrvVtopRequest {
|
||||
uint32_t pid;
|
||||
uint32_t _pad;
|
||||
uint64_t virtualAddress;
|
||||
};
|
||||
|
||||
struct RcxDrvVtopResponse {
|
||||
uint64_t physicalAddress; /* final translated physical address (with page offset) */
|
||||
uint64_t pml4e; /* raw PML4 entry value */
|
||||
uint64_t pdpte; /* raw PDPT entry value */
|
||||
uint64_t pde; /* raw PD entry value */
|
||||
uint64_t pte; /* raw PT entry value (0 if large/huge page) */
|
||||
uint8_t pageSize; /* 0=4KB, 1=2MB, 2=1GB */
|
||||
uint8_t valid; /* 1 if translation succeeded, 0 if not present */
|
||||
uint8_t _pad2[6];
|
||||
};
|
||||
|
||||
/* ── Compile-time validation ──────────────────────────────────────── */
|
||||
#ifdef __cplusplus
|
||||
static_assert(sizeof(RcxDrvReadRequest) == 24, "ReadRequest layout");
|
||||
static_assert(sizeof(RcxDrvWriteRequest) == 24, "WriteRequest layout");
|
||||
static_assert(sizeof(RcxDrvRegionEntry) == 24, "RegionEntry layout");
|
||||
static_assert(sizeof(RcxDrvModuleEntry) == 536, "ModuleEntry layout");
|
||||
static_assert(sizeof(RcxDrvTebEntry) == 16, "TebEntry layout");
|
||||
static_assert(sizeof(RcxDrvPingResponse) == 8, "PingResponse layout");
|
||||
static_assert(sizeof(RcxDrvReadCr3Response) == 16, "ReadCr3Response layout");
|
||||
static_assert(sizeof(RcxDrvVtopRequest) == 16, "VtopRequest layout");
|
||||
static_assert(sizeof(RcxDrvVtopResponse) == 48, "VtopResponse layout");
|
||||
#endif
|
||||
@@ -4,8 +4,7 @@ project(ProcessMemoryPlugin LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Find Qt
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets)
|
||||
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
@@ -24,10 +23,20 @@ set(PLUGIN_SOURCES
|
||||
add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
# Link Qt
|
||||
target_link_libraries(ProcessMemoryPlugin PRIVATE Qt6::Widgets)
|
||||
target_link_libraries(ProcessMemoryPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
|
||||
|
||||
# Platform-specific linking
|
||||
if(WIN32)
|
||||
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(ProcessMemoryPlugin PRIVATE -fvisibility=hidden)
|
||||
endif()
|
||||
|
||||
# Include directories
|
||||
target_include_directories(ProcessMemoryPlugin PRIVATE
|
||||
target_include_directories(ProcessMemoryPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
)
|
||||
|
||||
@@ -36,3 +45,12 @@ set_target_properties(ProcessMemoryPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
|
||||
if(APPLE AND TARGET Reclass)
|
||||
add_custom_command(TARGET ProcessMemoryPlugin POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/../PlugIns"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:ProcessMemoryPlugin>"
|
||||
"$<TARGET_FILE_DIR:Reclass>/../PlugIns/"
|
||||
COMMENT "Copying ProcessMemoryPlugin into Reclass.app/Contents/PlugIns")
|
||||
endif()
|
||||
|
||||
@@ -1,48 +1,73 @@
|
||||
#pragma once
|
||||
#include "../../src/iplugin.h"
|
||||
#include "../../src/core.h"
|
||||
#include <windows.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <psapi.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Windows process memory provider
|
||||
* Reads/writes memory from a live process using Win32 API
|
||||
* Process memory provider
|
||||
* Reads/writes memory from a live process using platform APIs
|
||||
*/
|
||||
class ProcessMemoryProvider : public rcx::Provider {
|
||||
class ProcessMemoryProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
ProcessMemoryProvider(DWORD pid, const QString& processName);
|
||||
ProcessMemoryProvider(uint32_t pid, const QString& processName);
|
||||
~ProcessMemoryProvider() override;
|
||||
|
||||
|
||||
// Required overrides
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
int size() const override { return m_handle ? INT_MAX : NULL; } // Process memory has no fixed size
|
||||
|
||||
int size() const override;
|
||||
|
||||
// Optional overrides
|
||||
bool write(uint64_t addr, const void* buf, int len) override;
|
||||
bool isWritable() const override { return m_writable; }
|
||||
QString name() const override { return m_processName; }
|
||||
QString kind() const override { return QStringLiteral("LocalProcess"); }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
|
||||
uint64_t symbolToAddress(const QString& name) const override;
|
||||
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
int pointerSize() const override { return m_pointerSize; }
|
||||
QVector<rcx::MemoryRegion> enumerateRegions() const override;
|
||||
bool isReadable(uint64_t, int len) const override {
|
||||
#ifdef _WIN32
|
||||
return m_handle && len >= 0;
|
||||
#elif defined(__linux__)
|
||||
return m_fd >= 0 && len >= 0;
|
||||
#elif defined(__APPLE__)
|
||||
return m_task != 0 && len >= 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Process-specific helpers
|
||||
DWORD pid() const { return m_pid; }
|
||||
uint64_t baseAddress() const { return m_base; }
|
||||
uint32_t pid() const { return m_pid; }
|
||||
void refreshModules() { m_modules.clear(); cacheModules(); }
|
||||
uint64_t peb() const override { return m_peb; }
|
||||
QVector<ThreadInfo> tebs() const override;
|
||||
QVector<ModuleEntry> enumerateModules() const override;
|
||||
|
||||
private:
|
||||
void cacheModules();
|
||||
|
||||
|
||||
private:
|
||||
HANDLE m_handle;
|
||||
DWORD m_pid;
|
||||
#ifdef _WIN32
|
||||
void* m_handle;
|
||||
#elif defined(__linux__)
|
||||
int m_fd;
|
||||
#elif defined(__APPLE__)
|
||||
uint32_t m_task;
|
||||
#endif
|
||||
uint32_t m_pid;
|
||||
QString m_processName;
|
||||
bool m_writable;
|
||||
uint64_t m_base;
|
||||
int m_pointerSize = 8;
|
||||
uint64_t m_peb = 0;
|
||||
|
||||
struct ModuleInfo {
|
||||
QString name;
|
||||
QString fullPath;
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
};
|
||||
@@ -52,24 +77,25 @@ private:
|
||||
/**
|
||||
* Plugin that provides ProcessMemoryProvider
|
||||
*/
|
||||
class ProcessMemoryPlugin : public IProviderPlugin {
|
||||
class ProcessMemoryPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
std::string Name() const override { return "Process Memory"; }
|
||||
std::string Version() const override { return "1.0.0"; }
|
||||
std::string Author() const override { return "ReclassX"; }
|
||||
std::string Description() const override { return "Read and write memory from local running Windows processes"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
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;
|
||||
|
||||
|
||||
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;
|
||||
|
||||
|
||||
// Optional: provide custom process list
|
||||
bool providesProcessList() const override { return true; }
|
||||
QVector<PluginProcessInfo> enumerateProcesses() override;
|
||||
};
|
||||
|
||||
// Plugin export
|
||||
extern "C" __declspec(dllexport) IPlugin* CreatePlugin();
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
#include "../../src/iplugin.h"
|
||||
#include "ReClassNET_Plugin.hpp"
|
||||
|
||||
#include <QLibrary>
|
||||
#include <memory>
|
||||
|
||||
#ifdef HAS_CLR_BRIDGE
|
||||
#include "ClrHost.h"
|
||||
#endif
|
||||
|
||||
/**
|
||||
* ReclassX plugin that loads ReClass.NET plugin DLLs
|
||||
* and exposes them as ReclassX providers.
|
||||
*
|
||||
* Supports both native DLLs (C exports) and, when built with
|
||||
* HAS_CLR_BRIDGE, managed .NET assemblies via in-process CLR hosting.
|
||||
*
|
||||
* Target string format: "dllpath|pid:processname"
|
||||
*/
|
||||
class RcNetCompatPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
// Plugin metadata
|
||||
std::string Name() const override { return "ReClass.NET Compat Layer"; }
|
||||
std::string Version() const override { return "1.0.0"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
std::string Description() const override {
|
||||
return "Loads ReClass.NET native and .NET plugin DLLs as Reclass data sources";
|
||||
}
|
||||
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
||||
QIcon Icon() const override;
|
||||
|
||||
// IProviderPlugin interface
|
||||
bool canHandle(const QString& target) const override;
|
||||
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
|
||||
uint64_t getInitialBaseAddress(const QString& target) const override;
|
||||
bool selectTarget(QWidget* parent, QString* target) override;
|
||||
|
||||
// Override process enumeration -- we enumerate via the loaded DLL
|
||||
bool providesProcessList() const override { return true; }
|
||||
QVector<PluginProcessInfo> enumerateProcesses() override;
|
||||
|
||||
private:
|
||||
bool loadPlugin(const QString& path, QString* errorMsg = nullptr);
|
||||
bool loadNativeDll(const QString& path, QString* errorMsg = nullptr);
|
||||
void unloadNativeDll();
|
||||
|
||||
#ifdef HAS_CLR_BRIDGE
|
||||
bool loadManagedDll(const QString& path, QString* errorMsg = nullptr);
|
||||
std::unique_ptr<ClrHost> m_clrHost;
|
||||
#endif
|
||||
|
||||
std::unique_ptr<QLibrary> m_lib;
|
||||
RcNetFunctions m_fns;
|
||||
QString m_dllPath;
|
||||
bool m_isManaged = false;
|
||||
};
|
||||
|
||||
// Plugin export
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||
132
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
#include "RcNetCompatProvider.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <cstring>
|
||||
|
||||
// -- Construction / destruction -------------------------------------------
|
||||
|
||||
RcNetCompatProvider::RcNetCompatProvider(const RcNetFunctions& fns,
|
||||
uint32_t pid,
|
||||
const QString& processName)
|
||||
: m_fns(fns)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
{
|
||||
if (m_fns.OpenRemoteProcess)
|
||||
m_handle = m_fns.OpenRemoteProcess(static_cast<RC_Size>(pid),
|
||||
ProcessAccess::Full);
|
||||
|
||||
if (m_handle)
|
||||
cacheModules();
|
||||
}
|
||||
|
||||
RcNetCompatProvider::~RcNetCompatProvider()
|
||||
{
|
||||
if (m_handle && m_fns.CloseRemoteProcess)
|
||||
m_fns.CloseRemoteProcess(m_handle);
|
||||
}
|
||||
|
||||
// -- Required overrides ---------------------------------------------------
|
||||
|
||||
bool RcNetCompatProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (!m_handle || !m_fns.ReadRemoteMemory || len <= 0)
|
||||
return false;
|
||||
|
||||
return m_fns.ReadRemoteMemory(m_handle,
|
||||
reinterpret_cast<RC_Pointer>(addr),
|
||||
static_cast<RC_Pointer>(buf),
|
||||
0, len);
|
||||
}
|
||||
|
||||
int RcNetCompatProvider::size() const
|
||||
{
|
||||
if (!m_handle) return 0;
|
||||
if (m_fns.IsProcessValid && !m_fns.IsProcessValid(m_handle)) return 0;
|
||||
return 0x10000;
|
||||
}
|
||||
|
||||
// -- Optional overrides ---------------------------------------------------
|
||||
|
||||
bool RcNetCompatProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (!m_handle || !m_fns.WriteRemoteMemory || len <= 0)
|
||||
return false;
|
||||
|
||||
return m_fns.WriteRemoteMemory(m_handle,
|
||||
reinterpret_cast<RC_Pointer>(addr),
|
||||
const_cast<RC_Pointer>(static_cast<const void*>(buf)),
|
||||
0, len);
|
||||
}
|
||||
|
||||
QString RcNetCompatProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules)
|
||||
{
|
||||
if (addr >= mod.base && addr < mod.base + mod.size)
|
||||
{
|
||||
uint64_t offset = addr - mod.base;
|
||||
return QStringLiteral("%1+0x%2")
|
||||
.arg(mod.name)
|
||||
.arg(offset, 0, 16, QChar('0'));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
uint64_t RcNetCompatProvider::symbolToAddress(const QString& name) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
|
||||
return mod.base;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -- Module enumeration ---------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// Thread-local collector for the module enumeration callback.
|
||||
// ReClass.NET callbacks are synchronous, so this is safe.
|
||||
struct ModuleCollector {
|
||||
QVector<RcNetCompatProvider::ModuleInfo>* dest = nullptr;
|
||||
};
|
||||
thread_local ModuleCollector g_moduleCollector;
|
||||
|
||||
void RC_CALLCONV moduleCallback(EnumerateRemoteModuleData* data)
|
||||
{
|
||||
if (!data || !g_moduleCollector.dest) return;
|
||||
|
||||
QString path = QString::fromUtf16(data->Path);
|
||||
QFileInfo fi(path);
|
||||
|
||||
RcNetCompatProvider::ModuleInfo info;
|
||||
info.name = fi.fileName();
|
||||
info.base = reinterpret_cast<uint64_t>(data->BaseAddress);
|
||||
info.size = static_cast<uint64_t>(data->Size);
|
||||
g_moduleCollector.dest->append(info);
|
||||
}
|
||||
|
||||
// We still need a section callback even though we don't use it.
|
||||
void RC_CALLCONV sectionCallback(EnumerateRemoteSectionData*)
|
||||
{
|
||||
// Intentionally empty -- we only need module data.
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void RcNetCompatProvider::cacheModules()
|
||||
{
|
||||
if (!m_fns.EnumerateRemoteSectionsAndModules || !m_handle)
|
||||
return;
|
||||
|
||||
m_modules.clear();
|
||||
g_moduleCollector.dest = &m_modules;
|
||||
m_fns.EnumerateRemoteSectionsAndModules(m_handle, sectionCallback, moduleCallback);
|
||||
g_moduleCollector.dest = nullptr;
|
||||
|
||||
// Set base to first module if we got any
|
||||
if (!m_modules.isEmpty() && m_base == 0)
|
||||
m_base = m_modules.first().base;
|
||||
}
|
||||
48
plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include "../../src/providers/provider.h"
|
||||
#include "ReClassNET_Plugin.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
/**
|
||||
* Provider that bridges ReClass.NET native plugin DLL calls
|
||||
* to the ReclassX Provider interface.
|
||||
*/
|
||||
class RcNetCompatProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
RcNetCompatProvider(const RcNetFunctions& fns, uint32_t pid,
|
||||
const QString& processName);
|
||||
~RcNetCompatProvider() override;
|
||||
|
||||
// Required overrides
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
int size() const override;
|
||||
|
||||
// Optional overrides
|
||||
bool write(uint64_t addr, const void* buf, int len) override;
|
||||
bool isWritable() const override { return m_fns.WriteRemoteMemory != nullptr; }
|
||||
QString name() const override { return m_processName; }
|
||||
QString kind() const override { return QStringLiteral("RcNet"); }
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
uint64_t symbolToAddress(const QString& name) const override;
|
||||
|
||||
struct ModuleInfo {
|
||||
QString name;
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
};
|
||||
|
||||
private:
|
||||
void cacheModules();
|
||||
|
||||
RcNetFunctions m_fns;
|
||||
RC_Pointer m_handle = nullptr;
|
||||
uint32_t m_pid;
|
||||
QString m_processName;
|
||||
uint64_t m_base = 0;
|
||||
QVector<ModuleInfo> m_modules;
|
||||
};
|
||||
140
plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp
Normal file
@@ -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
@@ -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
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<AssemblyName>RcNetBridge</AssemblyName>
|
||||
<RootNamespace>RcNetBridge</RootNamespace>
|
||||
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
124
plugins/RemoteProcessMemory/CMakeLists.txt
Normal file
@@ -0,0 +1,124 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(RemoteProcessMemory LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC OFF) # run uic manually to avoid dupbuild with ProcessMemoryPlugin
|
||||
|
||||
# ─── 1. Payload DLL/SO (no Qt, minimal dependencies) ────────────────
|
||||
|
||||
add_library(rcx_payload SHARED
|
||||
payload/rcx_payload.cpp
|
||||
rcx_rpc_protocol.h
|
||||
)
|
||||
|
||||
set_target_properties(rcx_payload PROPERTIES PREFIX "") # rcx_payload.dll / rcx_payload.so
|
||||
|
||||
target_include_directories(rcx_payload PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(rcx_payload PRIVATE psapi)
|
||||
else()
|
||||
target_link_libraries(rcx_payload PRIVATE pthread rt)
|
||||
target_compile_options(rcx_payload PRIVATE -fvisibility=hidden)
|
||||
endif()
|
||||
|
||||
# Output payload to Plugins/ (same dir as plugin DLL, discovered at runtime)
|
||||
set_target_properties(rcx_payload PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
|
||||
# Install rule: copy both DLLs to install Plugins/ folder
|
||||
install(TARGETS rcx_payload
|
||||
LIBRARY DESTINATION Plugins
|
||||
RUNTIME DESTINATION Plugins
|
||||
)
|
||||
|
||||
# ─── 2. Plugin DLL (Qt, implements IProviderPlugin) ──────────────────
|
||||
|
||||
# Generate ui_processpicker.h in our own build dir (avoids dupbuild with ProcessMemoryPlugin)
|
||||
set(_UI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui")
|
||||
set(_UI_HDR "${CMAKE_CURRENT_BINARY_DIR}/ui_processpicker.h")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_UI_HDR}"
|
||||
COMMAND ${QT}::uic -o "${_UI_HDR}" "${_UI_SRC}"
|
||||
DEPENDS "${_UI_SRC}"
|
||||
COMMENT "UIC processpicker.ui (RemoteProcessMemory)"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
set(PLUGIN_SOURCES
|
||||
RemoteProcessMemoryPlugin.h
|
||||
RemoteProcessMemoryPlugin.cpp
|
||||
rcx_rpc_protocol.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
|
||||
"${_UI_HDR}"
|
||||
)
|
||||
|
||||
add_library(RemoteProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
target_link_libraries(RemoteProcessMemoryPlugin PRIVATE
|
||||
${QT}::Widgets
|
||||
${_QT_WINEXTRAS}
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(RemoteProcessMemoryPlugin PRIVATE psapi shell32)
|
||||
else()
|
||||
target_link_libraries(RemoteProcessMemoryPlugin PRIVATE rt dl)
|
||||
target_compile_options(RemoteProcessMemoryPlugin PRIVATE -fvisibility=hidden)
|
||||
endif()
|
||||
|
||||
target_include_directories(RemoteProcessMemoryPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR} # for ui_processpicker.h
|
||||
)
|
||||
|
||||
set_target_properties(RemoteProcessMemoryPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
|
||||
install(TARGETS RemoteProcessMemoryPlugin
|
||||
LIBRARY DESTINATION Plugins
|
||||
RUNTIME DESTINATION Plugins
|
||||
)
|
||||
|
||||
# Plugin must be able to find the payload at runtime
|
||||
add_dependencies(RemoteProcessMemoryPlugin rcx_payload)
|
||||
|
||||
# ─── 3. Test executables (no Qt) ────────────────────────────────────
|
||||
|
||||
# Host: loads payload in-process, exposes test buffer
|
||||
add_executable(test_rpc_host tests/test_rpc_host.cpp)
|
||||
target_include_directories(test_rpc_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
if(WIN32)
|
||||
target_link_libraries(test_rpc_host PRIVATE psapi)
|
||||
else()
|
||||
target_link_libraries(test_rpc_host PRIVATE pthread rt dl)
|
||||
endif()
|
||||
set_target_properties(test_rpc_host PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
add_dependencies(test_rpc_host rcx_payload)
|
||||
|
||||
# Client: connects to host, tests + benchmarks
|
||||
add_executable(test_rpc_client tests/test_rpc_client.cpp)
|
||||
target_include_directories(test_rpc_client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
if(WIN32)
|
||||
target_link_libraries(test_rpc_client PRIVATE psapi)
|
||||
else()
|
||||
target_link_libraries(test_rpc_client PRIVATE pthread rt)
|
||||
endif()
|
||||
set_target_properties(test_rpc_client PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
add_dependencies(test_rpc_client test_rpc_host)
|
||||
939
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp
Normal file
@@ -0,0 +1,939 @@
|
||||
#include "RemoteProcessMemoryPlugin.h"
|
||||
#include "rcx_rpc_protocol.h"
|
||||
#include "../../src/processpicker.h"
|
||||
|
||||
#include <QStyle>
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QPixmap>
|
||||
#include <QImage>
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32)
|
||||
#include <QtWin>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
# include <tlhelp32.h>
|
||||
# include <psapi.h>
|
||||
# include <shellapi.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
# include <fcntl.h>
|
||||
# include <dlfcn.h>
|
||||
# include <sys/mman.h>
|
||||
# include <sys/wait.h>
|
||||
# include <sys/ptrace.h>
|
||||
# include <sys/user.h>
|
||||
# include <semaphore.h>
|
||||
# include <signal.h>
|
||||
# include <link.h>
|
||||
# include <climits>
|
||||
# include <cstring>
|
||||
# include <fstream>
|
||||
# include <sstream>
|
||||
#endif
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* IPC Client
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
struct IpcClient {
|
||||
#ifdef _WIN32
|
||||
HANDLE hShm = nullptr;
|
||||
HANDLE hReqEvent = nullptr;
|
||||
HANDLE hRspEvent = nullptr;
|
||||
#else
|
||||
int shmFd = -1;
|
||||
sem_t* reqSem = SEM_FAILED;
|
||||
sem_t* rspSem = SEM_FAILED;
|
||||
char shmNameBuf[128] = {};
|
||||
char reqNameBuf[128] = {};
|
||||
char rspNameBuf[128] = {};
|
||||
#endif
|
||||
void* mappedView = nullptr;
|
||||
QMutex mutex;
|
||||
bool connected = false;
|
||||
|
||||
RcxRpcHeader* header() const {
|
||||
return mappedView ? reinterpret_cast<RcxRpcHeader*>(mappedView) : nullptr;
|
||||
}
|
||||
|
||||
~IpcClient() { disconnect(); }
|
||||
|
||||
/* ── connect / disconnect ──────────────────────────────────────── */
|
||||
|
||||
bool connect(uint32_t pid, int timeoutMs = 5000)
|
||||
{
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
/* poll for shared memory to appear (payload creating it) */
|
||||
auto deadline = GetTickCount64() + (uint64_t)timeoutMs;
|
||||
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
|
||||
if (GetTickCount64() >= deadline) return false;
|
||||
Sleep(10);
|
||||
}
|
||||
|
||||
mappedView = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
|
||||
if (!mappedView) { CloseHandle(hShm); hShm = nullptr; return false; }
|
||||
|
||||
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
|
||||
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
|
||||
if (!hReqEvent || !hRspEvent) { disconnect(); return false; }
|
||||
#else
|
||||
strncpy(shmNameBuf, shmName, sizeof(shmNameBuf) - 1);
|
||||
strncpy(reqNameBuf, reqName, sizeof(reqNameBuf) - 1);
|
||||
strncpy(rspNameBuf, rspName, sizeof(rspNameBuf) - 1);
|
||||
|
||||
/* poll for shared memory */
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (true) {
|
||||
shmFd = shm_open(shmName, O_RDWR, 0);
|
||||
if (shmFd >= 0) break;
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) return false;
|
||||
usleep(10000);
|
||||
}
|
||||
|
||||
mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, shmFd, 0);
|
||||
if (mappedView == MAP_FAILED) { mappedView = nullptr; close(shmFd); shmFd = -1; return false; }
|
||||
|
||||
reqSem = sem_open(reqName, 0);
|
||||
rspSem = sem_open(rspName, 0);
|
||||
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) { disconnect(); return false; }
|
||||
#endif
|
||||
|
||||
/* wait for payloadReady */
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
#ifdef _WIN32
|
||||
while (!hdr->payloadReady) {
|
||||
if (GetTickCount64() >= deadline) { disconnect(); return false; }
|
||||
Sleep(5);
|
||||
}
|
||||
#else
|
||||
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) { disconnect(); return false; }
|
||||
usleep(5000);
|
||||
}
|
||||
#endif
|
||||
|
||||
connected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnect()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (mappedView) { UnmapViewOfFile(mappedView); mappedView = nullptr; }
|
||||
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
|
||||
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
|
||||
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
|
||||
#else
|
||||
if (mappedView) { munmap(mappedView, RCX_RPC_SHM_SIZE); mappedView = nullptr; }
|
||||
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
|
||||
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
|
||||
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
|
||||
#endif
|
||||
connected = false;
|
||||
}
|
||||
|
||||
/* ── low-level RPC round-trip ──────────────────────────────────── */
|
||||
|
||||
bool signalAndWait(int timeoutMs = 2000)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
SetEvent(hReqEvent);
|
||||
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
|
||||
#else
|
||||
sem_post(reqSem);
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_sec += timeoutMs / 1000;
|
||||
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
|
||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||
return sem_timedwait(rspSem, &ts) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── public API ────────────────────────────────────────────────── */
|
||||
|
||||
bool readSingle(uint64_t addr, void* buf, int len)
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected || len <= 0) return false;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_READ_BATCH;
|
||||
hdr->requestCount = 1;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
auto* entry = reinterpret_cast<RcxRpcReadEntry*>(data);
|
||||
entry->address = addr;
|
||||
entry->length = (uint32_t)len;
|
||||
entry->dataOffset = sizeof(RcxRpcReadEntry);
|
||||
|
||||
if (!signalAndWait()) { connected = false; return false; }
|
||||
|
||||
memcpy(buf, data + entry->dataOffset, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeSingle(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected || len <= 0) return false;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_WRITE;
|
||||
hdr->writeAddress = addr;
|
||||
hdr->writeLength = (uint32_t)len;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
memcpy(data, buf, len);
|
||||
|
||||
if (!signalAndWait()) { connected = false; return false; }
|
||||
|
||||
return hdr->status == RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
QVector<RemoteProcessProvider::ModuleInfo> enumerateModules()
|
||||
{
|
||||
QVector<RemoteProcessProvider::ModuleInfo> result;
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected) return result;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_ENUM_MODULES;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
if (!signalAndWait()) { connected = false; return result; }
|
||||
if (hdr->status != RCX_RPC_STATUS_OK) return result;
|
||||
|
||||
uint32_t count = hdr->responseCount;
|
||||
result.reserve((int)count);
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto* entry = reinterpret_cast<const RcxRpcModuleEntry*>(
|
||||
data + i * sizeof(RcxRpcModuleEntry));
|
||||
|
||||
QString modName;
|
||||
#ifdef _WIN32
|
||||
modName = QString::fromWCharArray(
|
||||
reinterpret_cast<const wchar_t*>(data + entry->nameOffset),
|
||||
(int)(entry->nameLength / sizeof(wchar_t)));
|
||||
#else
|
||||
modName = QString::fromUtf8(
|
||||
reinterpret_cast<const char*>(data + entry->nameOffset),
|
||||
(int)entry->nameLength);
|
||||
#endif
|
||||
result.push_back(RemoteProcessProvider::ModuleInfo{modName, entry->base, entry->size});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ping()
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected) return false;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
hdr->command = RPC_CMD_PING;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
if (!signalAndWait()) { connected = false; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown()
|
||||
{
|
||||
QMutexLocker lock(&mutex);
|
||||
if (!connected) return;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
||||
hdr->command = RPC_CMD_SHUTDOWN;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
signalAndWait(500);
|
||||
connected = false;
|
||||
}
|
||||
};
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* RemoteProcessProvider
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
RemoteProcessProvider::RemoteProcessProvider(
|
||||
uint32_t pid, const QString& processName,
|
||||
std::shared_ptr<IpcClient> ipc)
|
||||
: m_pid(pid)
|
||||
, m_processName(processName)
|
||||
, m_connected(ipc && ipc->connected)
|
||||
, m_base(0)
|
||||
, m_ipc(std::move(ipc))
|
||||
{
|
||||
if (m_connected) {
|
||||
cacheModules();
|
||||
// Read pointer size from payload's SHM header (0 means not set → default 8)
|
||||
auto* hdr = m_ipc ? m_ipc->header() : nullptr;
|
||||
if (hdr) {
|
||||
uint32_t ps = hdr->pointerSize;
|
||||
if (ps == 4 || ps == 8)
|
||||
m_pointerSize = (int)ps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RemoteProcessProvider::~RemoteProcessProvider() = default;
|
||||
|
||||
bool RemoteProcessProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (!m_connected || len <= 0) return false;
|
||||
bool ok = m_ipc->readSingle(addr, buf, len);
|
||||
if (!ok) {
|
||||
memset(buf, 0, (size_t)len);
|
||||
/* update connectivity flag through mutable ipc */
|
||||
const_cast<RemoteProcessProvider*>(this)->m_connected = m_ipc->connected;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
int RemoteProcessProvider::size() const
|
||||
{
|
||||
return m_connected ? 0x10000 : 0;
|
||||
}
|
||||
|
||||
bool RemoteProcessProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (!m_connected || len <= 0) return false;
|
||||
bool ok = m_ipc->writeSingle(addr, buf, len);
|
||||
if (!ok) m_connected = m_ipc->connected;
|
||||
return ok;
|
||||
}
|
||||
|
||||
QString RemoteProcessProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (addr >= mod.base && addr < mod.base + mod.size) {
|
||||
uint64_t off = addr - mod.base;
|
||||
return QStringLiteral("%1+0x%2")
|
||||
.arg(mod.name)
|
||||
.arg(off, 0, 16, QChar('0'));
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
uint64_t RemoteProcessProvider::symbolToAddress(const QString& n) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
if (mod.name.compare(n, Qt::CaseInsensitive) == 0)
|
||||
return mod.base;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RemoteProcessProvider::cacheModules()
|
||||
{
|
||||
m_modules = m_ipc->enumerateModules();
|
||||
if (!m_modules.isEmpty())
|
||||
m_base = m_modules.first().base;
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Injection helpers
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
namespace {
|
||||
|
||||
/* Resolve payload DLL/SO path next to this plugin DLL/SO */
|
||||
static QString payloadPath()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
HMODULE hSelf = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(&payloadPath), &hSelf);
|
||||
WCHAR buf[MAX_PATH];
|
||||
GetModuleFileNameW(hSelf, buf, MAX_PATH);
|
||||
QFileInfo fi(QString::fromWCharArray(buf));
|
||||
return fi.absolutePath() + QStringLiteral("/rcx_payload.dll");
|
||||
#else
|
||||
Dl_info info;
|
||||
dladdr(reinterpret_cast<void*>(&payloadPath), &info);
|
||||
QFileInfo fi(QString::fromUtf8(info.dli_fname));
|
||||
return fi.absolutePath() + QStringLiteral("/rcx_payload.so");
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* ── Windows injection: CreateRemoteThread + LoadLibraryA ─────────── */
|
||||
|
||||
static bool injectPayload(uint32_t pid, QString* errorMsg)
|
||||
{
|
||||
QString path = payloadPath();
|
||||
QByteArray pathUtf8 = QDir::toNativeSeparators(path).toLocal8Bit();
|
||||
|
||||
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
|
||||
if (!hProc) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("OpenProcess failed (error %1).\n"
|
||||
"Try running as Administrator.")
|
||||
.arg(GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
/* allocate + write path string in target */
|
||||
SIZE_T pathLen = (SIZE_T)(pathUtf8.size() + 1);
|
||||
void* remotePath = VirtualAllocEx(hProc, nullptr, pathLen,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (!remotePath) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("VirtualAllocEx failed.");
|
||||
CloseHandle(hProc);
|
||||
return false;
|
||||
}
|
||||
|
||||
WriteProcessMemory(hProc, remotePath, pathUtf8.constData(), pathLen, nullptr);
|
||||
|
||||
/* Step 1: LoadLibraryA — loads the DLL (DllMain is minimal) */
|
||||
HMODULE hK32 = GetModuleHandleA("kernel32.dll");
|
||||
auto pLoadLib = reinterpret_cast<LPTHREAD_START_ROUTINE>(
|
||||
GetProcAddress(hK32, "LoadLibraryA"));
|
||||
|
||||
HANDLE hThread = CreateRemoteThread(hProc, nullptr, 0,
|
||||
pLoadLib, remotePath, 0, nullptr);
|
||||
if (!hThread) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("CreateRemoteThread failed (error %1).")
|
||||
.arg(GetLastError());
|
||||
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
|
||||
CloseHandle(hProc);
|
||||
return false;
|
||||
}
|
||||
|
||||
WaitForSingleObject(hThread, 10000);
|
||||
|
||||
DWORD exitCode = 0;
|
||||
GetExitCodeThread(hThread, &exitCode);
|
||||
CloseHandle(hThread);
|
||||
|
||||
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
|
||||
|
||||
if (exitCode == 0) {
|
||||
CloseHandle(hProc);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n"
|
||||
"Ensure rcx_payload.dll is in: %1").arg(path);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Step 2: Call RcxPayloadInit() — safe to create timer queues now
|
||||
(loader lock is no longer held after LoadLibrary returned) */
|
||||
HMODULE hPayloadRemote = (HMODULE)(uintptr_t)exitCode;
|
||||
auto pGetProcAddr = reinterpret_cast<FARPROC(WINAPI*)(HMODULE, LPCSTR)>(
|
||||
GetProcAddress(hK32, "GetProcAddress"));
|
||||
|
||||
/* Write "RcxPayloadInit\0" into target, call GetProcAddress remotely */
|
||||
const char initName[] = "RcxPayloadInit";
|
||||
void* remoteInitName = VirtualAllocEx(hProc, nullptr, sizeof(initName),
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (remoteInitName) {
|
||||
WriteProcessMemory(hProc, remoteInitName, initName, sizeof(initName), nullptr);
|
||||
|
||||
/* We need to call GetProcAddress(hPayload, "RcxPayloadInit") then call the result.
|
||||
Simpler approach: write small shellcode that does both calls. */
|
||||
uint8_t shellcode[128];
|
||||
int off = 0;
|
||||
|
||||
/* sub rsp, 40 ; shadow space + alignment */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xEC; shellcode[off++] = 0x28;
|
||||
/* mov rcx, hPayloadRemote ; first arg = module handle */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0xB9;
|
||||
uint64_t hMod = (uint64_t)(uintptr_t)hPayloadRemote;
|
||||
memcpy(shellcode + off, &hMod, 8); off += 8;
|
||||
/* mov rdx, remoteInitName ; second arg = "RcxPayloadInit" */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0xBA;
|
||||
uint64_t pName = (uint64_t)(uintptr_t)remoteInitName;
|
||||
memcpy(shellcode + off, &pName, 8); off += 8;
|
||||
/* mov rax, GetProcAddress */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0xB8;
|
||||
uint64_t pGPA = (uint64_t)(uintptr_t)pGetProcAddr;
|
||||
memcpy(shellcode + off, &pGPA, 8); off += 8;
|
||||
/* call rax ; rax = RcxPayloadInit */
|
||||
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
|
||||
/* test rax, rax */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0x85; shellcode[off++] = 0xC0;
|
||||
/* jz skip (jump over the call if null) */
|
||||
shellcode[off++] = 0x74; shellcode[off++] = 0x02;
|
||||
/* call rax ; RcxPayloadInit() */
|
||||
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
|
||||
/* skip: add rsp, 40 */
|
||||
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xC4; shellcode[off++] = 0x28;
|
||||
/* ret */
|
||||
shellcode[off++] = 0xC3;
|
||||
|
||||
void* remoteCode = VirtualAllocEx(hProc, nullptr, (SIZE_T)off,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||
if (remoteCode) {
|
||||
WriteProcessMemory(hProc, remoteCode, shellcode, (SIZE_T)off, nullptr);
|
||||
|
||||
HANDLE hThread2 = CreateRemoteThread(hProc, nullptr, 0,
|
||||
(LPTHREAD_START_ROUTINE)remoteCode, nullptr, 0, nullptr);
|
||||
if (hThread2) {
|
||||
WaitForSingleObject(hThread2, 10000);
|
||||
CloseHandle(hThread2);
|
||||
}
|
||||
VirtualFreeEx(hProc, remoteCode, 0, MEM_RELEASE);
|
||||
}
|
||||
VirtualFreeEx(hProc, remoteInitName, 0, MEM_RELEASE);
|
||||
}
|
||||
|
||||
CloseHandle(hProc);
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
/* ── Linux injection: ptrace + dlopen ─────────────────────────────── */
|
||||
|
||||
static uint64_t findLibBase(pid_t pid, const char* libName)
|
||||
{
|
||||
char mapsPath[64];
|
||||
snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid);
|
||||
FILE* f = fopen(mapsPath, "r");
|
||||
if (!f) return 0;
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (strstr(line, libName)) {
|
||||
uint64_t base;
|
||||
if (sscanf(line, "%lx-", &base) == 1) {
|
||||
fclose(f);
|
||||
return base;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint64_t findSyscallInsn(pid_t pid)
|
||||
{
|
||||
char mapsPath[64];
|
||||
snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid);
|
||||
FILE* f = fopen(mapsPath, "r");
|
||||
if (!f) return 0;
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
if (strstr(line, "libc") && strstr(line, "r-xp")) {
|
||||
uint64_t start, end;
|
||||
if (sscanf(line, "%lx-%lx", &start, &end) != 2) continue;
|
||||
fclose(f);
|
||||
|
||||
/* scan for 0F 05 (syscall) */
|
||||
char memPath[64];
|
||||
snprintf(memPath, sizeof(memPath), "/proc/%d/mem", pid);
|
||||
int memFd = open(memPath, O_RDONLY);
|
||||
if (memFd < 0) return 0;
|
||||
|
||||
uint8_t buf[4096];
|
||||
for (uint64_t off = start; off < end; off += sizeof(buf)) {
|
||||
ssize_t n = pread(memFd, buf, sizeof(buf), (off_t)off);
|
||||
if (n <= 1) break;
|
||||
for (ssize_t i = 0; i + 1 < n; ++i) {
|
||||
if (buf[i] == 0x0F && buf[i + 1] == 0x05) {
|
||||
close(memFd);
|
||||
return off + (uint64_t)i;
|
||||
}
|
||||
}
|
||||
}
|
||||
close(memFd);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool writeTargetMem(pid_t pid, uint64_t addr, const void* src, size_t len)
|
||||
{
|
||||
const uint8_t* p = static_cast<const uint8_t*>(src);
|
||||
for (size_t i = 0; i < len; i += sizeof(long)) {
|
||||
long val = 0;
|
||||
size_t chunk = (len - i < sizeof(long)) ? (len - i) : sizeof(long);
|
||||
if (chunk < sizeof(long)) {
|
||||
errno = 0;
|
||||
val = ptrace(PTRACE_PEEKDATA, pid, (void*)(addr + i), nullptr);
|
||||
if (errno) return false;
|
||||
}
|
||||
memcpy(&val, p + i, chunk);
|
||||
if (ptrace(PTRACE_POKEDATA, pid, (void*)(addr + i), (void*)val) < 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool injectPayload(uint32_t pid, QString* errorMsg)
|
||||
{
|
||||
QString path = payloadPath();
|
||||
QByteArray pathUtf8 = path.toUtf8();
|
||||
|
||||
if (ptrace(PTRACE_ATTACH, (pid_t)pid, nullptr, nullptr) < 0) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("ptrace attach failed: %1\n"
|
||||
"Check /proc/sys/kernel/yama/ptrace_scope or run as root.")
|
||||
.arg(strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int status;
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
/* save registers */
|
||||
struct user_regs_struct savedRegs, regs;
|
||||
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, &savedRegs);
|
||||
regs = savedRegs;
|
||||
|
||||
/* find syscall instruction in target's libc */
|
||||
uint64_t syscallAddr = findSyscallInsn((pid_t)pid);
|
||||
if (!syscallAddr) {
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Could not find syscall instruction in target.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* find dlopen in target via libc offset technique */
|
||||
void* ourDlopen = dlsym(RTLD_DEFAULT, "dlopen");
|
||||
uint64_t ourLibcBase = findLibBase(getpid(), "libc");
|
||||
uint64_t targetLibcBase = findLibBase((pid_t)pid, "libc");
|
||||
|
||||
if (!ourDlopen || !ourLibcBase || !targetLibcBase) {
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Could not resolve dlopen address.");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t targetDlopen = targetLibcBase + ((uint64_t)ourDlopen - ourLibcBase);
|
||||
|
||||
/* call mmap in target via syscall: mmap(0, 4096, RWX, MAP_PRIVATE|MAP_ANON, -1, 0) */
|
||||
regs.rax = 9; /* __NR_mmap */
|
||||
regs.rdi = 0;
|
||||
regs.rsi = 4096;
|
||||
regs.rdx = 7; /* PROT_READ|PROT_WRITE|PROT_EXEC */
|
||||
regs.r10 = 0x22; /* MAP_PRIVATE|MAP_ANONYMOUS */
|
||||
regs.r8 = (uint64_t)-1;
|
||||
regs.r9 = 0;
|
||||
regs.rip = syscallAddr;
|
||||
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, ®s);
|
||||
ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr);
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, ®s);
|
||||
uint64_t mmapPage = regs.rax;
|
||||
|
||||
if ((int64_t)mmapPage < 0 || mmapPage == 0) {
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &savedRegs);
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
if (errorMsg) *errorMsg = QStringLiteral("mmap in target failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* write path string at start of page */
|
||||
writeTargetMem((pid_t)pid, mmapPage, pathUtf8.constData(), (size_t)(pathUtf8.size() + 1));
|
||||
|
||||
/* write shellcode after path:
|
||||
* mov rdi, pathAddr (48 BF xxxxxxxx)
|
||||
* mov rsi, 2 (48 BE 02000000 00000000)
|
||||
* mov rax, dlopenAddr (48 B8 xxxxxxxx)
|
||||
* call rax (FF D0)
|
||||
* int3 (CC)
|
||||
*/
|
||||
uint64_t pathAddr = mmapPage;
|
||||
uint64_t codeAddr = mmapPage + ((pathUtf8.size() + 1 + 15) & ~15ULL);
|
||||
|
||||
uint8_t sc[64];
|
||||
int len = 0;
|
||||
/* mov rdi, imm64 */
|
||||
sc[len++] = 0x48; sc[len++] = 0xBF;
|
||||
memcpy(sc + len, &pathAddr, 8); len += 8;
|
||||
/* mov rsi, 2 (RTLD_NOW) */
|
||||
sc[len++] = 0x48; sc[len++] = 0xBE;
|
||||
uint64_t rtldNow = 2;
|
||||
memcpy(sc + len, &rtldNow, 8); len += 8;
|
||||
/* mov rax, dlopen */
|
||||
sc[len++] = 0x48; sc[len++] = 0xB8;
|
||||
memcpy(sc + len, &targetDlopen, 8); len += 8;
|
||||
/* call rax */
|
||||
sc[len++] = 0xFF; sc[len++] = 0xD0;
|
||||
/* int3 */
|
||||
sc[len++] = 0xCC;
|
||||
|
||||
writeTargetMem((pid_t)pid, codeAddr, sc, (size_t)len);
|
||||
|
||||
/* execute shellcode */
|
||||
regs = savedRegs;
|
||||
regs.rip = codeAddr;
|
||||
regs.rsp = (mmapPage + 4096) & ~0xFULL;
|
||||
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, ®s);
|
||||
ptrace(PTRACE_CONT, (pid_t)pid, nullptr, nullptr);
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
bool ok = false;
|
||||
if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
|
||||
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, ®s);
|
||||
ok = (regs.rax != 0);
|
||||
}
|
||||
|
||||
/* clean up: munmap the page via syscall */
|
||||
struct user_regs_struct cleanRegs = savedRegs;
|
||||
cleanRegs.rax = 11; /* __NR_munmap */
|
||||
cleanRegs.rdi = mmapPage;
|
||||
cleanRegs.rsi = 4096;
|
||||
cleanRegs.rip = syscallAddr;
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &cleanRegs);
|
||||
ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr);
|
||||
waitpid((pid_t)pid, &status, 0);
|
||||
|
||||
/* restore and detach */
|
||||
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &savedRegs);
|
||||
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
|
||||
|
||||
if (!ok && errorMsg)
|
||||
*errorMsg = QStringLiteral("dlopen failed in target.\n"
|
||||
"Ensure payload is at: %1").arg(path);
|
||||
return ok;
|
||||
}
|
||||
#endif /* _WIN32 / linux injection */
|
||||
|
||||
} /* anonymous namespace */
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* RemoteProcessMemoryPlugin
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
RemoteProcessMemoryPlugin::RemoteProcessMemoryPlugin() = default;
|
||||
RemoteProcessMemoryPlugin::~RemoteProcessMemoryPlugin() = default;
|
||||
|
||||
QIcon RemoteProcessMemoryPlugin::Icon() const
|
||||
{
|
||||
return qApp->style()->standardIcon(QStyle::SP_DriveNetIcon);
|
||||
}
|
||||
|
||||
bool RemoteProcessMemoryPlugin::canHandle(const QString& target) const
|
||||
{
|
||||
return target.startsWith(QStringLiteral("rpm:"));
|
||||
}
|
||||
|
||||
std::unique_ptr<rcx::Provider>
|
||||
RemoteProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
{
|
||||
/* target = "rpm:{pid}:{name}" */
|
||||
QStringList parts = target.split(':');
|
||||
if (parts.size() < 3 || parts[0] != QStringLiteral("rpm")) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
uint32_t pid = parts[1].toUInt(&ok);
|
||||
QString name = parts.mid(2).join(':'); /* name may contain colons */
|
||||
|
||||
if (!ok || pid == 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto ipc = getOrCreateConnection(pid, errorMsg);
|
||||
if (!ipc) return nullptr;
|
||||
|
||||
return std::make_unique<RemoteProcessProvider>(pid, name, ipc);
|
||||
}
|
||||
|
||||
uint64_t RemoteProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||
{
|
||||
/* Read imageBase directly from the shared-memory header -- zero IPC cost.
|
||||
The payload filled it at init from PEB->Ldr (Win) / /proc/self/maps (Linux). */
|
||||
QStringList parts = target.split(':');
|
||||
if (parts.size() < 2 || parts[0] != QStringLiteral("rpm"))
|
||||
return 0;
|
||||
|
||||
bool ok;
|
||||
uint32_t pid = parts[1].toUInt(&ok);
|
||||
if (!ok) return 0;
|
||||
|
||||
QMutexLocker lock(&m_connectionsMutex);
|
||||
auto it = m_connections.constFind(pid);
|
||||
if (it == m_connections.constEnd() || !(*it)->connected)
|
||||
return 0;
|
||||
|
||||
auto* hdr = static_cast<const RcxRpcHeader*>((*it)->mappedView);
|
||||
return hdr->imageBase;
|
||||
}
|
||||
|
||||
bool RemoteProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
/* ── 1. pick a process ── */
|
||||
QVector<PluginProcessInfo> pluginProcs = enumerateProcesses();
|
||||
QList<ProcessInfo> procs;
|
||||
for (const auto& pi : pluginProcs) {
|
||||
ProcessInfo info;
|
||||
info.pid = pi.pid;
|
||||
info.name = pi.name;
|
||||
info.path = pi.path;
|
||||
info.icon = pi.icon;
|
||||
procs.append(info);
|
||||
}
|
||||
|
||||
ProcessPicker picker(procs, parent);
|
||||
if (picker.exec() != QDialog::Accepted) return false;
|
||||
|
||||
uint32_t pid = picker.selectedProcessId();
|
||||
QString name = picker.selectedProcessName();
|
||||
|
||||
/* ── 2. ask inject or connect ── */
|
||||
QMessageBox box(parent);
|
||||
box.setWindowTitle(QStringLiteral("Remote Process Memory"));
|
||||
box.setText(QStringLiteral("Connect to %1 (PID %2)").arg(name).arg(pid));
|
||||
box.setInformativeText(QStringLiteral("Choose how to connect to the target:"));
|
||||
QAbstractButton* injectBtn = box.addButton(QStringLiteral("Inject Payload"), QMessageBox::ActionRole);
|
||||
QAbstractButton* connectBtn = box.addButton(QStringLiteral("Already Injected"), QMessageBox::ActionRole);
|
||||
box.addButton(QMessageBox::Cancel);
|
||||
box.exec();
|
||||
|
||||
QAbstractButton* clicked = box.clickedButton();
|
||||
if (clicked == injectBtn) {
|
||||
QString injectErr;
|
||||
if (!injectPayload(pid, &injectErr)) {
|
||||
QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr);
|
||||
return false;
|
||||
}
|
||||
|
||||
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
else if (clicked == connectBtn) {
|
||||
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<PluginProcessInfo> RemoteProcessMemoryPlugin::enumerateProcesses()
|
||||
{
|
||||
QVector<PluginProcessInfo> procs;
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snap == INVALID_HANDLE_VALUE) return procs;
|
||||
|
||||
PROCESSENTRY32W entry;
|
||||
entry.dwSize = sizeof(entry);
|
||||
|
||||
if (Process32FirstW(snap, &entry)) {
|
||||
do {
|
||||
PluginProcessInfo info;
|
||||
info.pid = entry.th32ProcessID;
|
||||
info.name = QString::fromWCharArray(entry.szExeFile);
|
||||
|
||||
HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE, entry.th32ProcessID);
|
||||
if (hProc) {
|
||||
wchar_t path[MAX_PATH * 2];
|
||||
DWORD pathLen = sizeof(path) / sizeof(wchar_t);
|
||||
if (QueryFullProcessImageNameW(hProc, 0, path, &pathLen)) {
|
||||
info.path = QString::fromWCharArray(path);
|
||||
SHFILEINFOW sfi = {};
|
||||
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi),
|
||||
SHGFI_ICON | SHGFI_SMALLICON) && sfi.hIcon) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
info.icon = QIcon(QPixmap::fromImage(QImage::fromHICON(sfi.hIcon)));
|
||||
#else
|
||||
info.icon = QIcon(QtWin::fromHICON(sfi.hIcon));
|
||||
#endif
|
||||
DestroyIcon(sfi.hIcon);
|
||||
}
|
||||
}
|
||||
CloseHandle(hProc);
|
||||
}
|
||||
procs.append(info);
|
||||
} while (Process32NextW(snap, &entry));
|
||||
}
|
||||
CloseHandle(snap);
|
||||
|
||||
#else
|
||||
QDir procDir(QStringLiteral("/proc"));
|
||||
QIcon defIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
|
||||
|
||||
for (const QString& entry : procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
bool ok;
|
||||
uint32_t pid = entry.toUInt(&ok);
|
||||
if (!ok || pid == 0) continue;
|
||||
|
||||
QFile commFile(QStringLiteral("/proc/%1/comm").arg(pid));
|
||||
if (!commFile.open(QIODevice::ReadOnly)) continue;
|
||||
QString procName = QString::fromUtf8(commFile.readAll()).trimmed();
|
||||
commFile.close();
|
||||
if (procName.isEmpty()) continue;
|
||||
|
||||
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
|
||||
if (::access(memPath.toUtf8().constData(), R_OK) != 0) continue;
|
||||
|
||||
QFileInfo exeInfo(QStringLiteral("/proc/%1/exe").arg(pid));
|
||||
PluginProcessInfo info;
|
||||
info.pid = pid;
|
||||
info.name = procName;
|
||||
info.path = exeInfo.exists() ? exeInfo.symLinkTarget() : QString();
|
||||
info.icon = defIcon;
|
||||
procs.append(info);
|
||||
}
|
||||
#endif
|
||||
|
||||
return procs;
|
||||
}
|
||||
|
||||
std::shared_ptr<IpcClient>
|
||||
RemoteProcessMemoryPlugin::getOrCreateConnection(
|
||||
uint32_t pid, QString* errorMsg)
|
||||
{
|
||||
QMutexLocker lock(&m_connectionsMutex);
|
||||
|
||||
auto it = m_connections.find(pid);
|
||||
if (it != m_connections.end() && (*it)->connected)
|
||||
return *it;
|
||||
|
||||
auto ipc = std::make_shared<IpcClient>();
|
||||
if (!ipc->connect(pid)) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Failed to connect IPC to PID %1.\n"
|
||||
"Is the payload running?").arg(pid);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
m_connections[pid] = ipc;
|
||||
return ipc;
|
||||
}
|
||||
|
||||
/* ── Plugin factory ───────────────────────────────────────────────── */
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new RemoteProcessMemoryPlugin();
|
||||
}
|
||||
88
plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
#include "../../src/iplugin.h"
|
||||
#include "../../src/providers/provider.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <QMutex>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
|
||||
struct IpcClient; /* defined in .cpp */
|
||||
|
||||
/* ── Provider ─────────────────────────────────────────────────────── */
|
||||
|
||||
class RemoteProcessProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
struct ModuleInfo { QString name; uint64_t base; uint64_t size; };
|
||||
|
||||
RemoteProcessProvider(uint32_t pid, const QString& processName,
|
||||
std::shared_ptr<IpcClient> ipc);
|
||||
~RemoteProcessProvider() override;
|
||||
|
||||
/* required */
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
int size() const override;
|
||||
|
||||
/* optional */
|
||||
bool write(uint64_t addr, const void* buf, int len) override;
|
||||
bool isWritable() const override { return m_connected; }
|
||||
QString name() const override { return m_processName; }
|
||||
QString kind() const override { return QStringLiteral("RemoteProcess"); }
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
int pointerSize() const override { return m_pointerSize; }
|
||||
bool isReadable(uint64_t, int len) const override { return m_connected && len >= 0; }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
uint64_t symbolToAddress(const QString& n) const override;
|
||||
|
||||
uint32_t pid() const { return m_pid; }
|
||||
|
||||
private:
|
||||
void cacheModules();
|
||||
|
||||
uint32_t m_pid;
|
||||
QString m_processName;
|
||||
bool m_connected;
|
||||
uint64_t m_base;
|
||||
int m_pointerSize = 8;
|
||||
mutable std::shared_ptr<IpcClient> m_ipc;
|
||||
QVector<ModuleInfo> m_modules;
|
||||
};
|
||||
|
||||
/* ── Plugin ───────────────────────────────────────────────────────── */
|
||||
|
||||
class RemoteProcessMemoryPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
RemoteProcessMemoryPlugin();
|
||||
~RemoteProcessMemoryPlugin() override;
|
||||
|
||||
std::string Name() const override { return "Remote Process Memory"; }
|
||||
std::string Version() const override { return "1.0.0"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
std::string Description() const override {
|
||||
return "Read/write memory via injected payload (shared-memory IPC)";
|
||||
}
|
||||
k_ELoadType LoadType() const override { return k_ELoadTypeManual; }
|
||||
QIcon Icon() const override;
|
||||
|
||||
bool canHandle(const QString& target) const override;
|
||||
std::unique_ptr<rcx::Provider> createProvider(const QString& target,
|
||||
QString* errorMsg) override;
|
||||
uint64_t getInitialBaseAddress(const QString& target) const override;
|
||||
bool selectTarget(QWidget* parent, QString* target) override;
|
||||
|
||||
bool providesProcessList() const override { return true; }
|
||||
QVector<PluginProcessInfo> enumerateProcesses() override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<IpcClient> getOrCreateConnection(
|
||||
uint32_t pid, QString* errorMsg);
|
||||
|
||||
mutable QMutex m_connectionsMutex;
|
||||
QHash<uint32_t, std::shared_ptr<IpcClient>> m_connections;
|
||||
};
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||
612
plugins/RemoteProcessMemory/payload/rcx_payload.cpp
Normal file
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
* rcx_payload -- injected into target process.
|
||||
*
|
||||
* Pure Win32 / POSIX, NO Qt, minimal footprint.
|
||||
* Creates the main IPC channel (shared memory + events/semaphores)
|
||||
* using PID-only naming and uses a timer queue for polling.
|
||||
*/
|
||||
|
||||
#include "../rcx_rpc_protocol.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
/* ===================================================================
|
||||
* WINDOWS implementation
|
||||
* =================================================================== */
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
|
||||
/* ── globals ──────────────────────────────────────────────────────── */
|
||||
static HANDLE g_hShm = nullptr;
|
||||
static void* g_mappedView = nullptr;
|
||||
static HANDLE g_hReqEvent = nullptr;
|
||||
static HANDLE g_hRspEvent = nullptr;
|
||||
static HANDLE g_hTimerQueue = nullptr;
|
||||
static HANDLE g_hPollTimer = nullptr;
|
||||
static volatile LONG g_initialized = 0;
|
||||
|
||||
/* ── memory safety via VirtualQuery ────────────────────────────────── */
|
||||
|
||||
inline bool IsReadableProtect(DWORD p)
|
||||
{
|
||||
if (p & (PAGE_NOACCESS | PAGE_GUARD))
|
||||
return false;
|
||||
|
||||
const DWORD readable =
|
||||
PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
|
||||
PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
|
||||
return (p & readable) != 0;
|
||||
}
|
||||
|
||||
inline bool IsWritableProtect(DWORD p)
|
||||
{
|
||||
if (p & (PAGE_NOACCESS | PAGE_GUARD))
|
||||
return false;
|
||||
|
||||
const DWORD writable =
|
||||
PAGE_READWRITE | PAGE_WRITECOPY |
|
||||
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
|
||||
|
||||
return (p & writable) != 0;
|
||||
}
|
||||
|
||||
/* Check that the full range [addr, addr+len) is covered by readable pages. */
|
||||
static bool IsRangeReadable(uintptr_t addr, uint32_t len)
|
||||
{
|
||||
uintptr_t end = addr + len;
|
||||
uintptr_t cur = addr;
|
||||
while (cur < end) {
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
|
||||
return false;
|
||||
if (mbi.State != MEM_COMMIT || !IsReadableProtect(mbi.Protect))
|
||||
return false;
|
||||
uintptr_t regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
|
||||
cur = regionEnd;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool IsRangeWritable(uintptr_t addr, uint32_t len)
|
||||
{
|
||||
uintptr_t end = addr + len;
|
||||
uintptr_t cur = addr;
|
||||
while (cur < end) {
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
|
||||
return false;
|
||||
if (mbi.State != MEM_COMMIT || !IsWritableProtect(mbi.Protect))
|
||||
return false;
|
||||
uintptr_t regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
|
||||
cur = regionEnd;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── command handlers ─────────────────────────────────────────────── */
|
||||
|
||||
static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
auto* entries = reinterpret_cast<RcxRpcReadEntry*>(data);
|
||||
for (uint32_t i = 0; i < hdr->requestCount; ++i) {
|
||||
uint8_t* dest = data + entries[i].dataOffset;
|
||||
uintptr_t src = static_cast<uintptr_t>(entries[i].address);
|
||||
if (IsRangeReadable(src, entries[i].length)) {
|
||||
memcpy(dest, reinterpret_cast<const void*>(src), entries[i].length);
|
||||
} else {
|
||||
memset(dest, 0, entries[i].length);
|
||||
hdr->status = RCX_RPC_STATUS_PARTIAL;
|
||||
}
|
||||
/* SEH fallback (commented out, kept for reference):
|
||||
__try {
|
||||
memcpy(dest, reinterpret_cast<const void*>(src), entries[i].length);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
memset(dest, 0, entries[i].length);
|
||||
hdr->status = RCX_RPC_STATUS_PARTIAL;
|
||||
}
|
||||
*/
|
||||
}
|
||||
hdr->responseCount = hdr->requestCount;
|
||||
}
|
||||
|
||||
static void handle_write(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
uintptr_t dst = static_cast<uintptr_t>(hdr->writeAddress);
|
||||
if (IsRangeWritable(dst, hdr->writeLength)) {
|
||||
memcpy(reinterpret_cast<void*>(dst), data, hdr->writeLength);
|
||||
} else {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
}
|
||||
/* SEH fallback (commented out, kept for reference):
|
||||
__try {
|
||||
memcpy(reinterpret_cast<void*>(dst), data, hdr->writeLength);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
HANDLE hProc = GetCurrentProcess();
|
||||
HMODULE mods[1024];
|
||||
DWORD needed = 0;
|
||||
if (!EnumProcessModules(hProc, mods, sizeof(mods), &needed)) {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
hdr->responseCount = 0;
|
||||
return;
|
||||
}
|
||||
int count = (int)(needed / sizeof(HMODULE));
|
||||
if (count > 1024) count = 1024;
|
||||
|
||||
uint32_t entryBytes = (uint32_t)(count * sizeof(RcxRpcModuleEntry));
|
||||
uint32_t nameDataOff = entryBytes;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
MODULEINFO mi{};
|
||||
WCHAR modName[MAX_PATH];
|
||||
GetModuleInformation(hProc, mods[i], &mi, sizeof(mi));
|
||||
int nameLen = (int)GetModuleBaseNameW(hProc, mods[i], modName, MAX_PATH);
|
||||
uint32_t nameBytes = (uint32_t)(nameLen * sizeof(WCHAR));
|
||||
|
||||
auto* entry = reinterpret_cast<RcxRpcModuleEntry*>(data + i * sizeof(RcxRpcModuleEntry));
|
||||
entry->base = reinterpret_cast<uint64_t>(mi.lpBaseOfDll);
|
||||
entry->size = static_cast<uint64_t>(mi.SizeOfImage);
|
||||
entry->nameOffset = nameDataOff;
|
||||
entry->nameLength = nameBytes;
|
||||
|
||||
if (nameDataOff + nameBytes <= RCX_RPC_DATA_SIZE) {
|
||||
memcpy(data + nameDataOff, modName, nameBytes);
|
||||
nameDataOff += nameBytes;
|
||||
}
|
||||
}
|
||||
|
||||
hdr->responseCount = (uint32_t)count;
|
||||
hdr->totalDataUsed = nameDataOff;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
/* forward declaration */
|
||||
void RcxPayloadCleanup();
|
||||
|
||||
/* ── timer callback (non-blocking poll) ───────────────────────────── */
|
||||
|
||||
static VOID CALLBACK RcxPollTimerCallback(PVOID, BOOLEAN)
|
||||
{
|
||||
if (!g_mappedView || !g_hReqEvent || !g_hRspEvent)
|
||||
return;
|
||||
|
||||
/* non-blocking check: is there a pending request? */
|
||||
DWORD rc = WaitForSingleObject(g_hReqEvent, 0);
|
||||
if (rc != WAIT_OBJECT_0)
|
||||
return;
|
||||
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
switch (static_cast<RcxRpcCommand>(hdr->command)) {
|
||||
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
|
||||
case RPC_CMD_WRITE: handle_write(hdr, data); break;
|
||||
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
|
||||
case RPC_CMD_PING: break;
|
||||
case RPC_CMD_SHUTDOWN:
|
||||
RcxPayloadCleanup();
|
||||
return;
|
||||
default:
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
SetEvent(g_hRspEvent);
|
||||
}
|
||||
|
||||
/* ── cleanup ──────────────────────────────────────────────────────── */
|
||||
|
||||
void RcxPayloadCleanup()
|
||||
{
|
||||
if (!InterlockedCompareExchange(&g_initialized, 0, 0))
|
||||
return;
|
||||
|
||||
/* stop the poll timer first */
|
||||
if (g_hTimerQueue) {
|
||||
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE); /* waits for callbacks */
|
||||
g_hTimerQueue = nullptr;
|
||||
g_hPollTimer = nullptr;
|
||||
}
|
||||
|
||||
/* mark not-ready */
|
||||
if (g_mappedView) {
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
|
||||
}
|
||||
|
||||
if (g_mappedView) { UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; }
|
||||
if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; }
|
||||
if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; }
|
||||
if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; }
|
||||
|
||||
InterlockedExchange(&g_initialized, 0);
|
||||
}
|
||||
|
||||
/* ── init (called AFTER DllMain returns — safe for timer queues) ── */
|
||||
|
||||
extern "C" __declspec(dllexport)
|
||||
bool RcxPayloadInit()
|
||||
{
|
||||
if (InterlockedCompareExchange(&g_initialized, 1, 0) != 0)
|
||||
return true; /* already initialized */
|
||||
|
||||
uint32_t pid = GetCurrentProcessId();
|
||||
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
|
||||
|
||||
g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
|
||||
PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName);
|
||||
if (!g_hShm) {
|
||||
InterlockedExchange(&g_initialized, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
|
||||
if (!g_mappedView) {
|
||||
CloseHandle(g_hShm); g_hShm = nullptr;
|
||||
InterlockedExchange(&g_initialized, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
hdr->version = RCX_RPC_VERSION;
|
||||
|
||||
/* image base from PEB */
|
||||
{
|
||||
uint64_t peb;
|
||||
asm volatile("mov %%gs:0x60, %0" : "=r"(peb));
|
||||
uint64_t ldr = *reinterpret_cast<uint64_t*>(peb + 0x18);
|
||||
uint64_t firstLink = *reinterpret_cast<uint64_t*>(ldr + 0x10);
|
||||
hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30);
|
||||
}
|
||||
|
||||
g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName);
|
||||
g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName);
|
||||
if (!g_hReqEvent || !g_hRspEvent) {
|
||||
RcxPayloadCleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* create dedicated timer queue + fast poll timer (10ms interval) */
|
||||
g_hTimerQueue = CreateTimerQueue();
|
||||
if (!g_hTimerQueue) {
|
||||
RcxPayloadCleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CreateTimerQueueTimer(&g_hPollTimer, g_hTimerQueue,
|
||||
RcxPollTimerCallback, nullptr,
|
||||
0, /* start immediately */
|
||||
10, /* 10ms repeat */
|
||||
WT_EXECUTEDEFAULT)) {
|
||||
RcxPayloadCleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
/* mark ready */
|
||||
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── DllMain — minimal, no heavy work under loader lock ───────────── */
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID)
|
||||
{
|
||||
if (reason == DLL_PROCESS_DETACH) {
|
||||
RcxPayloadCleanup();
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
#else
|
||||
/* ===================================================================
|
||||
* LINUX implementation
|
||||
* =================================================================== */
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <semaphore.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <signal.h>
|
||||
|
||||
/* ── globals ──────────────────────────────────────────────────────── */
|
||||
static int g_shmFd = -1;
|
||||
static void* g_mappedView = nullptr;
|
||||
static sem_t* g_reqSem = SEM_FAILED;
|
||||
static sem_t* g_rspSem = SEM_FAILED;
|
||||
static pthread_t g_thread;
|
||||
static volatile int g_shutdown = 0;
|
||||
static volatile int g_threadRunning = 0;
|
||||
static int g_memFd = -1; /* /proc/self/mem for safe access */
|
||||
static char g_shmName[128];
|
||||
static char g_reqName[128];
|
||||
static char g_rspName[128];
|
||||
|
||||
/* ── safe memory access via /proc/self/mem ────────────────────────── */
|
||||
|
||||
static void safe_read(uint64_t addr, void* dest, uint32_t len, uint32_t* status)
|
||||
{
|
||||
ssize_t n = pread(g_memFd, dest, len, (off_t)addr);
|
||||
if (n < (ssize_t)len) {
|
||||
if (n > 0)
|
||||
memset((uint8_t*)dest + n, 0, len - (uint32_t)n);
|
||||
else
|
||||
memset(dest, 0, len);
|
||||
*status = RCX_RPC_STATUS_PARTIAL;
|
||||
}
|
||||
}
|
||||
|
||||
static void safe_write(uint64_t addr, const void* src, uint32_t len, uint32_t* status)
|
||||
{
|
||||
ssize_t n = pwrite(g_memFd, src, len, (off_t)addr);
|
||||
if (n < (ssize_t)len)
|
||||
*status = RCX_RPC_STATUS_ERROR;
|
||||
}
|
||||
|
||||
/* ── command handlers ─────────────────────────────────────────────── */
|
||||
|
||||
static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
auto* entries = reinterpret_cast<RcxRpcReadEntry*>(data);
|
||||
for (uint32_t i = 0; i < hdr->requestCount; ++i) {
|
||||
uint8_t* dest = data + entries[i].dataOffset;
|
||||
safe_read(entries[i].address, dest, entries[i].length, &hdr->status);
|
||||
}
|
||||
hdr->responseCount = hdr->requestCount;
|
||||
}
|
||||
|
||||
static void handle_write(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
safe_write(hdr->writeAddress, data, hdr->writeLength, &hdr->status);
|
||||
}
|
||||
|
||||
static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
|
||||
{
|
||||
FILE* f = fopen("/proc/self/maps", "r");
|
||||
if (!f) {
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
hdr->responseCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
/* first pass: collect unique file-backed mappings */
|
||||
struct ModRange { uint64_t base; uint64_t end; char path[512]; };
|
||||
static ModRange modules[512]; /* static to avoid large stack alloc */
|
||||
int modCount = 0;
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f) && modCount < 512) {
|
||||
uint64_t start, end;
|
||||
char perms[8] = {}, path[512] = {};
|
||||
if (sscanf(line, "%lx-%lx %7s %*x %*x:%*x %*u %511[^\n]",
|
||||
&start, &end, perms, path) < 4)
|
||||
continue;
|
||||
|
||||
/* skip non-file / special mappings */
|
||||
/* trim leading whitespace from path */
|
||||
char* p = path;
|
||||
while (*p == ' ' || *p == '\t') ++p;
|
||||
if (*p != '/') continue;
|
||||
if (strncmp(p, "/dev/", 5) == 0) continue;
|
||||
if (strncmp(p, "/memfd:", 7) == 0) continue;
|
||||
|
||||
bool found = false;
|
||||
for (int i = 0; i < modCount; ++i) {
|
||||
if (strcmp(modules[i].path, p) == 0) {
|
||||
if (start < modules[i].base) modules[i].base = start;
|
||||
if (end > modules[i].end) modules[i].end = end;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
modules[modCount].base = start;
|
||||
modules[modCount].end = end;
|
||||
strncpy(modules[modCount].path, p, 511);
|
||||
modules[modCount].path[511] = '\0';
|
||||
++modCount;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
/* write entries + name strings into data region */
|
||||
uint32_t entryBytes = (uint32_t)(modCount * sizeof(RcxRpcModuleEntry));
|
||||
uint32_t nameDataOff = entryBytes;
|
||||
|
||||
for (int i = 0; i < modCount; ++i) {
|
||||
const char* basename = strrchr(modules[i].path, '/');
|
||||
basename = basename ? basename + 1 : modules[i].path;
|
||||
uint32_t nameLen = (uint32_t)strlen(basename);
|
||||
|
||||
auto* entry = reinterpret_cast<RcxRpcModuleEntry*>(
|
||||
data + (uint32_t)i * sizeof(RcxRpcModuleEntry));
|
||||
entry->base = modules[i].base;
|
||||
entry->size = modules[i].end - modules[i].base;
|
||||
entry->nameOffset = nameDataOff;
|
||||
entry->nameLength = nameLen;
|
||||
|
||||
if (nameDataOff + nameLen <= RCX_RPC_DATA_SIZE) {
|
||||
memcpy(data + nameDataOff, basename, nameLen);
|
||||
nameDataOff += nameLen;
|
||||
}
|
||||
}
|
||||
|
||||
hdr->responseCount = (uint32_t)modCount;
|
||||
hdr->totalDataUsed = nameDataOff;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
/* ── server thread ────────────────────────────────────────────────── */
|
||||
|
||||
static void* server_thread_func(void*)
|
||||
{
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
__atomic_store_n(&hdr->payloadReady, 1, __ATOMIC_RELEASE);
|
||||
|
||||
while (!__atomic_load_n(&g_shutdown, __ATOMIC_ACQUIRE)) {
|
||||
/* timed wait: 250ms */
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_nsec += 250000000;
|
||||
if (ts.tv_nsec >= 1000000000) {
|
||||
ts.tv_sec += 1;
|
||||
ts.tv_nsec -= 1000000000;
|
||||
}
|
||||
|
||||
int rc = sem_timedwait(g_reqSem, &ts);
|
||||
if (rc != 0) {
|
||||
if (errno == ETIMEDOUT) continue;
|
||||
break;
|
||||
}
|
||||
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
switch (static_cast<RcxRpcCommand>(hdr->command)) {
|
||||
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
|
||||
case RPC_CMD_WRITE: handle_write(hdr, data); break;
|
||||
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
|
||||
case RPC_CMD_PING: break;
|
||||
case RPC_CMD_SHUTDOWN:
|
||||
__atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE);
|
||||
break;
|
||||
default:
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
sem_post(g_rspSem);
|
||||
|
||||
if (static_cast<RcxRpcCommand>(hdr->command) == RPC_CMD_SHUTDOWN)
|
||||
break;
|
||||
}
|
||||
|
||||
__atomic_store_n(&hdr->payloadReady, 0, __ATOMIC_RELEASE);
|
||||
__atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* ── init / cleanup ───────────────────────────────────────────────── */
|
||||
|
||||
static void payload_cleanup()
|
||||
{
|
||||
__atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE);
|
||||
|
||||
/* wake the thread if blocked */
|
||||
if (g_reqSem != SEM_FAILED) sem_post(g_reqSem);
|
||||
|
||||
if (__atomic_load_n(&g_threadRunning, __ATOMIC_ACQUIRE)) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_sec += 2;
|
||||
pthread_timedjoin_np(g_thread, nullptr, &ts);
|
||||
}
|
||||
|
||||
if (g_mappedView && g_mappedView != MAP_FAILED) {
|
||||
munmap(g_mappedView, RCX_RPC_SHM_SIZE);
|
||||
g_mappedView = nullptr;
|
||||
}
|
||||
if (g_shmFd >= 0) { close(g_shmFd); g_shmFd = -1; }
|
||||
if (g_reqSem != SEM_FAILED) { sem_close(g_reqSem); g_reqSem = SEM_FAILED; }
|
||||
if (g_rspSem != SEM_FAILED) { sem_close(g_rspSem); g_rspSem = SEM_FAILED; }
|
||||
|
||||
/* unlink named objects */
|
||||
if (g_shmName[0]) shm_unlink(g_shmName);
|
||||
if (g_reqName[0]) sem_unlink(g_reqName);
|
||||
if (g_rspName[0]) sem_unlink(g_rspName);
|
||||
|
||||
if (g_memFd >= 0) { close(g_memFd); g_memFd = -1; }
|
||||
}
|
||||
|
||||
__attribute__((constructor))
|
||||
static void payload_init()
|
||||
{
|
||||
uint32_t pid = (uint32_t)getpid();
|
||||
|
||||
/* ── open /proc/self/mem for safe access ── */
|
||||
g_memFd = open("/proc/self/mem", O_RDWR);
|
||||
if (g_memFd < 0) return;
|
||||
|
||||
/* ── create main shared memory (PID-only naming) ── */
|
||||
rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid);
|
||||
rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid);
|
||||
rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid);
|
||||
|
||||
g_shmFd = shm_open(g_shmName, O_CREAT | O_RDWR, 0600);
|
||||
if (g_shmFd < 0) return;
|
||||
if (ftruncate(g_shmFd, RCX_RPC_SHM_SIZE) != 0) {
|
||||
close(g_shmFd); g_shmFd = -1; return;
|
||||
}
|
||||
|
||||
g_mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, g_shmFd, 0);
|
||||
if (g_mappedView == MAP_FAILED) {
|
||||
g_mappedView = nullptr;
|
||||
close(g_shmFd); g_shmFd = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
hdr->version = RCX_RPC_VERSION;
|
||||
|
||||
/* image base from /proc/self/maps: first executable mapping */
|
||||
{
|
||||
FILE* f = fopen("/proc/self/maps", "r");
|
||||
if (f) {
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
uint64_t start;
|
||||
char perms[8] = {};
|
||||
if (sscanf(line, "%lx-%*x %7s", &start, perms) >= 2 && perms[2] == 'x') {
|
||||
hdr->imageBase = start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── create semaphores ── */
|
||||
g_reqSem = sem_open(g_reqName, O_CREAT, 0600, 0);
|
||||
g_rspSem = sem_open(g_rspName, O_CREAT, 0600, 0);
|
||||
if (g_reqSem == SEM_FAILED || g_rspSem == SEM_FAILED) {
|
||||
payload_cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
/* ── start server thread (it will set payloadReady = 1) ── */
|
||||
__atomic_store_n(&g_threadRunning, 1, __ATOMIC_RELEASE);
|
||||
if (pthread_create(&g_thread, nullptr, server_thread_func, nullptr) != 0) {
|
||||
__atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE);
|
||||
payload_cleanup();
|
||||
return;
|
||||
}
|
||||
pthread_detach(g_thread);
|
||||
}
|
||||
|
||||
__attribute__((destructor))
|
||||
static void payload_deinit()
|
||||
{
|
||||
payload_cleanup();
|
||||
}
|
||||
|
||||
#endif /* _WIN32 / linux */
|
||||
115
plugins/RemoteProcessMemory/rcx_rpc_protocol.h
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* RCX RPC Protocol -- shared between plugin DLL and payload DLL/SO.
|
||||
* No dependencies beyond standard C headers.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── constants ─────────────────────────────────────────────────────── */
|
||||
#define RCX_RPC_VERSION 1
|
||||
#define RCX_RPC_MAX_BATCH 256
|
||||
#define RCX_RPC_SHM_SIZE (1024 * 1024) /* 1 MB */
|
||||
#define RCX_RPC_HEADER_SIZE 4096
|
||||
#define RCX_RPC_DATA_OFFSET RCX_RPC_HEADER_SIZE
|
||||
#define RCX_RPC_DATA_SIZE (RCX_RPC_SHM_SIZE - RCX_RPC_DATA_OFFSET)
|
||||
|
||||
/* status codes */
|
||||
#define RCX_RPC_STATUS_OK 0
|
||||
#define RCX_RPC_STATUS_ERROR 1
|
||||
#define RCX_RPC_STATUS_PARTIAL 2
|
||||
|
||||
/* ── commands ──────────────────────────────────────────────────────── */
|
||||
#ifdef __cplusplus
|
||||
enum RcxRpcCommand : uint32_t {
|
||||
#else
|
||||
typedef uint32_t RcxRpcCommand;
|
||||
enum {
|
||||
#endif
|
||||
RPC_CMD_NONE = 0,
|
||||
RPC_CMD_READ_BATCH = 1, /* batch read: N {address, length} pairs */
|
||||
RPC_CMD_WRITE = 2, /* single write */
|
||||
RPC_CMD_ENUM_MODULES = 3, /* enumerate loaded modules */
|
||||
RPC_CMD_PING = 4, /* heartbeat */
|
||||
RPC_CMD_SHUTDOWN = 5, /* graceful teardown */
|
||||
};
|
||||
|
||||
/* ── wire structs (natural alignment, verified by static_assert) ─── */
|
||||
|
||||
struct RcxRpcReadEntry {
|
||||
uint64_t address;
|
||||
uint32_t length;
|
||||
uint32_t dataOffset; /* offset into data region for response bytes */
|
||||
};
|
||||
|
||||
struct RcxRpcModuleEntry {
|
||||
uint64_t base;
|
||||
uint64_t size;
|
||||
uint32_t nameOffset; /* offset into data region, UTF-16 on Win, UTF-8 on Linux */
|
||||
uint32_t nameLength; /* in bytes */
|
||||
};
|
||||
|
||||
/*
|
||||
* Header -- lives at shared-memory offset 0, padded to 4096 bytes.
|
||||
*
|
||||
* offset field
|
||||
* ------ -----
|
||||
* 0 version (4)
|
||||
* 4 payloadReady (4)
|
||||
* 8 command (4)
|
||||
* 12 requestCount (4)
|
||||
* 16 writeAddress (8)
|
||||
* 24 writeLength (4)
|
||||
* 28 status (4)
|
||||
* 32 responseCount (4)
|
||||
* 36 totalDataUsed (4)
|
||||
* 40 imageBase (8) -- main module base from PEB / procfs
|
||||
* 48 pointerSize (4) -- 4 for 32-bit, 8 for 64-bit payload
|
||||
* 52 _pad[4044]
|
||||
*/
|
||||
struct RcxRpcHeader {
|
||||
uint32_t version;
|
||||
uint32_t payloadReady; /* payload sets to 1 after init */
|
||||
uint32_t command; /* RcxRpcCommand */
|
||||
uint32_t requestCount;
|
||||
uint64_t writeAddress;
|
||||
uint32_t writeLength;
|
||||
uint32_t status; /* RCX_RPC_STATUS_* */
|
||||
uint32_t responseCount;
|
||||
uint32_t totalDataUsed;
|
||||
uint64_t imageBase; /* main module base (PEB on Win, /proc on Linux) */
|
||||
uint32_t pointerSize; /* 4 for 32-bit, 8 for 64-bit payload */
|
||||
uint8_t _pad[RCX_RPC_HEADER_SIZE - 52];
|
||||
};
|
||||
|
||||
/* ── name formatting helpers (PID-only, no nonce) ─────────────────── */
|
||||
|
||||
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_SHM_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_shm_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_REQ_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_req_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_RSP_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_rsp_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
|
||||
#endif
|
||||
593
plugins/RemoteProcessMemory/tests/test_rpc_client.cpp
Normal file
@@ -0,0 +1,593 @@
|
||||
/*
|
||||
* test_rpc_client -- connects to a running test_rpc_host (or spawns one),
|
||||
* exercises every RPC command, and benchmarks throughput.
|
||||
*
|
||||
* Usage:
|
||||
* test_rpc_client (auto-spawn host)
|
||||
* test_rpc_client <pid> [testbuf_hex testlen]
|
||||
*/
|
||||
|
||||
#include "../rcx_rpc_protocol.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <assert.h>
|
||||
#include <chrono>
|
||||
|
||||
#ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
# include <fcntl.h>
|
||||
# include <sys/mman.h>
|
||||
# include <semaphore.h>
|
||||
# include <libgen.h>
|
||||
# include <limits.h>
|
||||
#endif
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Minimal standalone IPC client (no Qt, mirrors plugin's IpcClient)
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
struct TestIpcClient {
|
||||
#ifdef _WIN32
|
||||
HANDLE hShm = nullptr;
|
||||
HANDLE hReqEvent = nullptr;
|
||||
HANDLE hRspEvent = nullptr;
|
||||
#else
|
||||
int shmFd = -1;
|
||||
sem_t* reqSem = SEM_FAILED;
|
||||
sem_t* rspSem = SEM_FAILED;
|
||||
#endif
|
||||
void* view = nullptr;
|
||||
bool ok = false;
|
||||
|
||||
bool connect(uint32_t pid, int timeoutMs = 5000)
|
||||
{
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
ULONGLONG deadline = GetTickCount64() + (ULONGLONG)timeoutMs;
|
||||
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
|
||||
if (GetTickCount64() >= deadline) return false;
|
||||
Sleep(10);
|
||||
}
|
||||
view = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
|
||||
if (!view) { CloseHandle(hShm); hShm = nullptr; return false; }
|
||||
|
||||
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
|
||||
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
|
||||
if (!hReqEvent || !hRspEvent) return false;
|
||||
#else
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
while (true) {
|
||||
shmFd = shm_open(shmName, O_RDWR, 0);
|
||||
if (shmFd >= 0) break;
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) return false;
|
||||
usleep(10000);
|
||||
}
|
||||
view = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, shmFd, 0);
|
||||
if (view == MAP_FAILED) { view = nullptr; close(shmFd); shmFd = -1; return false; }
|
||||
|
||||
reqSem = sem_open(reqName, 0);
|
||||
rspSem = sem_open(rspName, 0);
|
||||
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) return false;
|
||||
#endif
|
||||
/* wait for payloadReady */
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
#ifdef _WIN32
|
||||
while (!hdr->payloadReady) {
|
||||
if (GetTickCount64() >= deadline) return false;
|
||||
Sleep(5);
|
||||
}
|
||||
#else
|
||||
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
if (elapsed >= timeoutMs) return false;
|
||||
usleep(5000);
|
||||
}
|
||||
#endif
|
||||
ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnect()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (view) { UnmapViewOfFile(view); view = nullptr; }
|
||||
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
|
||||
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
|
||||
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
|
||||
#else
|
||||
if (view) { munmap(view, RCX_RPC_SHM_SIZE); view = nullptr; }
|
||||
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
|
||||
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
|
||||
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
|
||||
#endif
|
||||
ok = false;
|
||||
}
|
||||
|
||||
bool signalAndWait(int timeoutMs = 2000)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
SetEvent(hReqEvent);
|
||||
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
|
||||
#else
|
||||
sem_post(reqSem);
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_sec += timeoutMs / 1000;
|
||||
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
|
||||
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
|
||||
return sem_timedwait(rspSem, &ts) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── RPC helpers ──────────────────────────────────────────────── */
|
||||
|
||||
bool rpc_ping()
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
hdr->command = RPC_CMD_PING;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
return signalAndWait();
|
||||
}
|
||||
|
||||
bool rpc_read(uint64_t addr, void* buf, uint32_t len)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_READ_BATCH;
|
||||
hdr->requestCount = 1;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
auto* entry = (RcxRpcReadEntry*)data;
|
||||
entry->address = addr;
|
||||
entry->length = len;
|
||||
entry->dataOffset = sizeof(RcxRpcReadEntry);
|
||||
|
||||
if (!signalAndWait()) return false;
|
||||
memcpy(buf, data + entry->dataOffset, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool rpc_read_batch(const uint64_t* addrs, const uint32_t* lens,
|
||||
uint32_t count, uint8_t* outBuf)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_READ_BATCH;
|
||||
hdr->requestCount = count;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
/* lay out entries, then data offsets after all entries */
|
||||
uint32_t entriesSize = count * (uint32_t)sizeof(RcxRpcReadEntry);
|
||||
uint32_t dataOff = entriesSize;
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry));
|
||||
e->address = addrs[i];
|
||||
e->length = lens[i];
|
||||
e->dataOffset = dataOff;
|
||||
dataOff += lens[i];
|
||||
}
|
||||
|
||||
if (!signalAndWait()) return false;
|
||||
|
||||
/* copy out response data */
|
||||
uint32_t off = 0;
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry));
|
||||
memcpy(outBuf + off, data + e->dataOffset, e->length);
|
||||
off += e->length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool rpc_write(uint64_t addr, const void* buf, uint32_t len)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_WRITE;
|
||||
hdr->writeAddress = addr;
|
||||
hdr->writeLength = len;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
memcpy(data, buf, len);
|
||||
|
||||
if (!signalAndWait()) return false;
|
||||
return hdr->status == RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
struct ModInfo { uint64_t base; uint64_t size; char name[256]; };
|
||||
|
||||
int rpc_enum_modules(ModInfo* out, int maxOut)
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
|
||||
|
||||
hdr->command = RPC_CMD_ENUM_MODULES;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
if (!signalAndWait()) return -1;
|
||||
if (hdr->status != RCX_RPC_STATUS_OK) return -1;
|
||||
|
||||
int count = (int)hdr->responseCount;
|
||||
if (count > maxOut) count = maxOut;
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto* entry = (RcxRpcModuleEntry*)(data + i * sizeof(RcxRpcModuleEntry));
|
||||
out[i].base = entry->base;
|
||||
out[i].size = entry->size;
|
||||
#ifdef _WIN32
|
||||
/* names are UTF-16 on Windows */
|
||||
int wchars = (int)(entry->nameLength / sizeof(wchar_t));
|
||||
WideCharToMultiByte(CP_UTF8, 0,
|
||||
(const wchar_t*)(data + entry->nameOffset), wchars,
|
||||
out[i].name, 255, nullptr, nullptr);
|
||||
out[i].name[255] = '\0';
|
||||
#else
|
||||
int nLen = (int)entry->nameLength;
|
||||
if (nLen > 255) nLen = 255;
|
||||
memcpy(out[i].name, data + entry->nameOffset, nLen);
|
||||
out[i].name[nLen] = '\0';
|
||||
#endif
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void rpc_shutdown()
|
||||
{
|
||||
auto* hdr = (RcxRpcHeader*)view;
|
||||
hdr->command = RPC_CMD_SHUTDOWN;
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
signalAndWait(500);
|
||||
}
|
||||
};
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Auto-spawn host
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
#ifdef _WIN32
|
||||
static HANDLE g_hostProcess = nullptr;
|
||||
#else
|
||||
static pid_t g_hostPid = 0;
|
||||
#endif
|
||||
static FILE* g_hostPipe = nullptr;
|
||||
|
||||
static bool spawn_host(uint32_t* outPid,
|
||||
uint64_t* outTestBuf, uint32_t* outTestLen)
|
||||
{
|
||||
/* resolve path to test_rpc_host next to ourselves */
|
||||
char cmd[2048];
|
||||
#ifdef _WIN32
|
||||
char exePath[MAX_PATH];
|
||||
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
|
||||
char* slash = strrchr(exePath, '\\');
|
||||
if (!slash) slash = strrchr(exePath, '/');
|
||||
if (slash) *(slash + 1) = '\0';
|
||||
snprintf(cmd, sizeof(cmd), "\"%stest_rpc_host.exe\" autotest", exePath);
|
||||
g_hostPipe = _popen(cmd, "r");
|
||||
#else
|
||||
char exePath[PATH_MAX];
|
||||
ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
|
||||
if (n <= 0) return false;
|
||||
exePath[n] = '\0';
|
||||
char* dir = dirname(exePath);
|
||||
snprintf(cmd, sizeof(cmd), "%s/test_rpc_host autotest", dir);
|
||||
g_hostPipe = popen(cmd, "r");
|
||||
#endif
|
||||
if (!g_hostPipe) {
|
||||
fprintf(stderr, "ERROR: cannot spawn host: %s\n", cmd);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* read READY line */
|
||||
char line[512];
|
||||
if (!fgets(line, sizeof(line), g_hostPipe)) {
|
||||
fprintf(stderr, "ERROR: no output from host\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* parse: READY pid=X testbuf=0xZ testlen=N */
|
||||
unsigned long long tbuf = 0;
|
||||
unsigned tlen = 0;
|
||||
if (sscanf(line, "READY pid=%u testbuf=0x%llx testlen=%u",
|
||||
outPid, &tbuf, &tlen) < 1) {
|
||||
fprintf(stderr, "ERROR: cannot parse host output: %s\n", line);
|
||||
return false;
|
||||
}
|
||||
*outTestBuf = (uint64_t)tbuf;
|
||||
*outTestLen = (uint32_t)tlen;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cleanup_host()
|
||||
{
|
||||
if (g_hostPipe) {
|
||||
#ifdef _WIN32
|
||||
_pclose(g_hostPipe);
|
||||
#else
|
||||
pclose(g_hostPipe);
|
||||
#endif
|
||||
g_hostPipe = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* Printing helpers
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
static void print_pass(const char* name) { printf(" [PASS] %s\n", name); }
|
||||
static void print_fail(const char* name) { printf(" [FAIL] %s\n", name); exit(1); }
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
* main
|
||||
* ══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
uint32_t pid = 0;
|
||||
uint64_t testBuf = 0;
|
||||
uint32_t testLen = 0;
|
||||
bool autoMode = false;
|
||||
|
||||
if (argc >= 2) {
|
||||
pid = (uint32_t)atoi(argv[1]);
|
||||
if (argc >= 4) {
|
||||
testBuf = (uint64_t)strtoull(argv[2], nullptr, 0);
|
||||
testLen = (uint32_t)atoi(argv[3]);
|
||||
}
|
||||
} else {
|
||||
autoMode = true;
|
||||
printf("Auto-spawning test_rpc_host...\n");
|
||||
if (!spawn_host(&pid, &testBuf, &testLen)) return 1;
|
||||
}
|
||||
|
||||
printf("Connecting to PID=%u testbuf=0x%llx testlen=%u\n\n",
|
||||
pid, (unsigned long long)testBuf, testLen);
|
||||
|
||||
/* ── connect ── */
|
||||
TestIpcClient ipc;
|
||||
if (!ipc.connect(pid)) {
|
||||
fprintf(stderr, "ERROR: IPC connect failed\n");
|
||||
if (autoMode) cleanup_host();
|
||||
return 1;
|
||||
}
|
||||
printf("=== Functional Tests ===\n");
|
||||
|
||||
/* ── test: ping ── */
|
||||
if (ipc.rpc_ping()) print_pass("Ping");
|
||||
else print_fail("Ping");
|
||||
|
||||
/* ── test: enumerate modules ── */
|
||||
TestIpcClient::ModInfo mods[512];
|
||||
int modCount = ipc.rpc_enum_modules(mods, 512);
|
||||
if (modCount > 0) {
|
||||
printf(" [PASS] EnumModules (%d modules)\n", modCount);
|
||||
printf(" first: %s base=0x%llx size=0x%llx\n",
|
||||
mods[0].name,
|
||||
(unsigned long long)mods[0].base,
|
||||
(unsigned long long)mods[0].size);
|
||||
} else {
|
||||
print_fail("EnumModules");
|
||||
}
|
||||
|
||||
/* ── test: read module header (MZ / ELF magic) ── */
|
||||
if (modCount > 0) {
|
||||
uint8_t header[4] = {};
|
||||
if (ipc.rpc_read(mods[0].base, header, 4)) {
|
||||
#ifdef _WIN32
|
||||
if (header[0] == 'M' && header[1] == 'Z')
|
||||
print_pass("ReadModuleHeader (MZ)");
|
||||
else
|
||||
print_fail("ReadModuleHeader (expected MZ)");
|
||||
#else
|
||||
if (header[0] == 0x7F && header[1] == 'E' &&
|
||||
header[2] == 'L' && header[3] == 'F')
|
||||
print_pass("ReadModuleHeader (ELF)");
|
||||
else
|
||||
print_fail("ReadModuleHeader (expected ELF)");
|
||||
#endif
|
||||
} else {
|
||||
print_fail("ReadModuleHeader (read failed)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── test: read test buffer (known pattern) ── */
|
||||
if (testBuf && testLen >= 4096) {
|
||||
uint8_t buf[4096];
|
||||
if (ipc.rpc_read(testBuf, buf, 4096)) {
|
||||
bool good = true;
|
||||
for (int i = 0; i < 4096; ++i) {
|
||||
if (buf[i] != (uint8_t)(i & 0xFF)) { good = false; break; }
|
||||
}
|
||||
if (good) print_pass("ReadTestBuffer (4096 bytes, pattern verified)");
|
||||
else print_fail("ReadTestBuffer (pattern mismatch)");
|
||||
} else {
|
||||
print_fail("ReadTestBuffer (read failed)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── test: write ── */
|
||||
if (testBuf && testLen >= 16) {
|
||||
uint8_t patch[4] = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
if (ipc.rpc_write(testBuf, patch, 4)) {
|
||||
uint8_t verify[4] = {};
|
||||
ipc.rpc_read(testBuf, verify, 4);
|
||||
if (memcmp(verify, patch, 4) == 0)
|
||||
print_pass("Write + ReadBack (0xDEADBEEF)");
|
||||
else
|
||||
print_fail("Write + ReadBack (readback mismatch)");
|
||||
} else {
|
||||
print_fail("Write (write failed)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ── test: batch read ── */
|
||||
if (testBuf && testLen >= 8192) {
|
||||
const uint32_t N = 4;
|
||||
uint64_t addrs[N];
|
||||
uint32_t lens[N];
|
||||
for (uint32_t i = 0; i < N; ++i) {
|
||||
addrs[i] = testBuf + i * 1024;
|
||||
lens[i] = 1024;
|
||||
}
|
||||
uint8_t out[4096];
|
||||
if (ipc.rpc_read_batch(addrs, lens, N, out)) {
|
||||
print_pass("BatchRead (4 x 1024 bytes)");
|
||||
} else {
|
||||
print_fail("BatchRead");
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n=== Benchmarks ===\n");
|
||||
|
||||
/* choose a valid address for benchmarking */
|
||||
uint64_t benchAddr = testBuf ? testBuf : (modCount > 0 ? mods[0].base : 0);
|
||||
if (!benchAddr) {
|
||||
printf(" (no valid address for benchmarks, skipping)\n");
|
||||
} else {
|
||||
|
||||
/* ── benchmark: single 4 KB reads ── */
|
||||
{
|
||||
const int ITERS = 10000;
|
||||
const int PAGE = 4096;
|
||||
uint8_t tmp[4096];
|
||||
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_read(benchAddr, tmp, PAGE);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0);
|
||||
|
||||
printf(" Single 4 KB reads:\n");
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f MB\n", totalMB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
|
||||
printf(" Avg latency: %.2f us/read\n", us / ITERS);
|
||||
}
|
||||
|
||||
/* ── benchmark: single 64 B reads (pointer-chase-size) ── */
|
||||
{
|
||||
const int ITERS = 50000;
|
||||
const int SZ = 64;
|
||||
uint8_t tmp[64];
|
||||
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_read(benchAddr, tmp, SZ);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalKB = (double)ITERS * SZ / 1024.0;
|
||||
|
||||
printf(" Single 64 B reads (pointer-chase):\n");
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f KB\n", totalKB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f KB/s\n", totalKB / secs);
|
||||
printf(" Avg latency: %.2f us/read\n", us / ITERS);
|
||||
}
|
||||
|
||||
/* ── benchmark: batch read (50 x 4 KB, simulating refresh) ── */
|
||||
{
|
||||
const int ITERS = 2000;
|
||||
const uint32_t BATCH = 50;
|
||||
const uint32_t PAGE = 4096;
|
||||
|
||||
uint64_t addrs[BATCH];
|
||||
uint32_t lens[BATCH];
|
||||
for (uint32_t i = 0; i < BATCH; ++i) {
|
||||
/* wrap within test buffer or module */
|
||||
addrs[i] = benchAddr + (i * PAGE) % 65536;
|
||||
lens[i] = PAGE;
|
||||
}
|
||||
|
||||
/* allocate response buffer */
|
||||
uint8_t* outBuf = (uint8_t*)malloc(BATCH * PAGE);
|
||||
if (!outBuf) {
|
||||
printf(" (batch malloc failed, skipping)\n");
|
||||
} else {
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_read_batch(addrs, lens, BATCH, outBuf);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalMB = (double)ITERS * BATCH * PAGE / (1024.0 * 1024.0);
|
||||
|
||||
printf(" Batch read (%u x %u B, simulating refresh):\n", BATCH, PAGE);
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f MB\n", totalMB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
|
||||
printf(" Avg latency: %.2f us/batch\n", us / ITERS);
|
||||
printf(" Per-page : %.2f us/page\n", us / (ITERS * BATCH));
|
||||
|
||||
free(outBuf);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── benchmark: write 4 KB ── */
|
||||
if (testBuf && testLen >= 4096) {
|
||||
const int ITERS = 10000;
|
||||
const int PAGE = 4096;
|
||||
uint8_t tmp[4096];
|
||||
memset(tmp, 0x42, sizeof(tmp));
|
||||
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < ITERS; ++i)
|
||||
ipc.rpc_write(testBuf, tmp, PAGE);
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
double secs = us / 1e6;
|
||||
double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0);
|
||||
|
||||
printf(" Write 4 KB:\n");
|
||||
printf(" Iterations : %d\n", ITERS);
|
||||
printf(" Total data : %.2f MB\n", totalMB);
|
||||
printf(" Wall time : %.3f s\n", secs);
|
||||
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
|
||||
printf(" Avg latency: %.2f us/write\n", us / ITERS);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── shutdown ── */
|
||||
printf("\nSending shutdown...\n");
|
||||
ipc.rpc_shutdown();
|
||||
ipc.disconnect();
|
||||
|
||||
if (autoMode) {
|
||||
/* wait for host to exit */
|
||||
#ifdef _WIN32
|
||||
Sleep(500);
|
||||
#else
|
||||
usleep(500000);
|
||||
#endif
|
||||
cleanup_host();
|
||||
}
|
||||
|
||||
printf("Done.\n");
|
||||
return 0;
|
||||
}
|
||||
187
plugins/RemoteProcessMemory/tests/test_rpc_host.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* test_rpc_host -- loads rcx_payload in-process, acts as the "target".
|
||||
*
|
||||
* Usage: test_rpc_host
|
||||
*
|
||||
* Prints a READY line (machine-parseable), then waits for the payload
|
||||
* to shut down (RPC_CMD_SHUTDOWN from the client).
|
||||
*/
|
||||
|
||||
#include "../rcx_rpc_protocol.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <unistd.h>
|
||||
# include <dlfcn.h>
|
||||
# include <fcntl.h>
|
||||
# include <sys/mman.h>
|
||||
# include <semaphore.h>
|
||||
# include <libgen.h>
|
||||
# include <limits.h>
|
||||
#endif
|
||||
|
||||
/* ── Helpers ──────────────────────────────────────────────────────── */
|
||||
|
||||
static uint32_t current_pid()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return (uint32_t)GetCurrentProcessId();
|
||||
#else
|
||||
return (uint32_t)getpid();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void sleep_ms(int ms)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
Sleep((DWORD)ms);
|
||||
#else
|
||||
usleep((useconds_t)ms * 1000);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Resolve payload path relative to this executable */
|
||||
static int payload_path(char* out, int outLen)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
char exePath[MAX_PATH];
|
||||
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
|
||||
char* slash = strrchr(exePath, '\\');
|
||||
if (!slash) slash = strrchr(exePath, '/');
|
||||
if (slash) *(slash + 1) = '\0';
|
||||
snprintf(out, outLen, "%srcx_payload.dll", exePath);
|
||||
#else
|
||||
char exePath[PATH_MAX];
|
||||
ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
|
||||
if (n <= 0) return -1;
|
||||
exePath[n] = '\0';
|
||||
char* dir = dirname(exePath);
|
||||
snprintf(out, outLen, "%s/rcx_payload.so", dir);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Open the main shared memory (read-only, just to monitor payloadReady) */
|
||||
static void* open_main_shm(uint32_t pid)
|
||||
{
|
||||
char shmName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE h = nullptr;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
h = OpenFileMappingA(FILE_MAP_READ, FALSE, shmName);
|
||||
if (h) break;
|
||||
sleep_ms(10);
|
||||
}
|
||||
if (!h) return nullptr;
|
||||
void* v = MapViewOfFile(h, FILE_MAP_READ, 0, 0, sizeof(RcxRpcHeader));
|
||||
return v;
|
||||
#else
|
||||
int fd = -1;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
fd = shm_open(shmName, O_RDONLY, 0);
|
||||
if (fd >= 0) break;
|
||||
sleep_ms(10);
|
||||
}
|
||||
if (fd < 0) return nullptr;
|
||||
void* v = mmap(nullptr, sizeof(RcxRpcHeader), PROT_READ, MAP_SHARED, fd, 0);
|
||||
close(fd);
|
||||
return (v == MAP_FAILED) ? nullptr : v;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── Test buffer (known pattern for client to verify reads/writes) ── */
|
||||
static uint8_t g_testBuf[65536];
|
||||
|
||||
/* ── main ─────────────────────────────────────────────────────────── */
|
||||
|
||||
int main(int, char**)
|
||||
{
|
||||
uint32_t pid = current_pid();
|
||||
|
||||
/* fill test buffer with known pattern */
|
||||
for (int i = 0; i < (int)sizeof(g_testBuf); ++i)
|
||||
g_testBuf[i] = (uint8_t)(i & 0xFF);
|
||||
|
||||
/* load payload */
|
||||
char plPath[1024];
|
||||
if (payload_path(plPath, sizeof(plPath)) != 0) {
|
||||
fprintf(stderr, "ERROR: cannot determine payload path\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
HMODULE hPayload = LoadLibraryA(plPath);
|
||||
if (!hPayload) {
|
||||
fprintf(stderr, "ERROR: LoadLibrary(%s) failed (%lu)\n",
|
||||
plPath, GetLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Call RcxPayloadInit() — DllMain is minimal, init must be explicit */
|
||||
typedef bool (*RcxPayloadInitFn)();
|
||||
auto pfnInit = (RcxPayloadInitFn)GetProcAddress(hPayload, "RcxPayloadInit");
|
||||
if (!pfnInit || !pfnInit()) {
|
||||
fprintf(stderr, "ERROR: RcxPayloadInit() failed or not found\n");
|
||||
FreeLibrary(hPayload);
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
void* hPayload = dlopen(plPath, RTLD_NOW);
|
||||
if (!hPayload) {
|
||||
fprintf(stderr, "ERROR: dlopen(%s): %s\n", plPath, dlerror());
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* open main shm and wait for payloadReady */
|
||||
void* shmView = open_main_shm(pid);
|
||||
if (!shmView) {
|
||||
fprintf(stderr, "ERROR: failed to open main shared memory\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
RcxRpcHeader* hdr = (RcxRpcHeader*)shmView;
|
||||
for (int i = 0; i < 500; ++i) {
|
||||
if (hdr->payloadReady) break;
|
||||
sleep_ms(10);
|
||||
}
|
||||
if (!hdr->payloadReady) {
|
||||
fprintf(stderr, "ERROR: payload did not become ready\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* print READY line for the client to parse */
|
||||
printf("READY pid=%u testbuf=0x%llx testlen=%u\n",
|
||||
pid,
|
||||
(unsigned long long)(uintptr_t)g_testBuf,
|
||||
(unsigned)sizeof(g_testBuf));
|
||||
fflush(stdout);
|
||||
|
||||
/* wait until payload shuts down */
|
||||
while (hdr->payloadReady)
|
||||
sleep_ms(100);
|
||||
|
||||
printf("Payload shut down, exiting.\n");
|
||||
|
||||
#ifdef _WIN32
|
||||
/* give the timer queue a moment to drain */
|
||||
Sleep(200);
|
||||
FreeLibrary(hPayload);
|
||||
if (shmView) UnmapViewOfFile(shmView);
|
||||
#else
|
||||
usleep(200000);
|
||||
dlclose(hPayload);
|
||||
if (shmView) munmap(shmView, sizeof(RcxRpcHeader));
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
34
plugins/WinDbgMemory/CMakeLists.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(WinDbgMemoryPlugin 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
|
||||
WinDbgMemoryPlugin.h
|
||||
WinDbgMemoryPlugin.cpp
|
||||
)
|
||||
|
||||
# Create shared library (DLL)
|
||||
add_library(WinDbgMemoryPlugin SHARED ${PLUGIN_SOURCES})
|
||||
|
||||
# Link Qt + DbgEng
|
||||
target_link_libraries(WinDbgMemoryPlugin PRIVATE ${QT}::Widgets ole32)
|
||||
|
||||
# Include directories
|
||||
target_include_directories(WinDbgMemoryPlugin PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../src
|
||||
)
|
||||
|
||||
# Output to Plugins folder
|
||||
set_target_properties(WinDbgMemoryPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
777
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
Normal file
@@ -0,0 +1,777 @@
|
||||
#include "WinDbgMemoryPlugin.h"
|
||||
|
||||
#include <QStyle>
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QDialog>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QDebug>
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QSettings>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
#include <dbgeng.h>
|
||||
// dbgeng.dll is loaded dynamically — see loadDbgEngTools()
|
||||
|
||||
// The system dbgeng.dll (C:\Windows\System32) does not support remote
|
||||
// connections (DebugConnect returns 0x8007053d). The full version lives
|
||||
// in the Debugging Tools for Windows directory. We load it dynamically
|
||||
// so the plugin works without requiring the debugger tools on PATH.
|
||||
static const char* const kDbgToolsDirs[] = {
|
||||
"C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64",
|
||||
"C:\\Program Files\\Windows Kits\\10\\Debuggers\\x64",
|
||||
};
|
||||
static const char* const kSettingsKey = "WinDbgPlugin/DbgToolsDir";
|
||||
|
||||
typedef HRESULT (STDAPICALLTYPE *PFN_DebugConnect)(PCSTR, REFIID, PVOID*);
|
||||
typedef HRESULT (STDAPICALLTYPE *PFN_DebugCreate)(REFIID, PVOID*);
|
||||
|
||||
static QString s_loadedDir;
|
||||
static HMODULE s_hDbgEng = nullptr;
|
||||
|
||||
static HMODULE tryLoadFrom(const char* dir) {
|
||||
SetDllDirectoryA(dir);
|
||||
// Pre-load dependencies so the tools versions are used instead of
|
||||
// the older System32 copies (e.g. dbghelp.dll without StackWalk2).
|
||||
char path[MAX_PATH];
|
||||
for (auto dep : {"dbghelp.dll", "dbgcore.dll", "symsrv.dll"}) {
|
||||
snprintf(path, sizeof(path), "%s\\%s", dir, dep);
|
||||
LoadLibraryA(path); // OK if missing
|
||||
}
|
||||
snprintf(path, sizeof(path), "%s\\dbgeng.dll", dir);
|
||||
HMODULE h = LoadLibraryA(path);
|
||||
if (h) {
|
||||
s_loadedDir = QString::fromLocal8Bit(dir);
|
||||
qDebug() << "[WinDbg] Loaded dbgeng.dll from" << dir;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static HMODULE loadDbgEngTools() {
|
||||
if (s_hDbgEng) return s_hDbgEng;
|
||||
|
||||
// 1. Try user-configured path from settings
|
||||
QSettings settings;
|
||||
QString userDir = settings.value(kSettingsKey).toString();
|
||||
if (!userDir.isEmpty()) {
|
||||
s_hDbgEng = tryLoadFrom(userDir.toLocal8Bit().constData());
|
||||
if (s_hDbgEng) return s_hDbgEng;
|
||||
}
|
||||
|
||||
// 2. Try well-known install paths
|
||||
for (auto dir : kDbgToolsDirs) {
|
||||
s_hDbgEng = tryLoadFrom(dir);
|
||||
if (s_hDbgEng) return s_hDbgEng;
|
||||
}
|
||||
|
||||
SetDllDirectoryA(nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool dbgToolsFound() {
|
||||
loadDbgEngTools();
|
||||
return s_hDbgEng != nullptr;
|
||||
}
|
||||
|
||||
static PFN_DebugConnect getDebugConnect() {
|
||||
static PFN_DebugConnect pfn = nullptr;
|
||||
static bool tried = false;
|
||||
if (!tried) {
|
||||
tried = true;
|
||||
HMODULE h = loadDbgEngTools();
|
||||
if (h) pfn = (PFN_DebugConnect)GetProcAddress(h, "DebugConnect");
|
||||
if (!pfn) qWarning() << "[WinDbg] DebugConnect not available — Debugging Tools not found";
|
||||
}
|
||||
return pfn;
|
||||
}
|
||||
|
||||
static PFN_DebugCreate getDebugCreate() {
|
||||
static PFN_DebugCreate pfn = nullptr;
|
||||
static bool tried = false;
|
||||
if (!tried) {
|
||||
tried = true;
|
||||
HMODULE h = loadDbgEngTools();
|
||||
if (h) pfn = (PFN_DebugCreate)GetProcAddress(h, "DebugCreate");
|
||||
if (!pfn) qWarning() << "[WinDbg] DebugCreate not available — Debugging Tools not found";
|
||||
}
|
||||
return pfn;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Thread dispatch helper
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
template<typename Fn>
|
||||
void WinDbgMemoryProvider::dispatchToOwner(Fn&& fn) const
|
||||
{
|
||||
if (!m_dispatcher) { fn(); return; }
|
||||
|
||||
if (QThread::currentThread() == m_dispatcher->thread()) {
|
||||
// Already on the owning thread — call directly
|
||||
fn();
|
||||
} else {
|
||||
// Marshal to the owning thread and block until done
|
||||
QMetaObject::invokeMethod(m_dispatcher, std::forward<Fn>(fn),
|
||||
Qt::BlockingQueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// WinDbgMemoryProvider implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
WinDbgMemoryProvider::WinDbgMemoryProvider(const QString& target)
|
||||
{
|
||||
// Create a dedicated thread for all DbgEng COM operations.
|
||||
// DbgEng's remote transport (TCP/named-pipe) is thread-affine — all
|
||||
// calls must happen on the thread that called DebugConnect/DebugCreate.
|
||||
// A private thread with its own event loop guarantees:
|
||||
// 1. dispatchToOwner() works from any calling thread (main, thread-pool, etc.)
|
||||
// 2. No deadlock — the DbgEng thread is never blocked by the caller
|
||||
m_dbgThread = new QThread();
|
||||
m_dbgThread->setObjectName(QStringLiteral("DbgEngThread"));
|
||||
m_dbgThread->start();
|
||||
|
||||
m_dispatcher = new DbgEngDispatcher();
|
||||
m_dispatcher->moveToThread(m_dbgThread);
|
||||
|
||||
#ifdef _WIN32
|
||||
// Run all DbgEng initialization on the dedicated thread.
|
||||
// BlockingQueuedConnection blocks us until the lambda finishes,
|
||||
// so member variables written inside are visible after the call.
|
||||
dispatchToOwner([this, &target]() {
|
||||
HRESULT hr;
|
||||
|
||||
// COM must be initialized on this thread for DbgEng
|
||||
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
|
||||
qDebug() << "[WinDbg] Opening target:" << target
|
||||
<< "on DbgEng thread" << QThread::currentThread();
|
||||
|
||||
if (target.startsWith("tcp:", Qt::CaseInsensitive)
|
||||
|| target.startsWith("npipe:", Qt::CaseInsensitive))
|
||||
{
|
||||
// ── Remote: connect to existing WinDbg debug server ──
|
||||
auto pfnConnect = getDebugConnect();
|
||||
if (!pfnConnect) { qWarning() << "[WinDbg] Debugging Tools required for remote connections"; return; }
|
||||
QByteArray connUtf8 = target.toUtf8();
|
||||
qDebug() << "[WinDbg] DebugConnect:" << target;
|
||||
hr = pfnConnect(connUtf8.constData(), IID_IDebugClient, (void**)&m_client);
|
||||
qDebug() << "[WinDbg] DebugConnect hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "client=" << (void*)m_client;
|
||||
if (FAILED(hr) || !m_client) {
|
||||
qWarning() << "[WinDbg] DebugConnect FAILED hr=0x" << Qt::hex << (unsigned long)hr;
|
||||
return;
|
||||
}
|
||||
m_isRemote = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// ── Local: create debug client for pid/dump ──
|
||||
auto pfnCreate = getDebugCreate();
|
||||
if (!pfnCreate) { qWarning() << "[WinDbg] Debugging Tools required"; return; }
|
||||
hr = pfnCreate(IID_IDebugClient, (void**)&m_client);
|
||||
qDebug() << "[WinDbg] DebugCreate hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "client=" << (void*)m_client;
|
||||
if (FAILED(hr) || !m_client) {
|
||||
qWarning() << "[WinDbg] DebugCreate FAILED hr=0x" << Qt::hex << (unsigned long)hr;
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.startsWith("pid:", Qt::CaseInsensitive))
|
||||
{
|
||||
bool ok = false;
|
||||
ULONG pid = target.mid(4).trimmed().toULong(&ok);
|
||||
if (!ok || pid == 0) {
|
||||
qWarning() << "[WinDbg] Invalid PID in target:" << target;
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "[WinDbg] Attaching to PID" << pid << "(non-invasive)";
|
||||
hr = m_client->AttachProcess(
|
||||
0, pid,
|
||||
DEBUG_ATTACH_NONINVASIVE | DEBUG_ATTACH_NONINVASIVE_NO_SUSPEND);
|
||||
qDebug() << "[WinDbg] AttachProcess hr=" << Qt::hex << (unsigned long)hr;
|
||||
if (FAILED(hr)) {
|
||||
qWarning() << "[WinDbg] AttachProcess FAILED";
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (target.startsWith("dump:", Qt::CaseInsensitive))
|
||||
{
|
||||
QString path = target.mid(5).trimmed();
|
||||
QByteArray pathUtf8 = path.toUtf8();
|
||||
|
||||
qDebug() << "[WinDbg] Opening dump file:" << path;
|
||||
hr = m_client->OpenDumpFile(pathUtf8.constData());
|
||||
qDebug() << "[WinDbg] OpenDumpFile hr=" << Qt::hex << (unsigned long)hr;
|
||||
if (FAILED(hr)) {
|
||||
qWarning() << "[WinDbg] OpenDumpFile FAILED";
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "[WinDbg] Unknown target format:" << target;
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
initInterfaces();
|
||||
|
||||
// WaitForEvent to finalize the attach/dump load.
|
||||
// For remote connections the server session is already active — skip.
|
||||
if (m_control && !m_isRemote) {
|
||||
qDebug() << "[WinDbg] WaitForEvent...";
|
||||
hr = m_control->WaitForEvent(0, 10000);
|
||||
qDebug() << "[WinDbg] WaitForEvent hr=" << Qt::hex << (unsigned long)hr;
|
||||
}
|
||||
|
||||
querySessionInfo();
|
||||
});
|
||||
|
||||
#else
|
||||
Q_UNUSED(target);
|
||||
#endif
|
||||
}
|
||||
|
||||
void WinDbgMemoryProvider::initInterfaces()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_client) return;
|
||||
|
||||
HRESULT hr;
|
||||
hr = m_client->QueryInterface(IID_IDebugDataSpaces, (void**)&m_dataSpaces);
|
||||
qDebug() << "[WinDbg] IDebugDataSpaces hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "ptr=" << (void*)m_dataSpaces;
|
||||
|
||||
hr = m_client->QueryInterface(IID_IDebugDataSpaces2, (void**)&m_dataSpaces2);
|
||||
qDebug() << "[WinDbg] IDebugDataSpaces2 hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "ptr=" << (void*)m_dataSpaces2;
|
||||
|
||||
hr = m_client->QueryInterface(IID_IDebugControl, (void**)&m_control);
|
||||
qDebug() << "[WinDbg] IDebugControl hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "ptr=" << (void*)m_control;
|
||||
|
||||
hr = m_client->QueryInterface(IID_IDebugSymbols, (void**)&m_symbols);
|
||||
qDebug() << "[WinDbg] IDebugSymbols hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "ptr=" << (void*)m_symbols;
|
||||
|
||||
if (!m_dataSpaces) {
|
||||
qWarning() << "[WinDbg] No IDebugDataSpaces — cleaning up";
|
||||
cleanup();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void WinDbgMemoryProvider::querySessionInfo()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_client) return;
|
||||
HRESULT hr;
|
||||
|
||||
if (m_control) {
|
||||
ULONG debugClass = 0, debugQualifier = 0;
|
||||
hr = m_control->GetDebuggeeType(&debugClass, &debugQualifier);
|
||||
qDebug() << "[WinDbg] GetDebuggeeType hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "class=" << debugClass << "qualifier=" << debugQualifier;
|
||||
if (SUCCEEDED(hr)) {
|
||||
m_isLive = (debugQualifier < DEBUG_DUMP_SMALL);
|
||||
m_writable = m_isLive;
|
||||
}
|
||||
}
|
||||
|
||||
// Query effective processor type for pointer size detection
|
||||
if (m_control) {
|
||||
ULONG procType = 0;
|
||||
hr = m_control->GetEffectiveProcessorType(&procType);
|
||||
if (SUCCEEDED(hr)) {
|
||||
// IMAGE_FILE_MACHINE_I386 = 0x014C
|
||||
if (procType == 0x014C)
|
||||
m_pointerSize = 4;
|
||||
qDebug() << "[WinDbg] EffectiveProcessorType=" << Qt::hex << procType
|
||||
<< "pointerSize=" << m_pointerSize;
|
||||
}
|
||||
}
|
||||
|
||||
// WinDbg provides access to the entire virtual address space.
|
||||
// Do NOT auto-select a module as base — let the user set their
|
||||
// own base address. m_base stays 0 so the controller won't
|
||||
// override tree.baseAddress.
|
||||
m_name = m_isLive ? QStringLiteral("WinDbg (Live)")
|
||||
: QStringLiteral("WinDbg (Dump)");
|
||||
|
||||
qDebug() << "[WinDbg] Ready. name=" << m_name
|
||||
<< "isLive=" << m_isLive;
|
||||
#endif
|
||||
}
|
||||
|
||||
WinDbgMemoryProvider::~WinDbgMemoryProvider()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Dispatch COM cleanup to the DbgEng thread (thread-affine release)
|
||||
if (m_dbgThread && m_dbgThread->isRunning() && m_dispatcher) {
|
||||
dispatchToOwner([this]() {
|
||||
if (m_client) {
|
||||
if (m_isRemote)
|
||||
m_client->EndSession(DEBUG_END_DISCONNECT);
|
||||
else
|
||||
m_client->DetachProcesses();
|
||||
}
|
||||
cleanup();
|
||||
CoUninitialize();
|
||||
});
|
||||
} else {
|
||||
// Thread not running — clean up directly (best-effort)
|
||||
if (m_client) {
|
||||
if (m_isRemote)
|
||||
m_client->EndSession(DEBUG_END_DISCONNECT);
|
||||
else
|
||||
m_client->DetachProcesses();
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
#else
|
||||
cleanup();
|
||||
#endif
|
||||
|
||||
// Stop the dedicated thread
|
||||
if (m_dbgThread) {
|
||||
m_dbgThread->quit();
|
||||
m_dbgThread->wait(3000);
|
||||
delete m_dbgThread;
|
||||
m_dbgThread = nullptr;
|
||||
}
|
||||
delete m_dispatcher;
|
||||
m_dispatcher = nullptr;
|
||||
}
|
||||
|
||||
void WinDbgMemoryProvider::cleanup()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; }
|
||||
if (m_control) { m_control->Release(); m_control = nullptr; }
|
||||
if (m_dataSpaces2) { m_dataSpaces2->Release(); m_dataSpaces2 = nullptr; }
|
||||
if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; }
|
||||
if (m_client) { m_client->Release(); m_client = nullptr; }
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_dataSpaces || len <= 0) return false;
|
||||
|
||||
bool result = false;
|
||||
dispatchToOwner([&]() {
|
||||
ULONG bytesRead = 0;
|
||||
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
|
||||
if (SUCCEEDED(hr) && (int)bytesRead >= len) {
|
||||
result = true;
|
||||
return;
|
||||
}
|
||||
// Partial or failed read — zero-fill remainder and log
|
||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||
++m_readFailCount;
|
||||
if (m_readFailCount <= 5 || (m_readFailCount % 100) == 0)
|
||||
qDebug() << "[WinDbg] ReadVirtual FAILED addr=0x" << Qt::hex << addr
|
||||
<< "len=" << Qt::dec << len
|
||||
<< "hr=0x" << Qt::hex << (unsigned long)hr
|
||||
<< "got=" << Qt::dec << bytesRead;
|
||||
result = bytesRead > 0;
|
||||
});
|
||||
return result;
|
||||
#else
|
||||
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_dataSpaces || !m_writable || len <= 0) return false;
|
||||
|
||||
bool result = false;
|
||||
dispatchToOwner([&]() {
|
||||
ULONG bytesWritten = 0;
|
||||
HRESULT hr = m_dataSpaces->WriteVirtual(addr, const_cast<void*>(buf),
|
||||
(ULONG)len, &bytesWritten);
|
||||
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
|
||||
});
|
||||
return result;
|
||||
#else
|
||||
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
int WinDbgMemoryProvider::size() const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return m_dataSpaces ? 0x10000 : 0;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WinDbgMemoryProvider::isReadable(uint64_t /*addr*/, int len) const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// DbgEng's ReadVirtual can read any mapped virtual address.
|
||||
return m_dataSpaces != nullptr && len >= 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_symbols) return {};
|
||||
|
||||
QString result;
|
||||
dispatchToOwner([&]() {
|
||||
char nameBuf[512] = {};
|
||||
ULONG nameSize = 0;
|
||||
ULONG64 displacement = 0;
|
||||
HRESULT hr = m_symbols->GetNameByOffset(addr, nameBuf, sizeof(nameBuf),
|
||||
&nameSize, &displacement);
|
||||
if (SUCCEEDED(hr) && nameSize > 0) {
|
||||
result = QString::fromUtf8(nameBuf);
|
||||
if (displacement > 0)
|
||||
result += QStringLiteral("+0x%1").arg(displacement, 0, 16);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
#else
|
||||
Q_UNUSED(addr);
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> WinDbgMemoryProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
#ifdef _WIN32
|
||||
if (!m_dataSpaces) return regions;
|
||||
|
||||
// Enumerate modules — used for tagging (user-mode) or as the primary
|
||||
// source of regions (kernel-mode, where QueryVirtual is unavailable).
|
||||
struct ModInfo { uint64_t base; uint64_t size; QString name; };
|
||||
QVector<ModInfo> modules;
|
||||
|
||||
if (m_symbols) {
|
||||
dispatchToOwner([&]() {
|
||||
ULONG loaded = 0, unloaded = 0;
|
||||
if (FAILED(m_symbols->GetNumberModules(&loaded, &unloaded)))
|
||||
return;
|
||||
for (ULONG i = 0; i < loaded; i++) {
|
||||
ULONG64 modBase = 0;
|
||||
if (FAILED(m_symbols->GetModuleByIndex(i, &modBase)))
|
||||
continue;
|
||||
DEBUG_MODULE_PARAMETERS params = {};
|
||||
if (FAILED(m_symbols->GetModuleParameters(1, &modBase, 0, ¶ms)))
|
||||
continue;
|
||||
char nameBuf[256] = {};
|
||||
ULONG nameSize = 0;
|
||||
m_symbols->GetModuleNames(i, 0,
|
||||
nullptr, 0, nullptr,
|
||||
nameBuf, sizeof(nameBuf), &nameSize,
|
||||
nullptr, 0, nullptr);
|
||||
ModInfo mi;
|
||||
mi.base = modBase;
|
||||
mi.size = params.Size;
|
||||
mi.name = QString::fromUtf8(nameBuf);
|
||||
modules.append(mi);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Try QueryVirtual first (user-mode debugging / user-mode dumps).
|
||||
// MSDN: "This method is not available in kernel-mode debugging."
|
||||
if (m_dataSpaces2) {
|
||||
dispatchToOwner([&]() {
|
||||
ULONG64 addr = 0;
|
||||
int safety = 0;
|
||||
constexpr int kMaxRegions = 500000;
|
||||
|
||||
while (safety++ < kMaxRegions) {
|
||||
MEMORY_BASIC_INFORMATION64 mbi = {};
|
||||
HRESULT hr = m_dataSpaces2->QueryVirtual(addr, &mbi);
|
||||
if (FAILED(hr))
|
||||
break;
|
||||
|
||||
if (mbi.State == MEM_COMMIT &&
|
||||
!(mbi.Protect & PAGE_NOACCESS) &&
|
||||
!(mbi.Protect & PAGE_GUARD))
|
||||
{
|
||||
rcx::MemoryRegion region;
|
||||
region.base = mbi.BaseAddress;
|
||||
region.size = mbi.RegionSize;
|
||||
region.readable = true;
|
||||
region.writable = (mbi.Protect & PAGE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_WRITECOPY) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
|
||||
region.executable = (mbi.Protect & PAGE_EXECUTE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READ) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
|
||||
|
||||
for (const auto& mod : modules) {
|
||||
if (region.base >= mod.base && region.base < mod.base + mod.size) {
|
||||
region.moduleName = mod.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
regions.append(region);
|
||||
}
|
||||
|
||||
ULONG64 next = mbi.BaseAddress + mbi.RegionSize;
|
||||
if (next <= addr) break;
|
||||
addr = next;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback for kernel-mode debugging: QueryVirtual is unavailable,
|
||||
// so use loaded modules as scannable regions. Each module image
|
||||
// becomes one region — the scanner reads through module code/data.
|
||||
if (regions.isEmpty() && !modules.isEmpty()) {
|
||||
for (const auto& mod : modules) {
|
||||
if (mod.size == 0) continue;
|
||||
rcx::MemoryRegion region;
|
||||
region.base = mod.base;
|
||||
region.size = mod.size;
|
||||
region.readable = true;
|
||||
region.writable = false;
|
||||
region.executable = true;
|
||||
region.moduleName = mod.name;
|
||||
regions.append(region);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return regions;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// WinDbgMemoryPlugin implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
QIcon WinDbgMemoryPlugin::Icon() const
|
||||
{
|
||||
return qApp->style()->standardIcon(QStyle::SP_DriveNetIcon);
|
||||
}
|
||||
|
||||
bool WinDbgMemoryPlugin::canHandle(const QString& target) const
|
||||
{
|
||||
return target.startsWith("tcp:", Qt::CaseInsensitive)
|
||||
|| target.startsWith("npipe:", Qt::CaseInsensitive)
|
||||
|| target.startsWith("pid:", Qt::CaseInsensitive)
|
||||
|| target.startsWith("dump:", Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
std::unique_ptr<rcx::Provider> WinDbgMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
{
|
||||
auto provider = std::make_unique<WinDbgMemoryProvider>(target);
|
||||
if (!provider->isValid())
|
||||
{
|
||||
if (errorMsg) {
|
||||
if (target.startsWith("tcp:", Qt::CaseInsensitive)
|
||||
|| target.startsWith("npipe:", Qt::CaseInsensitive))
|
||||
*errorMsg = QString("Failed to connect to debug server.\n\n"
|
||||
"Target: %1\n\n"
|
||||
"Make sure WinDbg is running with a matching .server command\n"
|
||||
"(e.g. .server tcp:port=5056) and the port/pipe is reachable.")
|
||||
.arg(target);
|
||||
else if (target.startsWith("pid:", Qt::CaseInsensitive))
|
||||
*errorMsg = QString("Failed to attach to process.\n\n"
|
||||
"Target: %1\n\n"
|
||||
"Make sure the process is running and you have "
|
||||
"sufficient privileges (try Run as Administrator).")
|
||||
.arg(target);
|
||||
else
|
||||
*errorMsg = QString("Failed to open dump file.\n\n"
|
||||
"Target: %1\n\n"
|
||||
"Make sure the file exists and is a valid dump.")
|
||||
.arg(target);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
uint64_t WinDbgMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||
{
|
||||
Q_UNUSED(target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
QDialog dlg(parent);
|
||||
dlg.setWindowTitle("WinDbg Settings");
|
||||
dlg.resize(480, 360);
|
||||
|
||||
QPalette dlgPal = qApp->palette();
|
||||
dlg.setPalette(dlgPal);
|
||||
dlg.setAutoFillBackground(true);
|
||||
|
||||
auto* layout = new QVBoxLayout(&dlg);
|
||||
|
||||
QColor editBg = dlgPal.window().color().darker(115);
|
||||
QString editSS = QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||
" border-radius: 3px; padding: 4px 6px; }")
|
||||
.arg(editBg.name(),
|
||||
dlgPal.color(QPalette::Text).name(),
|
||||
dlgPal.color(QPalette::Mid).name());
|
||||
|
||||
layout->addWidget(new QLabel(
|
||||
"Connect to a running WinDbg debug server.\n"
|
||||
"In WinDbg, run: .server tcp:port=5056\n\n"
|
||||
"Non-invasive debug and dump files only.\n"
|
||||
"Execution control (bp, g, t, p) is not supported.\n"
|
||||
"WinDbg Classic is recommended."));
|
||||
|
||||
layout->addSpacing(8);
|
||||
layout->addWidget(new QLabel("Connection string:"));
|
||||
auto* connEdit = new QLineEdit;
|
||||
connEdit->setPlaceholderText("tcp:Port=5056,Server=127.0.0.1");
|
||||
connEdit->setText("tcp:Port=5056,Server=127.0.0.1");
|
||||
connEdit->setStyleSheet(editSS);
|
||||
layout->addWidget(connEdit);
|
||||
|
||||
layout->addSpacing(4);
|
||||
layout->addWidget(new QLabel("Run one of these in WinDbg first:"));
|
||||
|
||||
auto addExample = [&](const QString& text) {
|
||||
auto* row = new QHBoxLayout;
|
||||
auto* label = new QLabel(text);
|
||||
QPalette lp = dlgPal;
|
||||
lp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||
label->setPalette(lp);
|
||||
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
row->addWidget(label, 1);
|
||||
auto* copyBtn = new QPushButton("Copy");
|
||||
copyBtn->setFixedWidth(50);
|
||||
copyBtn->setToolTip("Copy to clipboard");
|
||||
QObject::connect(copyBtn, &QPushButton::clicked, [text]() {
|
||||
QGuiApplication::clipboard()->setText(text);
|
||||
});
|
||||
row->addWidget(copyBtn);
|
||||
layout->addLayout(row);
|
||||
};
|
||||
|
||||
addExample(".server tcp:port=5056");
|
||||
addExample(".server npipe:pipe=reclass");
|
||||
|
||||
// ── Debugger Tools status ──
|
||||
layout->addSpacing(8);
|
||||
#ifdef _WIN32
|
||||
bool found = dbgToolsFound();
|
||||
auto* toolsRow = new QHBoxLayout;
|
||||
auto* toolsLabel = new QLabel;
|
||||
if (found) {
|
||||
toolsLabel->setText(QStringLiteral("Debugging Tools: %1").arg(s_loadedDir));
|
||||
QPalette tp = dlgPal;
|
||||
tp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||
toolsLabel->setPalette(tp);
|
||||
} else {
|
||||
toolsLabel->setText("Debugging Tools: not found");
|
||||
QPalette tp = dlgPal;
|
||||
tp.setColor(QPalette::WindowText, QColor(220, 120, 80));
|
||||
toolsLabel->setPalette(tp);
|
||||
}
|
||||
toolsLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
toolsRow->addWidget(toolsLabel, 1);
|
||||
|
||||
auto* browseBtn = new QPushButton("Browse...");
|
||||
browseBtn->setFixedWidth(70);
|
||||
browseBtn->setToolTip("Locate Debugging Tools for Windows directory (contains dbgeng.dll)");
|
||||
QObject::connect(browseBtn, &QPushButton::clicked, [&dlg, toolsLabel, &dlgPal]() {
|
||||
QString dir = QFileDialog::getExistingDirectory(&dlg,
|
||||
"Locate Debugging Tools for Windows",
|
||||
"C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers");
|
||||
if (dir.isEmpty()) return;
|
||||
QString dllPath = dir + "/dbgeng.dll";
|
||||
if (!QFileInfo::exists(dllPath)) {
|
||||
QMessageBox::warning(&dlg, "Not Found",
|
||||
"dbgeng.dll was not found in that directory.\n"
|
||||
"Select the folder containing dbgeng.dll\n"
|
||||
"(e.g. Debuggers\\x64).");
|
||||
return;
|
||||
}
|
||||
QSettings settings;
|
||||
settings.setValue(kSettingsKey, dir);
|
||||
// Force reload on next use
|
||||
s_hDbgEng = nullptr;
|
||||
s_loadedDir.clear();
|
||||
if (dbgToolsFound()) {
|
||||
toolsLabel->setText(QStringLiteral("Debugging Tools: %1").arg(s_loadedDir));
|
||||
QPalette tp = dlgPal;
|
||||
tp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||
toolsLabel->setPalette(tp);
|
||||
}
|
||||
});
|
||||
toolsRow->addWidget(browseBtn);
|
||||
layout->addLayout(toolsRow);
|
||||
|
||||
if (!found) {
|
||||
auto* note = new QLabel(
|
||||
"The system dbgeng.dll does not support remote connections.\n"
|
||||
"Install Debugging Tools for Windows or use Browse to locate them.");
|
||||
QPalette np = dlgPal;
|
||||
np.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
|
||||
note->setPalette(np);
|
||||
note->setWordWrap(true);
|
||||
layout->addWidget(note);
|
||||
}
|
||||
#endif
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
auto* btnLayout = new QHBoxLayout;
|
||||
btnLayout->addStretch();
|
||||
auto* okBtn = new QPushButton("OK");
|
||||
auto* cancelBtn = new QPushButton("Cancel");
|
||||
btnLayout->addWidget(okBtn);
|
||||
btnLayout->addWidget(cancelBtn);
|
||||
layout->addLayout(btnLayout);
|
||||
|
||||
QObject::connect(okBtn, &QPushButton::clicked, &dlg, &QDialog::accept);
|
||||
QObject::connect(cancelBtn, &QPushButton::clicked, &dlg, &QDialog::reject);
|
||||
|
||||
if (dlg.exec() != QDialog::Accepted)
|
||||
return false;
|
||||
|
||||
QString conn = connEdit->text().trimmed();
|
||||
if (conn.isEmpty()) return false;
|
||||
*target = conn;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Plugin factory
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
|
||||
{
|
||||
return new WinDbgMemoryPlugin();
|
||||
}
|
||||
127
plugins/WinDbgMemory/WinDbgMemoryPlugin.h
Normal file
@@ -0,0 +1,127 @@
|
||||
#pragma once
|
||||
#include "../../src/iplugin.h"
|
||||
#include "../../src/core.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
|
||||
// Forward declarations for DbgEng COM interfaces
|
||||
struct IDebugClient;
|
||||
struct IDebugDataSpaces;
|
||||
struct IDebugDataSpaces2;
|
||||
struct IDebugControl;
|
||||
struct IDebugSymbols;
|
||||
|
||||
/**
|
||||
* WinDbg memory provider
|
||||
*
|
||||
* Uses DbgEng to read memory from:
|
||||
* - An existing WinDbg debug server via DebugConnect (tcp/npipe)
|
||||
* - A live process by PID via DebugCreate (non-invasive attach)
|
||||
* - A crash dump (.dmp) file via DebugCreate
|
||||
*
|
||||
* Target string format:
|
||||
* "tcp:Port=5055,Server=localhost" - connect to WinDbg debug server (TCP)
|
||||
* "npipe:Pipe=name,Server=localhost" - connect to WinDbg debug server (named pipe)
|
||||
* "pid:1234" - attach to process 1234
|
||||
* "dump:C:/path/to/file.dmp" - open dump file
|
||||
*
|
||||
* Threading: All DbgEng COM calls are dispatched to the thread that created
|
||||
* the connection (DebugConnect/DebugCreate). This is required because the
|
||||
* remote transport (TCP/named-pipe) binds to the creating thread. The
|
||||
* controller's background refresh threads call read() which transparently
|
||||
* marshals to the owning thread via BlockingQueuedConnection.
|
||||
*/
|
||||
|
||||
// Helper QObject that lives on the DbgEng-owning thread.
|
||||
// Used as a target for QMetaObject::invokeMethod to marshal calls.
|
||||
class DbgEngDispatcher : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using QObject::QObject;
|
||||
};
|
||||
|
||||
class WinDbgMemoryProvider : public rcx::Provider
|
||||
{
|
||||
public:
|
||||
/// Create a provider from a target string
|
||||
WinDbgMemoryProvider(const QString& target);
|
||||
~WinDbgMemoryProvider() override;
|
||||
|
||||
// Required overrides
|
||||
bool read(uint64_t addr, void* buf, int len) const override;
|
||||
int size() const override;
|
||||
|
||||
// Optional overrides
|
||||
bool isReadable(uint64_t addr, int len) const override;
|
||||
bool write(uint64_t addr, const void* buf, int len) override;
|
||||
bool isWritable() const override { return m_writable; }
|
||||
QString name() const override { return m_name; }
|
||||
QString kind() const override { return QStringLiteral("WinDbg"); }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
QVector<rcx::MemoryRegion> enumerateRegions() const override;
|
||||
|
||||
bool isLive() const override { return m_isLive; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
int pointerSize() const override { return m_pointerSize; }
|
||||
|
||||
private:
|
||||
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
|
||||
void querySessionInfo(); // determine live/dump, writable, name, base
|
||||
void cleanup();
|
||||
|
||||
// Marshal a lambda to the DbgEng-owning thread. If already on that
|
||||
// thread, calls directly. Otherwise blocks via QueuedConnection.
|
||||
template<typename Fn>
|
||||
void dispatchToOwner(Fn&& fn) const;
|
||||
|
||||
IDebugClient* m_client = nullptr;
|
||||
IDebugDataSpaces* m_dataSpaces = nullptr;
|
||||
IDebugDataSpaces2* m_dataSpaces2 = nullptr;
|
||||
IDebugControl* m_control = nullptr;
|
||||
IDebugSymbols* m_symbols = nullptr;
|
||||
|
||||
QString m_name;
|
||||
uint64_t m_base = 0;
|
||||
bool m_isLive = false;
|
||||
bool m_writable = false;
|
||||
int m_pointerSize = 8;
|
||||
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
|
||||
mutable int m_readFailCount = 0;
|
||||
|
||||
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe
|
||||
// transport is thread-affine — all calls must happen on the thread
|
||||
// that called DebugConnect. A private thread with its own event loop
|
||||
// ensures dispatchToOwner() works from any calling thread (including
|
||||
// QtConcurrent workers and the main/GUI thread) without deadlock.
|
||||
QThread* m_dbgThread = nullptr;
|
||||
DbgEngDispatcher* m_dispatcher = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin that provides WinDbgMemoryProvider
|
||||
*
|
||||
* Uses DbgEng to read memory via:
|
||||
* - Remote connection to an existing WinDbg debug server (tcp/npipe)
|
||||
* - Local non-invasive attach to a live process (pid)
|
||||
* - Local crash dump file (dump)
|
||||
*/
|
||||
class WinDbgMemoryPlugin : public IProviderPlugin
|
||||
{
|
||||
public:
|
||||
std::string Name() const override { return "WinDbg Memory"; }
|
||||
std::string Version() const override { return "2.0.0"; }
|
||||
std::string Author() const override { return "Reclass"; }
|
||||
std::string Description() const override { return "Read memory via DbgEng (live process attach or crash dump)"; }
|
||||
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
|
||||
QIcon Icon() const override;
|
||||
|
||||
bool canHandle(const QString& target) const override;
|
||||
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
|
||||
uint64_t getInitialBaseAddress(const QString& target) const override;
|
||||
bool selectTarget(QWidget* parent, QString* target) override;
|
||||
};
|
||||
|
||||
// Plugin export
|
||||
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
|
||||
BIN
screenshot.png
|
Before Width: | Height: | Size: 74 KiB |
@@ -1,4 +1,4 @@
|
||||
# PowerShell script to build ReclassX
|
||||
# PowerShell script to build Reclass
|
||||
# Automatically detects Qt installation and configures build environment
|
||||
|
||||
#Requires -Version 5.1
|
||||
@@ -283,9 +283,10 @@ function Find-MinGWDirectory {
|
||||
$toolsDir = Join-Path $qtRoot "Tools"
|
||||
|
||||
if (Test-Path $toolsDir) {
|
||||
# Prefer GCC-based MinGW (has g++.exe); exclude llvm-mingw. Prefer 64-bit, then newest.
|
||||
$mingwToolDirs = Get-ChildItem -Path $toolsDir -Directory -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Name -match 'mingw'
|
||||
}
|
||||
$_.Name -match '^mingw\d+_\d+$'
|
||||
} | Sort-Object -Property @{ Expression = { if ($_.Name -match '_64$') { 1 } else { 0 } }; Descending = $true }, Name -Descending
|
||||
|
||||
foreach ($dir in $mingwToolDirs) {
|
||||
$testBin = Join-Path $dir.FullName "bin\g++.exe"
|
||||
@@ -303,7 +304,7 @@ function Find-MinGWDirectory {
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Write-ColorOutput "`n========================================" Cyan
|
||||
Write-ColorOutput "ReclassX Build Script" Cyan
|
||||
Write-ColorOutput "Reclass Build Script" Cyan
|
||||
Write-ColorOutput "========================================`n" Cyan
|
||||
|
||||
# Get script directory and project root
|
||||
@@ -426,7 +427,7 @@ try {
|
||||
Write-ColorOutput "`nCMake configuration completed successfully.`n" Green
|
||||
|
||||
# Build
|
||||
Write-ColorOutput "Building ReclassX..." Cyan
|
||||
Write-ColorOutput "Building Reclass..." Cyan
|
||||
|
||||
$cores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors
|
||||
if (-not $cores -or $cores -lt 1) {
|
||||
@@ -445,8 +446,8 @@ try {
|
||||
# Find executable
|
||||
Write-ColorOutput "Locating executable..." Cyan
|
||||
$exePaths = @(
|
||||
(Join-Path $buildDir "ReclassX.exe"),
|
||||
(Join-Path $buildDir "$BuildType\ReclassX.exe")
|
||||
(Join-Path $buildDir "Reclass.exe"),
|
||||
(Join-Path $buildDir "$BuildType\Reclass.exe")
|
||||
)
|
||||
|
||||
$exePath = $null
|
||||
@@ -477,7 +478,7 @@ try {
|
||||
|
||||
# Count deployed files
|
||||
$deployedFiles = Get-ChildItem -Path $exeDir -Recurse -File | Where-Object {
|
||||
$_.Name -ne "ReclassX.exe" -and $_.Extension -match '\.(dll|qm)$'
|
||||
$_.Name -ne "Reclass.exe" -and $_.Extension -match '\.(dll|qm)$'
|
||||
}
|
||||
if ($deployedFiles) {
|
||||
Write-ColorOutput "Deployed $($deployedFiles.Count) Qt dependency files." Gray
|
||||
@@ -491,7 +492,7 @@ try {
|
||||
Write-ColorOutput "Application may not run without Qt DLLs in PATH" Yellow
|
||||
}
|
||||
} else {
|
||||
Write-ColorOutput "WARNING: Could not locate ReclassX.exe" Yellow
|
||||
Write-ColorOutput "WARNING: Could not locate Reclass.exe" Yellow
|
||||
}
|
||||
|
||||
} catch {
|
||||
@@ -507,5 +508,5 @@ Write-ColorOutput "========================================`n" Cyan
|
||||
|
||||
if ($exePath) {
|
||||
Write-ColorOutput "Run the application with:" White
|
||||
Write-ColorOutput " .\build\ReclassX.exe`n" Cyan
|
||||
Write-ColorOutput " .\build\Reclass.exe`n" Cyan
|
||||
}
|
||||
|
||||
168
scripts/build_macos.sh
Executable file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
print_help() {
|
||||
cat <<'EOF'
|
||||
Reclass macOS Build Script
|
||||
|
||||
Usage:
|
||||
./scripts/build_macos.sh [options]
|
||||
|
||||
Options:
|
||||
--qt-dir <path> Qt installation prefix (e.g. /opt/homebrew/opt/qt)
|
||||
--build-type <type> Release | Debug | RelWithDebInfo | MinSizeRel (default: Release)
|
||||
--build-dir <path> Build directory (default: <repo>/build)
|
||||
--generator <name> CMake generator (default: Ninja if available)
|
||||
--clean Remove build directory before configuring
|
||||
--rebuild Clean then build
|
||||
--package Run macdeployqt and create a zip
|
||||
--tests Run ctest after build
|
||||
-h, --help Show this help
|
||||
|
||||
Notes:
|
||||
- You can set QTDIR or Qt6_DIR in your environment instead of --qt-dir.
|
||||
- If Qt is installed via Homebrew, the script will try to detect it.
|
||||
EOF
|
||||
}
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
project_root="$(cd "${script_dir}/.." && pwd)"
|
||||
|
||||
qt_dir=""
|
||||
build_type="Release"
|
||||
build_dir="${project_root}/build"
|
||||
generator=""
|
||||
do_clean="false"
|
||||
do_package="false"
|
||||
do_tests="false"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--qt-dir)
|
||||
qt_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-type)
|
||||
build_type="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--build-dir)
|
||||
build_dir="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--generator)
|
||||
generator="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--clean)
|
||||
do_clean="true"
|
||||
shift
|
||||
;;
|
||||
--rebuild)
|
||||
do_clean="true"
|
||||
shift
|
||||
;;
|
||||
--package)
|
||||
do_package="true"
|
||||
shift
|
||||
;;
|
||||
--tests)
|
||||
do_tests="true"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
print_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
print_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${qt_dir}" ]]; then
|
||||
if [[ -n "${QTDIR:-}" ]]; then
|
||||
qt_dir="${QTDIR}"
|
||||
elif [[ -n "${Qt6_DIR:-}" ]]; then
|
||||
qt_dir="${Qt6_DIR}"
|
||||
elif command -v brew >/dev/null 2>&1; then
|
||||
if brew --prefix qt >/dev/null 2>&1; then
|
||||
qt_dir="$(brew --prefix qt)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v cmake >/dev/null 2>&1; then
|
||||
echo "ERROR: cmake not found. Install CMake and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${generator}" ]]; then
|
||||
if command -v ninja >/dev/null 2>&1; then
|
||||
generator="Ninja"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${do_clean}" == "true" && -d "${build_dir}" ]]; then
|
||||
echo "Cleaning build directory: ${build_dir}"
|
||||
rm -rf "${build_dir}"
|
||||
fi
|
||||
|
||||
mkdir -p "${build_dir}"
|
||||
|
||||
cmake_args=(
|
||||
-S "${project_root}"
|
||||
-B "${build_dir}"
|
||||
-DCMAKE_BUILD_TYPE="${build_type}"
|
||||
)
|
||||
|
||||
if [[ -n "${generator}" ]]; then
|
||||
cmake_args+=(-G "${generator}")
|
||||
fi
|
||||
|
||||
if [[ -n "${qt_dir}" ]]; then
|
||||
export PATH="${qt_dir}/bin:${PATH}"
|
||||
cmake_args+=(-DCMAKE_PREFIX_PATH="${qt_dir}")
|
||||
fi
|
||||
|
||||
echo "Configuring..."
|
||||
cmake "${cmake_args[@]}"
|
||||
|
||||
echo "Building..."
|
||||
cmake --build "${build_dir}" --config "${build_type}"
|
||||
|
||||
if [[ "${do_tests}" == "true" ]]; then
|
||||
echo "Running tests..."
|
||||
ctest --test-dir "${build_dir}" --output-on-failure -C "${build_type}"
|
||||
fi
|
||||
|
||||
if [[ "${do_package}" == "true" ]]; then
|
||||
app_path="${build_dir}/Reclass.app"
|
||||
if [[ ! -d "${app_path}" ]]; then
|
||||
echo "ERROR: ${app_path} not found. Build may have failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
macdeployqt_bin=""
|
||||
if [[ -n "${qt_dir}" && -x "${qt_dir}/bin/macdeployqt" ]]; then
|
||||
macdeployqt_bin="${qt_dir}/bin/macdeployqt"
|
||||
elif command -v macdeployqt >/dev/null 2>&1; then
|
||||
macdeployqt_bin="$(command -v macdeployqt)"
|
||||
fi
|
||||
|
||||
if [[ -z "${macdeployqt_bin}" ]]; then
|
||||
echo "ERROR: macdeployqt not found. Ensure Qt is installed and in PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Running macdeployqt..."
|
||||
"${macdeployqt_bin}" "${app_path}" -always-overwrite
|
||||
|
||||
arch="$(uname -m)"
|
||||
zip_name="Reclass-macos-${arch}-qt6.zip"
|
||||
echo "Creating zip: ${zip_name}"
|
||||
ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${build_dir}/${zip_name}"
|
||||
echo "Packaged: ${build_dir}/${zip_name}"
|
||||
fi
|
||||
@@ -1,4 +1,4 @@
|
||||
# PowerShell script to build QScintilla static library for ReclassX
|
||||
# PowerShell script to build QScintilla static library for Reclass
|
||||
# This script checks for Qt installation, prompts if missing, and builds QScintilla
|
||||
|
||||
#Requires -Version 5.1
|
||||
@@ -272,7 +272,7 @@ function Find-MakeCommand {
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Write-ColorOutput "`n========================================" Cyan
|
||||
Write-ColorOutput "QScintilla Build Script for ReclassX" Cyan
|
||||
Write-ColorOutput "QScintilla Build Script for Reclass" Cyan
|
||||
Write-ColorOutput "========================================`n" Cyan
|
||||
|
||||
# Get script directory and project root
|
||||
@@ -318,10 +318,10 @@ $qtRoot = Split-Path (Split-Path $selectedQtDir -Parent) -Parent
|
||||
$toolsDir = Join-Path $qtRoot "Tools"
|
||||
|
||||
if (Test-Path $toolsDir) {
|
||||
# Look for MinGW tools directory
|
||||
# Prefer GCC-based MinGW (has g++.exe); exclude llvm-mingw. Prefer 64-bit, then newest.
|
||||
$mingwToolDirs = Get-ChildItem -Path $toolsDir -Directory -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Name -match 'mingw'
|
||||
}
|
||||
$_.Name -match '^mingw\d+_\d+$'
|
||||
} | Sort-Object -Property @{ Expression = { if ($_.Name -match '_64$') { 1 } else { 0 } }; Descending = $true }, Name -Descending
|
||||
|
||||
foreach ($dir in $mingwToolDirs) {
|
||||
$testBin = Join-Path $dir.FullName "bin\g++.exe"
|
||||
@@ -423,7 +423,7 @@ try {
|
||||
Write-Host " - $($lib.Name) ($sizeMB MB)" -ForegroundColor Green
|
||||
Write-Host " Path: $($lib.Path)" -ForegroundColor Gray
|
||||
}
|
||||
Write-ColorOutput "`nYou can now build ReclassX with CMake." Green
|
||||
Write-ColorOutput "`nYou can now build Reclass with CMake." Green
|
||||
} else {
|
||||
Write-ColorOutput "`nWARNING: Build completed but no library files found." Yellow
|
||||
Write-ColorOutput "Expected files: qscintilla2_qt6.a or qscintilla2_qt6.lib" Yellow
|
||||
|
||||
540
src/addressparser.cpp
Normal file
@@ -0,0 +1,540 @@
|
||||
#include "addressparser.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Address Expression Parser ──────────────────────────────────────────
|
||||
//
|
||||
// Parses expressions like:
|
||||
// "7FF66CCE0000" → plain hex address
|
||||
// "0x100 + 0x200" → arithmetic on hex values
|
||||
// "<Program.exe> + 0xDE" → module base + offset
|
||||
// "[<Program.exe> + 0xDE] - AB" → dereference pointer, then subtract
|
||||
// "7ff6`6cce0000" → WinDbg-style backtick separator (stripped before parsing)
|
||||
// "base + e_lfanew" → C/C++ style identifier resolution
|
||||
// "0xFF & 0x0F" → bitwise AND
|
||||
// "1 << 4" → shift left
|
||||
//
|
||||
// Grammar (C operator precedence):
|
||||
//
|
||||
// bitwiseOr = bitwiseXor ('|' bitwiseXor)*
|
||||
// bitwiseXor = bitwiseAnd ('^' bitwiseAnd)*
|
||||
// bitwiseAnd = shift ('&' shift)*
|
||||
// shift = expr (('<<' | '>>') expr)*
|
||||
// expr = term (('+' | '-') term)*
|
||||
// term = unary (('*' | '/') unary)*
|
||||
// unary = '-' unary | '~' unary | atom
|
||||
// atom = '[' bitwiseOr ']' -- read pointer at address (dereference)
|
||||
// | '<' moduleName '>' -- resolve module base address
|
||||
// | '(' bitwiseOr ')' -- grouping
|
||||
// | identifier -- C/C++ name resolved via callback
|
||||
// | hexLiteral -- hex number, optional 0x prefix
|
||||
//
|
||||
// All numeric literals are hexadecimal (base 16).
|
||||
// Identifiers: [a-zA-Z_][a-zA-Z0-9_]* containing at least one non-hex char.
|
||||
// Module names with extensions (e.g. "client.dll") are scanned as one token.
|
||||
// Pure hex-digit words (e.g. "DEAD") are treated as hex literals.
|
||||
|
||||
class ExpressionParser {
|
||||
public:
|
||||
ExpressionParser(const QString& input, const AddressParserCallbacks* callbacks)
|
||||
: m_input(input), m_callbacks(callbacks) {}
|
||||
|
||||
AddressParseResult parse() {
|
||||
skipSpaces();
|
||||
if (atEnd())
|
||||
return error("empty expression");
|
||||
|
||||
uint64_t value = 0;
|
||||
if (!parseBitwiseOr(value))
|
||||
return error(m_error);
|
||||
|
||||
skipSpaces();
|
||||
if (!atEnd())
|
||||
return error(QStringLiteral("unexpected '%1'").arg(m_input[m_pos]));
|
||||
|
||||
return {true, value, {}, -1};
|
||||
}
|
||||
|
||||
private:
|
||||
const QString& m_input;
|
||||
const AddressParserCallbacks* m_callbacks;
|
||||
int m_pos = 0;
|
||||
QString m_error;
|
||||
int m_errorPos = 0;
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
bool atEnd() const { return m_pos >= m_input.size(); }
|
||||
|
||||
QChar peek() const { return atEnd() ? QChar('\0') : m_input[m_pos]; }
|
||||
|
||||
void advance() { m_pos++; }
|
||||
|
||||
void skipSpaces() {
|
||||
while (!atEnd() && m_input[m_pos].isSpace())
|
||||
m_pos++;
|
||||
}
|
||||
|
||||
AddressParseResult error(const QString& msg) const {
|
||||
return {false, 0, msg, m_errorPos};
|
||||
}
|
||||
|
||||
bool fail(const QString& msg) {
|
||||
m_error = msg;
|
||||
m_errorPos = m_pos;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool expect(QChar ch) {
|
||||
skipSpaces();
|
||||
if (peek() != ch)
|
||||
return fail(QStringLiteral("expected '%1'").arg(ch));
|
||||
advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool isHexDigit(QChar ch) {
|
||||
return (ch >= '0' && ch <= '9')
|
||||
|| (ch >= 'a' && ch <= 'f')
|
||||
|| (ch >= 'A' && ch <= 'F');
|
||||
}
|
||||
|
||||
static bool isIdentStart(QChar ch) {
|
||||
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_';
|
||||
}
|
||||
|
||||
static bool isIdentChar(QChar ch) {
|
||||
return isIdentStart(ch) || (ch >= '0' && ch <= '9');
|
||||
}
|
||||
|
||||
// ── Recursive descent parsing ──
|
||||
|
||||
// bitwiseOr = bitwiseXor ('|' bitwiseXor)*
|
||||
bool parseBitwiseOr(uint64_t& result) {
|
||||
if (!parseBitwiseXor(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
if (peek() != '|')
|
||||
break;
|
||||
advance();
|
||||
uint64_t rhs = 0;
|
||||
if (!parseBitwiseXor(rhs))
|
||||
return false;
|
||||
result |= rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// bitwiseXor = bitwiseAnd ('^' bitwiseAnd)*
|
||||
bool parseBitwiseXor(uint64_t& result) {
|
||||
if (!parseBitwiseAnd(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
if (peek() != '^')
|
||||
break;
|
||||
advance();
|
||||
uint64_t rhs = 0;
|
||||
if (!parseBitwiseAnd(rhs))
|
||||
return false;
|
||||
result ^= rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// bitwiseAnd = shift ('&' shift)*
|
||||
bool parseBitwiseAnd(uint64_t& result) {
|
||||
if (!parseShift(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
if (peek() != '&')
|
||||
break;
|
||||
advance();
|
||||
uint64_t rhs = 0;
|
||||
if (!parseShift(rhs))
|
||||
return false;
|
||||
result &= rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// shift = expr (('<<' | '>>') expr)*
|
||||
bool parseShift(uint64_t& result) {
|
||||
if (!parseExpression(result))
|
||||
return false;
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
QChar c = peek();
|
||||
if (c != '<' && c != '>')
|
||||
break;
|
||||
// Must be << or >> (not < or > alone)
|
||||
if (m_pos + 1 >= m_input.size() || m_input[m_pos + 1] != c)
|
||||
break;
|
||||
bool isLeft = (c == '<');
|
||||
advance(); advance(); // skip << or >>
|
||||
uint64_t rhs = 0;
|
||||
if (!parseExpression(rhs))
|
||||
return false;
|
||||
result = isLeft ? (result << rhs) : (result >> rhs);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// expr = term (('+' | '-') term)*
|
||||
bool parseExpression(uint64_t& result) {
|
||||
if (!parseTerm(result))
|
||||
return false;
|
||||
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
QChar op = peek();
|
||||
if (op != '+' && op != '-')
|
||||
break;
|
||||
advance();
|
||||
|
||||
uint64_t rhs = 0;
|
||||
if (!parseTerm(rhs))
|
||||
return false;
|
||||
|
||||
result = (op == '+') ? result + rhs : result - rhs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// term = unary (('*' | '/') unary)*
|
||||
bool parseTerm(uint64_t& result) {
|
||||
if (!parseUnary(result))
|
||||
return false;
|
||||
|
||||
for (;;) {
|
||||
skipSpaces();
|
||||
QChar op = peek();
|
||||
if (op != '*' && op != '/')
|
||||
break;
|
||||
advance();
|
||||
|
||||
uint64_t rhs = 0;
|
||||
if (!parseUnary(rhs))
|
||||
return false;
|
||||
|
||||
if (op == '*') {
|
||||
result *= rhs;
|
||||
} else {
|
||||
if (rhs == 0)
|
||||
return fail("division by zero");
|
||||
result /= rhs;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// unary = '-' unary | '~' unary | atom
|
||||
bool parseUnary(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (peek() == '-') {
|
||||
advance();
|
||||
uint64_t inner = 0;
|
||||
if (!parseUnary(inner))
|
||||
return false;
|
||||
result = static_cast<uint64_t>(-static_cast<int64_t>(inner));
|
||||
return true;
|
||||
}
|
||||
if (peek() == '~') {
|
||||
advance();
|
||||
uint64_t inner = 0;
|
||||
if (!parseUnary(inner))
|
||||
return false;
|
||||
result = ~inner;
|
||||
return true;
|
||||
}
|
||||
return parseAtom(result);
|
||||
}
|
||||
|
||||
// atom = '[' bitwiseOr ']' | '<' name '>' | '(' bitwiseOr ')' | identifier | hexLiteral
|
||||
bool parseAtom(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (atEnd())
|
||||
return fail("unexpected end of expression");
|
||||
|
||||
QChar ch = peek();
|
||||
|
||||
if (ch == '[') return parseDereference(result);
|
||||
if (ch == '<') return parseModuleName(result);
|
||||
if (ch == '(') return parseGrouping(result);
|
||||
|
||||
// Try identifier before hex — identifiers start with [a-zA-Z_]
|
||||
if (isIdentStart(ch))
|
||||
return parseIdentifierOrHex(result);
|
||||
|
||||
return parseHexNumber(result);
|
||||
}
|
||||
|
||||
// Identifier or hex literal disambiguation.
|
||||
// Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier.
|
||||
// Otherwise → backtrack and parse as hex number.
|
||||
// WinDbg-style "module!symbol" is scanned as a single identifier token.
|
||||
// If the identifier is followed by '(', try to parse as a built-in function call.
|
||||
bool parseIdentifierOrHex(uint64_t& result) {
|
||||
int start = m_pos;
|
||||
bool hasNonHex = false;
|
||||
|
||||
// Scan full token, including "module!symbol" as one token
|
||||
while (!atEnd() && isIdentChar(peek())) {
|
||||
if (!isHexDigit(peek()))
|
||||
hasNonHex = true;
|
||||
advance();
|
||||
}
|
||||
// Handle module.dll / module.exe / module.sys extensions
|
||||
// e.g. "client.dll + 0xFF" should parse "client.dll" as one token
|
||||
if (!atEnd() && peek() == '.' && m_pos > start) {
|
||||
int dotPos = m_pos;
|
||||
advance(); // skip '.'
|
||||
int extStart = m_pos;
|
||||
while (!atEnd() && isIdentChar(peek()))
|
||||
advance();
|
||||
if (m_pos > extStart) {
|
||||
hasNonHex = true; // '.' makes it definitively an identifier
|
||||
} else {
|
||||
m_pos = dotPos; // backtrack — '.' at end isn't an extension
|
||||
}
|
||||
}
|
||||
// If we hit '!' and the next char is an identifier start, extend the token
|
||||
// to include the second part (WinDbg module!symbol syntax)
|
||||
if (!atEnd() && peek() == '!' && m_pos > start) {
|
||||
int bangPos = m_pos;
|
||||
advance(); // skip '!'
|
||||
if (!atEnd() && isIdentStart(peek())) {
|
||||
hasNonHex = true;
|
||||
while (!atEnd() && isIdentChar(peek())) {
|
||||
advance();
|
||||
}
|
||||
} else {
|
||||
m_pos = bangPos; // backtrack — '!' at end isn't module!symbol
|
||||
}
|
||||
}
|
||||
|
||||
QString token = m_input.mid(start, m_pos - start);
|
||||
|
||||
if (!hasNonHex) {
|
||||
// Pure hex digits (e.g. "DEAD") — backtrack, parse as hex
|
||||
m_pos = start;
|
||||
return parseHexNumber(result);
|
||||
}
|
||||
|
||||
// Check for function call syntax: identifier '(' args ')'
|
||||
skipSpaces();
|
||||
if (peek() == '(')
|
||||
return parseFunctionCall(token, result);
|
||||
|
||||
// It's an identifier — resolve via callback
|
||||
if (!m_callbacks || !m_callbacks->resolveIdentifier) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
result = m_callbacks->resolveIdentifier(token, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("unknown identifier '%1'").arg(token));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Built-in function call: vtop(pid, va), cr3(pid), phys(addr)
|
||||
bool parseFunctionCall(const QString& name, uint64_t& result) {
|
||||
advance(); // skip '('
|
||||
|
||||
if (name == QStringLiteral("vtop")) {
|
||||
// vtop(pid, virtualAddress) → physical address
|
||||
uint64_t pid = 0;
|
||||
if (!parseBitwiseOr(pid)) return false;
|
||||
skipSpaces();
|
||||
if (peek() != ',')
|
||||
return fail("vtop() requires 2 arguments: vtop(pid, va)");
|
||||
advance(); // skip ','
|
||||
uint64_t va = 0;
|
||||
if (!parseBitwiseOr(va)) return false;
|
||||
if (!expect(')')) return false;
|
||||
|
||||
if (!m_callbacks || !m_callbacks->vtop) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
bool ok = false;
|
||||
result = m_callbacks->vtop((uint32_t)pid, va, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("vtop(0x%1, 0x%2) failed")
|
||||
.arg(pid, 0, 16).arg(va, 0, 16));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name == QStringLiteral("cr3")) {
|
||||
// cr3(pid) → CR3 value
|
||||
uint64_t pid = 0;
|
||||
if (!parseBitwiseOr(pid)) return false;
|
||||
if (!expect(')')) return false;
|
||||
|
||||
if (!m_callbacks || !m_callbacks->cr3) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
bool ok = false;
|
||||
result = m_callbacks->cr3((uint32_t)pid, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("cr3(%1) failed").arg(pid));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name == QStringLiteral("phys")) {
|
||||
// phys(addr) → read 8 bytes from physical address
|
||||
uint64_t addr = 0;
|
||||
if (!parseBitwiseOr(addr)) return false;
|
||||
if (!expect(')')) return false;
|
||||
|
||||
if (!m_callbacks || !m_callbacks->physRead) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
bool ok = false;
|
||||
result = m_callbacks->physRead(addr, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("phys(0x%1) failed").arg(addr, 0, 16));
|
||||
return true;
|
||||
}
|
||||
|
||||
return fail(QStringLiteral("unknown function '%1'").arg(name));
|
||||
}
|
||||
|
||||
// '[' bitwiseOr ']' — read the pointer value at the computed address
|
||||
bool parseDereference(uint64_t& result) {
|
||||
advance(); // skip '['
|
||||
|
||||
uint64_t address = 0;
|
||||
if (!parseBitwiseOr(address))
|
||||
return false;
|
||||
if (!expect(']'))
|
||||
return false;
|
||||
|
||||
// Without a callback, just return 0 (syntax-check mode)
|
||||
if (!m_callbacks || !m_callbacks->readPointer) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
result = m_callbacks->readPointer(address, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("failed to read memory at 0x%1").arg(address, 0, 16));
|
||||
return true;
|
||||
}
|
||||
|
||||
// '<' moduleName '>' — resolve a module's base address (e.g. <Program.exe>)
|
||||
bool parseModuleName(uint64_t& result) {
|
||||
advance(); // skip '<'
|
||||
|
||||
int nameStart = m_pos;
|
||||
while (!atEnd() && peek() != '>')
|
||||
advance();
|
||||
if (atEnd())
|
||||
return fail("expected '>'");
|
||||
|
||||
QString name = m_input.mid(nameStart, m_pos - nameStart).trimmed();
|
||||
advance(); // skip '>'
|
||||
|
||||
if (name.isEmpty())
|
||||
return fail("empty module name");
|
||||
|
||||
// Without a callback, just return 0 (syntax-check mode)
|
||||
if (!m_callbacks || !m_callbacks->resolveModule) {
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
result = m_callbacks->resolveModule(name, &ok);
|
||||
if (!ok)
|
||||
return fail(QStringLiteral("module '%1' not found").arg(name));
|
||||
return true;
|
||||
}
|
||||
|
||||
// '(' bitwiseOr ')' — parenthesized sub-expression for grouping
|
||||
bool parseGrouping(uint64_t& result) {
|
||||
advance(); // skip '('
|
||||
if (!parseBitwiseOr(result))
|
||||
return false;
|
||||
return expect(')');
|
||||
}
|
||||
|
||||
// Hex number with optional "0x" prefix. All literals are base-16.
|
||||
bool parseHexNumber(uint64_t& result) {
|
||||
skipSpaces();
|
||||
if (atEnd())
|
||||
return fail("unexpected end of expression");
|
||||
|
||||
int start = m_pos;
|
||||
|
||||
// Skip optional 0x/0X prefix
|
||||
if (m_pos + 1 < m_input.size()
|
||||
&& m_input[m_pos] == '0'
|
||||
&& (m_input[m_pos + 1] == 'x' || m_input[m_pos + 1] == 'X'))
|
||||
m_pos += 2;
|
||||
|
||||
// Consume hex digits
|
||||
int digitsStart = m_pos;
|
||||
while (!atEnd() && isHexDigit(peek()))
|
||||
advance();
|
||||
|
||||
if (m_pos == digitsStart) {
|
||||
m_errorPos = start;
|
||||
return fail("expected hex number");
|
||||
}
|
||||
|
||||
QString digits = m_input.mid(digitsStart, m_pos - digitsStart);
|
||||
bool ok = false;
|
||||
result = digits.toULongLong(&ok, 16);
|
||||
if (!ok) {
|
||||
m_errorPos = start;
|
||||
return fail("invalid hex number");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────
|
||||
|
||||
AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize,
|
||||
const AddressParserCallbacks* cb)
|
||||
{
|
||||
// ptrSize is used by the caller to configure the readPointer callback;
|
||||
// the parser itself doesn't need it directly.
|
||||
Q_UNUSED(ptrSize);
|
||||
|
||||
// WinDbg displays 64-bit addresses with backtick separators for readability,
|
||||
// e.g. "00007ff6`1a2b3c4d". Strip them so users can paste directly.
|
||||
// Also remove ' in case user uses it
|
||||
QString cleaned = formula;
|
||||
cleaned.remove('`');
|
||||
cleaned.remove('\'');
|
||||
|
||||
ExpressionParser parser(cleaned, cb);
|
||||
return parser.parse();
|
||||
}
|
||||
|
||||
QString AddressParser::validate(const QString& formula)
|
||||
{
|
||||
QString cleaned = formula;
|
||||
cleaned.remove('`');
|
||||
cleaned.remove('\'');
|
||||
cleaned = cleaned.trimmed();
|
||||
if (cleaned.isEmpty())
|
||||
return QStringLiteral("empty");
|
||||
|
||||
// Parse with no callbacks — modules, dereferences, identifiers succeed but return 0.
|
||||
// This checks syntax only.
|
||||
ExpressionParser parser(cleaned, nullptr);
|
||||
auto result = parser.parse();
|
||||
return result.ok ? QString() : result.error;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
33
src/addressparser.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct AddressParseResult {
|
||||
bool ok;
|
||||
uint64_t value;
|
||||
QString error;
|
||||
int errorPos;
|
||||
};
|
||||
|
||||
struct AddressParserCallbacks {
|
||||
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
|
||||
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
|
||||
std::function<uint64_t(const QString& name, bool* ok)> resolveIdentifier;
|
||||
|
||||
// Kernel paging functions (optional — only wired when kernel provider active)
|
||||
std::function<uint64_t(uint32_t pid, uint64_t va, bool* ok)> vtop;
|
||||
std::function<uint64_t(uint32_t pid, bool* ok)> cr3;
|
||||
std::function<uint64_t(uint64_t physAddr, bool* ok)> physRead;
|
||||
};
|
||||
|
||||
class AddressParser {
|
||||
public:
|
||||
static AddressParseResult evaluate(const QString& formula, int ptrSize = 8,
|
||||
const AddressParserCallbacks* cb = nullptr);
|
||||
static QString validate(const QString& formula);
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
1
src/app.rc
Normal file
@@ -0,0 +1 @@
|
||||
IDI_ICON1 ICON "icons/class.ico"
|
||||
1023
src/compose.cpp
3276
src/controller.cpp
@@ -7,13 +7,15 @@
|
||||
#include <QUndoCommand>
|
||||
#include <QTimer>
|
||||
#include <QFutureWatcher>
|
||||
#include <QPointer>
|
||||
#include <memory>
|
||||
|
||||
class QSplitter;
|
||||
|
||||
namespace rcx {
|
||||
|
||||
class RcxController;
|
||||
class TypeSelectorPopup;
|
||||
struct TypeEntry;
|
||||
enum class TypePopupMode;
|
||||
|
||||
// ── Document ──
|
||||
|
||||
@@ -38,7 +40,10 @@ public:
|
||||
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
||||
}
|
||||
|
||||
ComposeResult compose(uint64_t viewRootId = 0) const;
|
||||
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
|
||||
bool treeLines = false, bool braceWrap = false,
|
||||
bool typeHints = false,
|
||||
SymbolLookupFn symbolLookup = {}) const;
|
||||
bool save(const QString& path);
|
||||
bool load(const QString& path);
|
||||
void loadData(const QString& binaryPath);
|
||||
@@ -63,12 +68,12 @@ private:
|
||||
// ── Saved source entry ──
|
||||
|
||||
struct SavedSourceEntry {
|
||||
QString kind; // "File" or "Process"
|
||||
QString kind; // "File" or provider identifier (e.g. "processmemory")
|
||||
QString displayName; // filename or process name
|
||||
QString filePath; // for File sources
|
||||
uint32_t pid = 0; // for Process sources
|
||||
QString processName; // for Process sources
|
||||
QString providerTarget; // for plugin providers (e.g. "pid:name")
|
||||
uint64_t baseAddress = 0;
|
||||
QString baseAddressFormula;
|
||||
};
|
||||
|
||||
// ── Controller ──
|
||||
@@ -80,23 +85,36 @@ public:
|
||||
~RcxController() override;
|
||||
|
||||
RcxEditor* primaryEditor() const;
|
||||
RcxEditor* addSplitEditor(QSplitter* splitter);
|
||||
RcxEditor* addSplitEditor(QWidget* parent = nullptr);
|
||||
void removeSplitEditor(RcxEditor* editor);
|
||||
QList<RcxEditor*> editors() const { return m_editors; }
|
||||
|
||||
void convertRootKeyword(const QString& newKeyword);
|
||||
void changeNodeKind(int nodeIdx, NodeKind newKind);
|
||||
void renameNode(int nodeIdx, const QString& newName);
|
||||
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
||||
void insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name);
|
||||
void removeNode(int nodeIdx);
|
||||
void toggleCollapse(int nodeIdx);
|
||||
void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false);
|
||||
void materializeRefChildren(int nodeIdx);
|
||||
void setNodeValue(int nodeIdx, int subLine, const QString& text,
|
||||
bool isAscii = false, uint64_t resolvedAddr = 0);
|
||||
void duplicateNode(int nodeIdx);
|
||||
void convertToTypedPointer(uint64_t nodeId);
|
||||
void splitHexNode(uint64_t nodeId);
|
||||
void toggleBitfieldBit(uint64_t nodeId, int memberIdx);
|
||||
void editBitfieldValue(uint64_t nodeId, int memberIdx);
|
||||
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
|
||||
void batchRemoveNodes(const QVector<int>& nodeIndices);
|
||||
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
|
||||
void deleteRootStruct(uint64_t structId);
|
||||
void groupIntoUnion(const QSet<uint64_t>& nodeIds);
|
||||
void dissolveUnion(uint64_t unionId);
|
||||
|
||||
void applyCommand(const Command& cmd, bool isUndo);
|
||||
void refresh();
|
||||
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
|
||||
uint64_t findOrCreateStructByName(const QString& typeName);
|
||||
|
||||
// Selection
|
||||
void handleNodeClick(RcxEditor* source, int line, uint64_t nodeId,
|
||||
@@ -111,10 +129,43 @@ public:
|
||||
|
||||
RcxDocument* document() const { return m_doc; }
|
||||
void setEditorFont(const QString& fontName);
|
||||
void setRefreshInterval(int ms);
|
||||
void setCompactColumns(bool v);
|
||||
void setTreeLines(bool v);
|
||||
void setBraceWrap(bool v);
|
||||
void setTypeHints(bool v);
|
||||
bool typeHints() const { return m_typeHints; }
|
||||
void resetProvider();
|
||||
|
||||
// MCP bridge accessors
|
||||
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
||||
void attachViaPlugin(const QString& providerIdentifier, const QString& target);
|
||||
const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; }
|
||||
int activeSourceIndex() const { return m_activeSourceIdx; }
|
||||
void switchSource(int idx) { switchToSavedSource(idx); }
|
||||
void clearSources();
|
||||
void selectSource(const QString& text);
|
||||
void copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx);
|
||||
|
||||
// Value tracking toggle (per-tab, off by default)
|
||||
bool trackValues() const { return m_trackValues; }
|
||||
void setTrackValues(bool on);
|
||||
void resetChangeTracking();
|
||||
|
||||
// Cross-tab type visibility: point at the project's full document list
|
||||
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
||||
|
||||
// Test accessors
|
||||
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
||||
const ComposeResult& lastResult() const { return m_lastResult; }
|
||||
int dataExtent() const { return computeDataExtent(); }
|
||||
|
||||
signals:
|
||||
void nodeSelected(int nodeIdx);
|
||||
void selectionChanged(int count);
|
||||
void contextMenuAboutToShow(QMenu* menu, int line);
|
||||
void requestOpenProviderTab(const QString& pluginId, const QString& target,
|
||||
const QString& title);
|
||||
|
||||
private:
|
||||
RcxDocument* m_doc;
|
||||
@@ -123,29 +174,43 @@ private:
|
||||
QSet<uint64_t> m_selIds;
|
||||
int m_anchorLine = -1;
|
||||
bool m_suppressRefresh = false;
|
||||
bool m_compactColumns = false;
|
||||
bool m_treeLines = false;
|
||||
bool m_braceWrap = false;
|
||||
bool m_typeHints = false;
|
||||
uint64_t m_viewRootId = 0;
|
||||
|
||||
// ── Saved sources for quick-switch ──
|
||||
QVector<SavedSourceEntry> m_savedSources;
|
||||
int m_activeSourceIdx = -1;
|
||||
|
||||
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
|
||||
QPointer<TypeSelectorPopup> m_cachedPopup;
|
||||
int m_typePopupGen = 0; // generation counter for deferred content loading
|
||||
|
||||
// ── Auto-refresh state ──
|
||||
using PageMap = QHash<uint64_t, QByteArray>;
|
||||
QTimer* m_refreshTimer = nullptr;
|
||||
QFutureWatcher<QByteArray>* m_refreshWatcher = nullptr;
|
||||
QFutureWatcher<PageMap>* m_refreshWatcher = nullptr;
|
||||
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
||||
QByteArray m_prevSnapshot;
|
||||
PageMap m_prevPages;
|
||||
QSet<int64_t> m_changedOffsets;
|
||||
QHash<uint64_t, ValueHistory> m_valueHistory;
|
||||
bool m_trackValues = true;
|
||||
int m_valueTrackCooldown = 0; // suppress value recording for N refresh cycles after clear
|
||||
uint64_t m_refreshGen = 0;
|
||||
uint64_t m_readGen = 0;
|
||||
bool m_readInFlight = false;
|
||||
|
||||
QVector<RcxDocument*>* m_projectDocs = nullptr;
|
||||
|
||||
void connectEditor(RcxEditor* editor);
|
||||
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
|
||||
void updateCommandRow();
|
||||
void performRealignment(uint64_t structId, int targetAlign);
|
||||
void attachToProcess(uint32_t pid, const QString& processName);
|
||||
void switchToSavedSource(int idx);
|
||||
void pushSavedSourcesToEditors();
|
||||
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);
|
||||
TypeSelectorPopup* ensurePopup(RcxEditor* editor);
|
||||
|
||||
// ── Auto-refresh methods ──
|
||||
void setupAutoRefresh();
|
||||
@@ -153,6 +218,10 @@ private:
|
||||
void onReadComplete();
|
||||
int computeDataExtent() const;
|
||||
void resetSnapshot();
|
||||
void collectPointerRanges(uint64_t structId, uint64_t memBase,
|
||||
int depth, int maxDepth,
|
||||
QSet<QPair<uint64_t,uint64_t>>& visited,
|
||||
QVector<QPair<uint64_t,int>>& ranges) const;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
475
src/core.h
@@ -8,8 +8,10 @@
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "providers/provider.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
@@ -25,17 +27,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
|
||||
};
|
||||
|
||||
// ── Kind flags (replaces repeated Hex/Padding switches) ──
|
||||
} // 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<int>(key), seed); }
|
||||
#endif
|
||||
namespace rcx { // reopen
|
||||
|
||||
// ── 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
|
||||
@@ -72,13 +80,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::UTF8, "UTF8", "str", 1, 1, 1, KF_String},
|
||||
{NodeKind::UTF16, "UTF16", "wstr", 2, 1, 2, KF_String},
|
||||
{NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
|
||||
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
|
||||
};
|
||||
@@ -128,15 +137,27 @@ inline constexpr bool isHexNode(NodeKind k) {
|
||||
inline constexpr bool isVectorKind(NodeKind k) {
|
||||
return k == NodeKind::Vec2 || k == NodeKind::Vec3 || k == NodeKind::Vec4;
|
||||
}
|
||||
inline constexpr bool isMatrixKind(NodeKind k) {
|
||||
return k == NodeKind::Mat4x4;
|
||||
}
|
||||
inline constexpr bool isFuncPtr(NodeKind k) {
|
||||
return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64;
|
||||
}
|
||||
// Hex types, pointer types, function pointers, and containers are not meaningful
|
||||
// primitive-pointer targets — dereferencing them produces the same output as void*.
|
||||
inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) {
|
||||
if (isHexNode(k)) return false;
|
||||
if (k == NodeKind::Pointer32 || k == NodeKind::Pointer64) return false;
|
||||
if (isFuncPtr(k)) return false;
|
||||
if (k == NodeKind::Struct || k == NodeKind::Array) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
|
||||
inline QStringList allTypeNamesForUI(bool /*stripBrackets*/ = false) {
|
||||
QStringList out;
|
||||
out.reserve(std::size(kKindMeta));
|
||||
for (const auto& m : kKindMeta) {
|
||||
QString t = QString::fromLatin1(m.typeName);
|
||||
if (stripBrackets) t.remove(QStringLiteral("[]"));
|
||||
out << t;
|
||||
}
|
||||
for (const auto& m : kKindMeta)
|
||||
out << QString::fromLatin1(m.typeName);
|
||||
out.sort(Qt::CaseInsensitive);
|
||||
out.removeDuplicates();
|
||||
return out;
|
||||
@@ -146,7 +167,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,
|
||||
@@ -154,6 +174,15 @@ enum Marker : int {
|
||||
M_HOVER = 6,
|
||||
M_SELECTED = 7,
|
||||
M_CMD_ROW = 8,
|
||||
M_ACCENT = 9,
|
||||
};
|
||||
|
||||
// ── Bitfield member (name + bit position + width within a container) ──
|
||||
|
||||
struct BitfieldMember {
|
||||
QString name;
|
||||
uint8_t bitOffset = 0; // position from LSB within the container
|
||||
uint8_t bitWidth = 1; // number of bits (1..64)
|
||||
};
|
||||
|
||||
// ── Node ──
|
||||
@@ -166,20 +195,35 @@ struct Node {
|
||||
QString classKeyword; // "struct", "class", or "enum" (empty = "struct")
|
||||
uint64_t parentId = 0; // 0 = root (no parent)
|
||||
int offset = 0;
|
||||
bool isStatic = false; // static field — excluded from struct layout
|
||||
QString offsetExpr; // C/C++ expression → absolute address (static fields only)
|
||||
bool isRelative = false; // Pointer: target = base + value (RVA) instead of absolute
|
||||
int arrayLen = 1; // Array: element count
|
||||
int strLen = 64;
|
||||
bool collapsed = false;
|
||||
bool collapsed = true;
|
||||
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
|
||||
NodeKind elementKind = NodeKind::UInt8; // Array: element type
|
||||
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
|
||||
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
|
||||
int viewIndex = 0; // Array: current view offset (transient)
|
||||
QVector<QPair<QString, int64_t>> enumMembers; // Enum: name→value pairs
|
||||
QVector<BitfieldMember> bitfieldMembers; // Bitfield: per-bit member definitions
|
||||
|
||||
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
|
||||
int byteSize() const {
|
||||
switch (kind) {
|
||||
case NodeKind::UTF8: return strLen;
|
||||
case NodeKind::UTF16: return strLen * 2;
|
||||
case NodeKind::Padding: return qMax(1, arrayLen);
|
||||
case NodeKind::Array: return arrayLen * sizeForKind(elementKind);
|
||||
case NodeKind::UTF16: return qMin(strLen, INT_MAX / 2) * 2;
|
||||
case NodeKind::Array: {
|
||||
int elemSz = sizeForKind(elementKind);
|
||||
if (elemSz <= 0) return 0;
|
||||
return qMin(arrayLen, INT_MAX / elemSz) * elemSz;
|
||||
}
|
||||
case NodeKind::Struct:
|
||||
if (classKeyword == QStringLiteral("bitfield")) {
|
||||
int sz = sizeForKind(elementKind);
|
||||
return sz > 0 ? sz : 4;
|
||||
}
|
||||
return 0;
|
||||
default: return sizeForKind(kind);
|
||||
}
|
||||
}
|
||||
@@ -195,11 +239,40 @@ struct Node {
|
||||
o["classKeyword"] = classKeyword;
|
||||
o["parentId"] = QString::number(parentId);
|
||||
o["offset"] = offset;
|
||||
if (isStatic)
|
||||
o["isStatic"] = true;
|
||||
if (!offsetExpr.isEmpty())
|
||||
o["offsetExpr"] = offsetExpr;
|
||||
if (isRelative)
|
||||
o["isRelative"] = true;
|
||||
o["arrayLen"] = arrayLen;
|
||||
o["strLen"] = strLen;
|
||||
o["collapsed"] = collapsed;
|
||||
o["refId"] = QString::number(refId);
|
||||
o["elementKind"] = kindToString(elementKind);
|
||||
if (ptrDepth > 0)
|
||||
o["ptrDepth"] = ptrDepth;
|
||||
if (!enumMembers.isEmpty()) {
|
||||
QJsonArray arr;
|
||||
for (const auto& m : enumMembers) {
|
||||
QJsonObject em;
|
||||
em["name"] = m.first;
|
||||
em["value"] = QString::number(m.second);
|
||||
arr.append(em);
|
||||
}
|
||||
o["enumMembers"] = arr;
|
||||
}
|
||||
if (!bitfieldMembers.isEmpty()) {
|
||||
QJsonArray arr;
|
||||
for (const auto& m : bitfieldMembers) {
|
||||
QJsonObject bm;
|
||||
bm["name"] = m.name;
|
||||
bm["bitOffset"] = m.bitOffset;
|
||||
bm["bitWidth"] = m.bitWidth;
|
||||
arr.append(bm);
|
||||
}
|
||||
o["bitfieldMembers"] = arr;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
static Node fromJson(const QJsonObject& o) {
|
||||
@@ -211,11 +284,34 @@ 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.collapsed = o["collapsed"].toBool(false);
|
||||
n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false));
|
||||
n.offsetExpr = o["offsetExpr"].toString();
|
||||
n.isRelative = o["isRelative"].toBool(false);
|
||||
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
|
||||
n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
|
||||
n.collapsed = true; // Always load collapsed; user expands as needed
|
||||
n.refId = o["refId"].toString("0").toULongLong();
|
||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
|
||||
if (o.contains("enumMembers")) {
|
||||
QJsonArray arr = o["enumMembers"].toArray();
|
||||
for (const auto& v : arr) {
|
||||
QJsonObject em = v.toObject();
|
||||
n.enumMembers.emplaceBack(em["name"].toString(),
|
||||
em["value"].toString("0").toLongLong());
|
||||
}
|
||||
}
|
||||
if (o.contains("bitfieldMembers")) {
|
||||
QJsonArray arr = o["bitfieldMembers"].toArray();
|
||||
for (const auto& v : arr) {
|
||||
QJsonObject bm = v.toObject();
|
||||
BitfieldMember m;
|
||||
m.name = bm["name"].toString();
|
||||
m.bitOffset = (uint8_t)qBound(0, bm["bitOffset"].toInt(0), 255);
|
||||
m.bitWidth = (uint8_t)qBound(1, bm["bitWidth"].toInt(1), 64);
|
||||
n.bitfieldMembers.append(m);
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -224,11 +320,12 @@ struct Node {
|
||||
return classKeyword.isEmpty() ? QStringLiteral("struct") : classKeyword;
|
||||
}
|
||||
|
||||
// Helper: is this a string-like array (char[] or wchar_t[])?
|
||||
bool isStringArray() const {
|
||||
return kind == NodeKind::Array &&
|
||||
(elementKind == NodeKind::UInt8 || elementKind == NodeKind::UInt16);
|
||||
}
|
||||
// NOTE: isStringArray() was checking UInt8/UInt16 instead of UTF8/UTF16.
|
||||
// Currently unused — commented out until a caller needs it.
|
||||
// bool isStringArray() const {
|
||||
// return kind == NodeKind::Array &&
|
||||
// (elementKind == NodeKind::UTF8 || elementKind == NodeKind::UTF16);
|
||||
// }
|
||||
};
|
||||
|
||||
// ── NodeTree ──
|
||||
@@ -236,8 +333,11 @@ struct Node {
|
||||
struct NodeTree {
|
||||
QVector<Node> nodes;
|
||||
uint64_t baseAddress = 0x00400000;
|
||||
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
|
||||
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
|
||||
uint64_t m_nextId = 1;
|
||||
mutable QHash<uint64_t, int> m_idCache;
|
||||
mutable QHash<uint64_t, QVector<int>> m_childCache;
|
||||
|
||||
int addNode(const Node& n) {
|
||||
Node copy = n;
|
||||
@@ -247,13 +347,15 @@ struct NodeTree {
|
||||
nodes.append(copy);
|
||||
if (!m_idCache.isEmpty())
|
||||
m_idCache[copy.id] = idx;
|
||||
if (!m_childCache.isEmpty())
|
||||
m_childCache[copy.parentId].append(idx);
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Reserve a unique ID atomically (for use before pushing undo commands)
|
||||
uint64_t reserveId() { return m_nextId++; }
|
||||
|
||||
void invalidateIdCache() const { m_idCache.clear(); }
|
||||
void invalidateIdCache() const { m_idCache.clear(); m_childCache.clear(); }
|
||||
|
||||
int indexOfId(uint64_t id) const {
|
||||
if (m_idCache.isEmpty() && !nodes.isEmpty()) {
|
||||
@@ -264,11 +366,11 @@ struct NodeTree {
|
||||
}
|
||||
|
||||
QVector<int> childrenOf(uint64_t parentId) const {
|
||||
QVector<int> result;
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
if (nodes[i].parentId == parentId) result.append(i);
|
||||
if (m_childCache.isEmpty() && !nodes.isEmpty()) {
|
||||
for (int i = 0; i < nodes.size(); i++)
|
||||
m_childCache[nodes[i].parentId].append(i);
|
||||
}
|
||||
return result;
|
||||
return m_childCache.value(parentId);
|
||||
}
|
||||
|
||||
// Collect node + all descendants (iterative, cycle-safe)
|
||||
@@ -349,18 +451,20 @@ struct NodeTree {
|
||||
QVector<int> kids = childMap ? childMap->value(structId) : childrenOf(structId);
|
||||
for (int ci : kids) {
|
||||
const Node& c = nodes[ci];
|
||||
if (c.isStatic) continue; // static fields don't affect struct size
|
||||
int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array)
|
||||
? structSpan(c.id, childMap, visited) : c.byteSize();
|
||||
int end = c.offset + sz;
|
||||
if (end > maxEnd) maxEnd = end;
|
||||
int64_t end = (int64_t)c.offset + sz;
|
||||
if (end > maxEnd) maxEnd = (int)qMin(end, (int64_t)INT_MAX);
|
||||
}
|
||||
|
||||
// Embedded struct reference: no own children but refId points to a struct definition
|
||||
if (kids.isEmpty() && node.kind == NodeKind::Struct && node.refId != 0)
|
||||
maxEnd = qMax(maxEnd, structSpan(node.refId, childMap, visited));
|
||||
|
||||
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;
|
||||
@@ -368,6 +472,10 @@ struct NodeTree {
|
||||
QJsonObject toJson() const {
|
||||
QJsonObject o;
|
||||
o["baseAddress"] = QString::number(baseAddress, 16);
|
||||
if (!baseAddressFormula.isEmpty())
|
||||
o["baseAddressFormula"] = baseAddressFormula;
|
||||
if (pointerSize != 8)
|
||||
o["pointerSize"] = pointerSize;
|
||||
o["nextId"] = QString::number(m_nextId);
|
||||
QJsonArray arr;
|
||||
for (const auto& n : nodes) arr.append(n.toJson());
|
||||
@@ -378,8 +486,11 @@ struct NodeTree {
|
||||
static NodeTree fromJson(const QJsonObject& o) {
|
||||
NodeTree t;
|
||||
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
|
||||
t.baseAddressFormula = o["baseAddressFormula"].toString();
|
||||
t.pointerSize = o["pointerSize"].toInt(8);
|
||||
t.m_nextId = o["nextId"].toString("1").toULongLong();
|
||||
QJsonArray arr = o["nodes"].toArray();
|
||||
t.nodes.reserve(arr.size());
|
||||
for (const auto& v : arr) {
|
||||
Node n = Node::fromJson(v.toObject());
|
||||
t.nodes.append(n);
|
||||
@@ -390,6 +501,66 @@ struct NodeTree {
|
||||
|
||||
};
|
||||
|
||||
// ── Value History (ring buffer for heatmap) ──
|
||||
|
||||
struct ValueHistory {
|
||||
static constexpr int kCapacity = 10;
|
||||
std::array<QString, kCapacity> values;
|
||||
std::array<qint64, kCapacity> timestamps{}; // msec since epoch
|
||||
int count = 0; // total unique values recorded
|
||||
int head = 0; // next write position in ring
|
||||
|
||||
void record(const QString& v) {
|
||||
if (count > 0) {
|
||||
int last = (head + kCapacity - 1) % kCapacity;
|
||||
if (values[last] == v) return; // no change
|
||||
}
|
||||
values[head] = v;
|
||||
timestamps[head] = QDateTime::currentMSecsSinceEpoch();
|
||||
head = (head + 1) % kCapacity;
|
||||
if (count < INT_MAX) count++;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
count = 0;
|
||||
head = 0;
|
||||
}
|
||||
|
||||
int uniqueCount() const { return qMin(count, kCapacity); }
|
||||
|
||||
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
|
||||
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]);
|
||||
}
|
||||
|
||||
// Iterate with timestamps from newest to oldest
|
||||
template<typename Fn>
|
||||
void forEachWithTime(Fn&& fn) const {
|
||||
int n = uniqueCount();
|
||||
for (int i = 0; i < n; i++) {
|
||||
int idx = (head + kCapacity - 1 - i) % kCapacity;
|
||||
fn(values[idx], timestamps[idx]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── LineMeta ──
|
||||
|
||||
enum class LineKind : uint8_t {
|
||||
@@ -402,6 +573,30 @@ static constexpr uint64_t kCommandRowId = UINT64_MAX;
|
||||
static constexpr int kCommandRowLine = 0;
|
||||
static constexpr int kFirstDataLine = 1;
|
||||
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
|
||||
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
|
||||
static constexpr uint64_t kArrayElemShift = 42; // bits 42-61 hold element index
|
||||
static constexpr uint64_t kArrayElemMask = 0x3FFFFC0000000000ULL; // 20 bits → max 1048575 elements
|
||||
|
||||
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 42)
|
||||
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
|
||||
Q_ASSERT(elemIdx >= 0);
|
||||
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0xFFFFF) << kArrayElemShift);
|
||||
}
|
||||
inline int arrayElemIdxFromSelId(uint64_t selId) {
|
||||
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
|
||||
}
|
||||
|
||||
// Member selection encoding (enum/bitfield members) — mirrors array element pattern
|
||||
static constexpr uint64_t kMemberBit = 0x2000000000000000ULL;
|
||||
static constexpr uint64_t kMemberSubShift = 42;
|
||||
static constexpr uint64_t kMemberSubMask = 0x3FFFFC0000000000ULL;
|
||||
|
||||
inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) {
|
||||
return nodeId | kMemberBit | ((uint64_t)(subLine & 0xFFFFF) << kMemberSubShift);
|
||||
}
|
||||
inline int memberSubFromSelId(uint64_t selId) {
|
||||
return (int)((selId & kMemberSubMask) >> kMemberSubShift);
|
||||
}
|
||||
|
||||
struct LineMeta {
|
||||
int nodeIdx = -1;
|
||||
@@ -421,13 +616,22 @@ struct LineMeta {
|
||||
int arrayCount = 0; // Array: total element count
|
||||
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
|
||||
QString offsetText;
|
||||
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
|
||||
uint64_t ptrBase = 0; // Pointer expansion base (non-zero = use for RVA)
|
||||
uint32_t markerMask = 0;
|
||||
bool dataChanged = false; // true if any byte in this node changed since last refresh
|
||||
int heatLevel = 0; // 0=static, 1=cold, 2=warm, 3=hot (from ValueHistory)
|
||||
QVector<int> changedByteIndices; // Hex preview: which byte indices (0-based) changed on this line
|
||||
int lineByteCount = 0; // Hex preview: actual data byte count on this line
|
||||
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
||||
int effectiveNameW = 22; // Per-line name column width used for rendering
|
||||
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
|
||||
bool isArrayElement = false; // true for synthesized primitive array element lines
|
||||
bool isMemberLine = false; // true for enum member / bitfield member lines
|
||||
bool isStaticLine = false; // true for static field node lines
|
||||
QString typeHint; // Type inference hint text (e.g. "Float×2") — only set for hex nodes when hints enabled
|
||||
int typeHintStart = -1; // Character offset where hint text starts in line text (-1 = none)
|
||||
QVector<NodeKind> typeHintKinds; // Suggested kinds from inference (empty = no hint)
|
||||
};
|
||||
|
||||
inline bool isSyntheticLine(const LineMeta& lm) {
|
||||
@@ -439,6 +643,9 @@ inline bool isSyntheticLine(const LineMeta& lm) {
|
||||
struct LayoutInfo {
|
||||
int typeW = 14; // Effective type column width (default = kColType)
|
||||
int nameW = 22; // Effective name column width (default = kColName)
|
||||
int offsetHexDigits = 8; // Hex digits for offset margin (4/8/12/16)
|
||||
uint64_t baseAddress = 0; // Base address for relative offset computation
|
||||
bool treeLines = false; // Whether tree line connectors are embedded in the text
|
||||
};
|
||||
|
||||
// ── ComposeResult ──
|
||||
@@ -460,7 +667,7 @@ namespace cmd {
|
||||
struct Insert { Node node; QVector<OffsetAdj> offAdjs; };
|
||||
struct Remove { uint64_t nodeId; QVector<Node> subtree;
|
||||
QVector<OffsetAdj> offAdjs; };
|
||||
struct ChangeBase { uint64_t oldBase, newBase; };
|
||||
struct ChangeBase { uint64_t oldBase, newBase; QString oldFormula, newFormula; };
|
||||
struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; };
|
||||
struct ChangeArrayMeta { uint64_t nodeId;
|
||||
NodeKind oldElementKind, newElementKind;
|
||||
@@ -470,13 +677,19 @@ namespace cmd {
|
||||
struct ChangeStructTypeName { uint64_t nodeId; QString oldName, newName; };
|
||||
struct ChangeClassKeyword { uint64_t nodeId; QString oldKeyword, newKeyword; };
|
||||
struct ChangeOffset { uint64_t nodeId; int oldOffset, newOffset; };
|
||||
struct ChangeEnumMembers { uint64_t nodeId;
|
||||
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
|
||||
struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; };
|
||||
struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; };
|
||||
struct ToggleRelative { uint64_t nodeId; bool oldVal, newVal; };
|
||||
}
|
||||
|
||||
using Command = std::variant<
|
||||
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
|
||||
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
|
||||
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
|
||||
cmd::ChangeClassKeyword, cmd::ChangeOffset
|
||||
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers,
|
||||
cmd::ChangeOffsetExpr, cmd::ToggleStatic, cmd::ToggleRelative
|
||||
>;
|
||||
|
||||
// ── Column spans (for inline editing) ──
|
||||
@@ -489,36 +702,37 @@ struct ColumnSpan {
|
||||
|
||||
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount,
|
||||
ArrayElementType, ArrayElementCount, PointerTarget,
|
||||
RootClassType, RootClassName };
|
||||
RootClassType, RootClassName, TypeSelector, StaticExpr };
|
||||
|
||||
// Column layout constants (shared with format.cpp span computation)
|
||||
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
|
||||
inline constexpr int kColType = 14; // Max type column width (fits "uint64_t[999]")
|
||||
inline constexpr int kColName = 22;
|
||||
inline constexpr int kColValue = 32;
|
||||
inline constexpr int kColValue = 96;
|
||||
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
||||
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
|
||||
inline constexpr int kSepWidth = 1;
|
||||
inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t")
|
||||
inline constexpr int kMinTypeW = 7; // Minimum type column width (fits "uint8_t")
|
||||
inline constexpr int kMaxTypeW = 128; // Maximum type column width
|
||||
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
|
||||
inline constexpr int kMaxNameW = 128; // Maximum name column width
|
||||
inline constexpr int kCompactTypeW = 20; // Type column cap for compact column mode
|
||||
|
||||
inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) {
|
||||
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
|
||||
if (lm.lineKind != LineKind::Field || lm.isContinuation || lm.isMemberLine) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
return {ind, ind + typeW, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int nameW = kColName) {
|
||||
if (lm.isContinuation || lm.lineKind != LineKind::Field) return {};
|
||||
if (lm.isContinuation || lm.lineKind != LineKind::Field || lm.isMemberLine) return {};
|
||||
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int start = ind + typeW + kSepWidth;
|
||||
|
||||
// Hex/Padding: ASCII preview takes the name column position (8 chars)
|
||||
// Hex: ASCII preview occupies the name column (padded to nameW)
|
||||
if (isHexPreview(lm.nodeKind))
|
||||
return {start, start + 8, true};
|
||||
return {start, start + nameW, true};
|
||||
|
||||
return {start, start + nameW, true};
|
||||
}
|
||||
@@ -526,44 +740,77 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
|
||||
inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) {
|
||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer ||
|
||||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||
if (lm.isMemberLine) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
|
||||
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;
|
||||
|
||||
if (lm.isContinuation) {
|
||||
int prefixW = isHexPad
|
||||
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (typeW + nameW + 2 * kSepWidth);
|
||||
int start = ind + prefixW;
|
||||
return {start, start + valWidth, true};
|
||||
}
|
||||
if (lm.lineKind != LineKind::Field) return {};
|
||||
|
||||
int start = isHexPad
|
||||
? (ind + typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (ind + typeW + kSepWidth + nameW + kSepWidth);
|
||||
int start = ind + prefixW;
|
||||
return {start, start + valWidth, true};
|
||||
}
|
||||
|
||||
// Member line spans (enum "name = value", bitfield "name : N = value")
|
||||
inline ColumnSpan memberNameSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||
if (!lm.isMemberLine) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int eq = lineText.indexOf(QLatin1String(" = "), ind);
|
||||
if (eq < 0) return {};
|
||||
int nameEnd = eq;
|
||||
while (nameEnd > ind && lineText[nameEnd - 1] == ' ') nameEnd--;
|
||||
return {ind, nameEnd, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan memberValueSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||
if (!lm.isMemberLine) return {};
|
||||
int eq = lineText.indexOf(QLatin1String(" = "));
|
||||
if (eq < 0) return {};
|
||||
int valStart = eq + 3;
|
||||
int valEnd = lineText.size();
|
||||
while (valEnd > valStart && lineText[valEnd - 1] == ' ') valEnd--;
|
||||
return {valStart, valEnd, true};
|
||||
}
|
||||
|
||||
// Static field expression span: locates text between "return " and "→" / "(error)" / end
|
||||
inline ColumnSpan staticExprSpanFor(const LineMeta& /*lm*/, const QString& lineText) {
|
||||
int ret = lineText.indexOf(QLatin1String("return "));
|
||||
if (ret < 0) return {};
|
||||
int exprStart = ret + 7;
|
||||
// End: before arrow, before "(error)", or line end
|
||||
int exprEnd = lineText.size();
|
||||
int arrow = lineText.indexOf(QChar(0x2192), exprStart);
|
||||
if (arrow > exprStart) exprEnd = arrow;
|
||||
int err = lineText.indexOf(QLatin1String("(error)"), exprStart);
|
||||
if (err > exprStart && err < exprEnd) exprEnd = err;
|
||||
// Also stop at " }" for collapsed format
|
||||
int brace = lineText.indexOf(QLatin1String(" }"), exprStart);
|
||||
if (brace > exprStart && brace < exprEnd) exprEnd = brace;
|
||||
while (exprEnd > exprStart && lineText[exprEnd - 1] == ' ') exprEnd--;
|
||||
return {exprStart, exprEnd, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName) {
|
||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
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;
|
||||
if (lm.isContinuation) {
|
||||
int prefixW = isHexPad
|
||||
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (typeW + nameW + 2 * kSepWidth);
|
||||
start = ind + prefixW + valWidth;
|
||||
} else {
|
||||
start = isHexPad
|
||||
? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth)
|
||||
: (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth);
|
||||
start = ind + prefixW + valWidth;
|
||||
}
|
||||
return {start, lineLength, start < lineLength};
|
||||
}
|
||||
@@ -572,50 +819,56 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
||||
// Line format: "source▾ · 0x140000000"
|
||||
|
||||
inline ColumnSpan commandRowSrcSpan(const QString& lineText) {
|
||||
int idx = lineText.indexOf(QStringLiteral(" \u00B7"));
|
||||
if (idx < 0) return {};
|
||||
// Source label ends at the ▾ dropdown arrow
|
||||
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||
if (arrow < 0) return {};
|
||||
int start = 0;
|
||||
while (start < idx && !lineText[start].isLetterOrNumber()
|
||||
while (start < arrow && !lineText[start].isLetterOrNumber()
|
||||
&& lineText[start] != '<' && lineText[start] != '\'') start++;
|
||||
if (start >= idx) return {};
|
||||
// Exclude trailing ▾ from the editable span
|
||||
int end = idx;
|
||||
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
|
||||
int tag = lineText.indexOf(QStringLiteral(" \u00B7"));
|
||||
if (tag < 0) return {};
|
||||
int start = tag + 3; // after " · "
|
||||
int end = start;
|
||||
while (end < lineText.size() && !lineText[end].isSpace()) end++;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
if (start >= arrow) return {};
|
||||
return {start, arrow, true};
|
||||
}
|
||||
|
||||
// ── CommandRow root-class spans ──
|
||||
// Combined CommandRow format ends with: " struct▾ ClassName {"
|
||||
// Combined CommandRow format ends with: " struct ClassName {"
|
||||
|
||||
inline int commandRowRootStart(const QString& lineText) {
|
||||
int best = -1;
|
||||
int i;
|
||||
i = lineText.lastIndexOf(QStringLiteral("struct\u25BE"));
|
||||
// Match "struct " / "class " / "enum " as whole words before the class name
|
||||
i = lineText.lastIndexOf(QStringLiteral("struct "));
|
||||
if (i > best) best = i;
|
||||
i = lineText.lastIndexOf(QStringLiteral("class\u25BE"));
|
||||
i = lineText.lastIndexOf(QStringLiteral("class "));
|
||||
if (i > best) best = i;
|
||||
i = lineText.lastIndexOf(QStringLiteral("enum\u25BE"));
|
||||
i = lineText.lastIndexOf(QStringLiteral("enum "));
|
||||
if (i > best) best = i;
|
||||
return best;
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
|
||||
// Address starts at "0x" after the source dropdown arrow
|
||||
int arrow = lineText.indexOf(QChar(0x25BE));
|
||||
if (arrow < 0) return {};
|
||||
int start = lineText.indexOf(QStringLiteral("0x"), arrow);
|
||||
if (start < 0) {
|
||||
// Formula mode: address is between arrow and root keyword
|
||||
start = arrow + 1;
|
||||
while (start < lineText.size() && lineText[start].isSpace()) start++;
|
||||
}
|
||||
// End at root keyword (struct/class/enum) or end of line
|
||||
int rootStart = commandRowRootStart(lineText);
|
||||
int end = (rootStart > start) ? rootStart : lineText.size();
|
||||
// Trim trailing whitespace
|
||||
while (end > start && lineText[end - 1].isSpace()) end--;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
}
|
||||
|
||||
inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) {
|
||||
int start = commandRowRootStart(lineText);
|
||||
if (start < 0) return {};
|
||||
int end = start;
|
||||
while (end < lineText.size() && lineText[end] != QChar(' ')
|
||||
&& lineText[end] != QChar(0x25BE)) end++;
|
||||
while (end < lineText.size() && lineText[end] != QChar(' ')) end++;
|
||||
if (end <= start) return {};
|
||||
return {start, end, true};
|
||||
}
|
||||
@@ -635,6 +888,16 @@ inline ColumnSpan commandRowRootNameSpan(const QString& lineText) {
|
||||
return {nameStart, nameEnd, true};
|
||||
}
|
||||
|
||||
// ── CommandRow type-selector chevron span ──
|
||||
// Detects "[▸]" at the start of the command row text
|
||||
|
||||
inline ColumnSpan commandRowChevronSpan(const QString& lineText) {
|
||||
if (lineText.size() < 3) return {};
|
||||
if (lineText[0] == '[' && lineText[1] == QChar(0x25B8) && lineText[2] == ']')
|
||||
return {0, qMin(4, (int)lineText.size()), true}; // include trailing space for easier clicking
|
||||
return {};
|
||||
}
|
||||
|
||||
// ── Array element type/count spans (within type column of array headers) ──
|
||||
// Line format: " int32_t[10] name {"
|
||||
// arrayElemTypeSpan covers "int32_t", arrayElemCountSpan covers "10"
|
||||
@@ -657,6 +920,16 @@ inline ColumnSpan arrayElemCountSpanFor(const LineMeta& lm, const QString& lineT
|
||||
return {openBracket + 1, closeBracket, true};
|
||||
}
|
||||
|
||||
// Click-area version: includes brackets [N] for hit testing
|
||||
inline ColumnSpan arrayElemCountClickSpanFor(const LineMeta& lm, const QString& lineText) {
|
||||
if (lm.lineKind != LineKind::Header || !lm.isArrayHeader) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int openBracket = lineText.indexOf('[', ind);
|
||||
int closeBracket = lineText.indexOf(']', openBracket);
|
||||
if (openBracket < 0 || closeBracket < 0 || closeBracket <= openBracket + 1) return {};
|
||||
return {openBracket, closeBracket + 1, true};
|
||||
}
|
||||
|
||||
// ── Pointer kind/target spans (within type column of pointer fields) ──
|
||||
// Line format: " void* name -> 0x..."
|
||||
// pointerTargetSpan covers the target name before '*'
|
||||
@@ -739,17 +1012,18 @@ namespace fmt {
|
||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
uint64_t addr, int depth, int subLine = 0,
|
||||
const QString& comment = {}, int colType = kColType, int colName = kColName,
|
||||
const QString& typeOverride = {});
|
||||
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation);
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName);
|
||||
const QString& typeOverride = {}, bool compact = false);
|
||||
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits = 8);
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName, bool compact = false);
|
||||
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
||||
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName);
|
||||
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {}, bool compact = false);
|
||||
QString structTypeName(const Node& node); // Full type string for struct headers
|
||||
QString arrayTypeName(NodeKind elemKind, int count);
|
||||
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName = {});
|
||||
QString pointerTypeName(NodeKind kind, const QString& targetName);
|
||||
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
||||
const Provider& prov, uint64_t addr,
|
||||
const QString& ptrTypeName, int colType = kColType, int colName = kColName);
|
||||
const QString& ptrTypeName, int colType = kColType, int colName = kColName,
|
||||
bool compact = false);
|
||||
QString validateBaseAddress(const QString& text);
|
||||
QString indent(int depth);
|
||||
QString readValue(const Node& node, const Provider& prov,
|
||||
@@ -759,10 +1033,23 @@ namespace fmt {
|
||||
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
|
||||
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
|
||||
QString validateValue(NodeKind kind, const QString& text);
|
||||
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW);
|
||||
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
|
||||
uint64_t value, int depth, int nameW);
|
||||
uint64_t extractBits(const Provider& prov, uint64_t addr,
|
||||
NodeKind containerKind,
|
||||
uint8_t bitOffset, uint8_t bitWidth);
|
||||
} // namespace fmt
|
||||
|
||||
// ── Compose function forward declaration ──
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0);
|
||||
// Optional callback: given an absolute address, return a symbol name (e.g. "nt!PsActiveProcessHead")
|
||||
// or empty string if no symbol matches. Used for PDB symbol annotations on rows.
|
||||
using SymbolLookupFn = std::function<QString(uint64_t addr)>;
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
||||
bool compactColumns = false, bool treeLines = false,
|
||||
bool braceWrap = false, bool typeHints = false,
|
||||
SymbolLookupFn symbolLookup = {});
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
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
@@ -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
|
||||
36
src/dock_tab_buttons.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QToolButton>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
|
||||
// Dock tab button widget (close button)
|
||||
// Placed on the right side of each dock tab via QTabBar::setTabButton.
|
||||
class DockTabButtons : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
QToolButton* closeBtn;
|
||||
|
||||
explicit DockTabButtons(QWidget* parent = nullptr) : QWidget(parent) {
|
||||
auto* hl = new QHBoxLayout(this);
|
||||
hl->setContentsMargins(0, 0, 0, 0);
|
||||
hl->setSpacing(0);
|
||||
|
||||
closeBtn = new QToolButton(this);
|
||||
closeBtn->setAutoRaise(true);
|
||||
closeBtn->setCursor(Qt::PointingHandCursor);
|
||||
closeBtn->setFixedSize(16, 16);
|
||||
closeBtn->setToolTip("Close tab");
|
||||
closeBtn->setIcon(QIcon(":/vsicons/close.svg"));
|
||||
closeBtn->setIconSize(QSize(12, 12));
|
||||
hl->addWidget(closeBtn);
|
||||
}
|
||||
|
||||
void applyTheme(const QColor& hover) {
|
||||
QString style = QStringLiteral(
|
||||
"QToolButton { border: none; padding: 1px; border-radius: 0px; }"
|
||||
"QToolButton:hover { background: %1; }").arg(hover.name());
|
||||
closeBtn->setStyleSheet(style);
|
||||
}
|
||||
};
|
||||
2673
src/editor.cpp
77
src/editor.h
@@ -1,19 +1,18 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include "providerregistry.h"
|
||||
#include "themes/theme.h"
|
||||
#include <QWidget>
|
||||
#include <QSet>
|
||||
#include <QPoint>
|
||||
#include <QHash>
|
||||
|
||||
class QLineEdit;
|
||||
class QsciScintilla;
|
||||
class QsciLexerCPP;
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct SavedSourceDisplay {
|
||||
QString text;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
class RcxEditor : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
@@ -26,9 +25,15 @@ public:
|
||||
void restoreViewState(const ViewState& vs);
|
||||
|
||||
QsciScintilla* scintilla() const { return m_sci; }
|
||||
QWidget* historyPopup() const { return m_historyPopup; }
|
||||
QWidget* disasmPopup() const { return m_disasmPopup; }
|
||||
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
|
||||
const LineMeta* metaForLine(int line) const;
|
||||
int currentNodeIndex() const;
|
||||
void scrollToNodeId(uint64_t nodeId);
|
||||
void showFindBar();
|
||||
void dismissHistoryPopup();
|
||||
void dismissAllPopups();
|
||||
|
||||
// ── Column span computation ──
|
||||
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
|
||||
@@ -40,16 +45,27 @@ public:
|
||||
|
||||
// ── Inline editing ──
|
||||
bool isEditing() const { return m_editState.active; }
|
||||
bool beginInlineEdit(EditTarget target, int line = -1);
|
||||
bool beginInlineEdit(EditTarget target, int line = -1, int col = -1);
|
||||
void cancelInlineEdit();
|
||||
void setHexEditPending(bool v) { m_hexEditPending = v; }
|
||||
void setStaticCompletions(const QStringList& words) { m_staticCompletions = words; }
|
||||
|
||||
void applySelectionOverlay(const QSet<uint64_t>& selIds);
|
||||
void setCommandRowText(const QString& line);
|
||||
void setEditorFont(const QString& fontName);
|
||||
static void setGlobalFontName(const QString& fontName);
|
||||
static QString globalFontName();
|
||||
void applyTheme(const Theme& theme);
|
||||
|
||||
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
|
||||
QString textWithMargins() const;
|
||||
void setCustomTypeNames(const QStringList& names);
|
||||
void setValueHistoryRef(const QHash<uint64_t, ValueHistory>* ref) { m_valueHistory = ref; }
|
||||
void setProviderRef(const Provider* prov, const Provider* realProv, const NodeTree* tree) {
|
||||
m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree;
|
||||
}
|
||||
|
||||
void setRelativeOffsets(bool rel) { m_relativeOffsets = rel; reformatMargins(); }
|
||||
|
||||
// Saved sources for quick-switch in source picker
|
||||
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
||||
@@ -57,10 +73,19 @@ public:
|
||||
signals:
|
||||
void marginClicked(int margin, int line, Qt::KeyboardModifiers mods);
|
||||
void contextMenuRequested(int line, int nodeIdx, int subLine, QPoint globalPos);
|
||||
void keywordConvertRequested(const QString& newKeyword);
|
||||
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
|
||||
void inlineEditCommitted(int nodeIdx, int subLine,
|
||||
EditTarget target, const QString& text);
|
||||
EditTarget target, const QString& text,
|
||||
uint64_t resolvedAddr = 0);
|
||||
void inlineEditCancelled();
|
||||
void typeSelectorRequested();
|
||||
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
||||
void insertAboveRequested(int nodeIdx, NodeKind kind);
|
||||
void relativeOffsetsChanged(bool relative);
|
||||
void appendBytesRequested(uint64_t structId, int byteCount);
|
||||
void trimHexRequested(uint64_t structId);
|
||||
void appendEnumMembersRequested(uint64_t enumId, int count);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
@@ -71,6 +96,9 @@ private:
|
||||
QVector<LineMeta> m_meta;
|
||||
LayoutInfo m_layout; // cached from ComposeResult
|
||||
|
||||
// ── Toggle: absolute vs relative offset margin
|
||||
bool m_relativeOffsets = true;
|
||||
|
||||
int m_marginStyleBase = -1;
|
||||
int m_hintLine = -1;
|
||||
|
||||
@@ -79,8 +107,12 @@ private:
|
||||
bool m_hoverInside = false;
|
||||
uint64_t m_hoveredNodeId = 0;
|
||||
int m_hoveredLine = -1;
|
||||
uint64_t m_prevHoveredNodeId = 0; // for incremental marker update
|
||||
int m_prevHoveredLine = -1; // for incremental marker update
|
||||
QSet<uint64_t> m_currentSelIds;
|
||||
QVector<int> m_hoverSpanLines; // Lines with hover span indicators
|
||||
// ── nodeId → display-line index (built in applyDocument) ──
|
||||
QHash<uint64_t, QVector<int>> m_nodeLineIndex;
|
||||
// ── Drag selection ──
|
||||
bool m_dragging = false;
|
||||
bool m_dragStarted = false; // true once drag threshold exceeded
|
||||
@@ -108,8 +140,10 @@ private:
|
||||
NodeKind editKind = NodeKind::Int32;
|
||||
int commentCol = -1; // fixed comment column (stored at edit start)
|
||||
bool lastValidationOk = true; // track state to avoid redundant updates
|
||||
bool hexOverwrite = false; // true for hex-byte / ASCII-preview fixed-length editing
|
||||
};
|
||||
InlineEditState m_editState;
|
||||
QStringList m_staticCompletions; // autocomplete words for StaticExpr editing
|
||||
|
||||
// ── Tab cycling state ──
|
||||
EditTarget m_lastTabTarget = EditTarget::Value;
|
||||
@@ -120,7 +154,27 @@ 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; // TitleBodyPopup (file-local class in editor.cpp)
|
||||
QWidget* m_structPreviewPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
|
||||
QWidget* m_arrowTooltip = nullptr; // RcxTooltip (arrow callout)
|
||||
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
|
||||
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||
const NodeTree* m_disasmTree = nullptr;
|
||||
|
||||
// ── Find bar ──
|
||||
QWidget* m_findBarContainer = nullptr;
|
||||
QLineEdit* m_findBar = nullptr;
|
||||
long m_findPos = 0;
|
||||
void hideFindBar();
|
||||
|
||||
// ── Hex inline edit ──
|
||||
bool m_hexEditPending = false; // set by context menu before calling beginInlineEdit
|
||||
|
||||
// ── Reentrancy guards ──
|
||||
bool m_applyingDocument = false;
|
||||
bool m_clampingSelection = false;
|
||||
bool m_updatingComment = false;
|
||||
|
||||
@@ -131,11 +185,11 @@ private:
|
||||
void setupMarkers();
|
||||
void allocateMarginStyles();
|
||||
|
||||
void applyMarginText(const QVector<LineMeta>& meta);
|
||||
void applyMarkers(const QVector<LineMeta>& meta);
|
||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||
void applyLineAttributes(const QVector<LineMeta>& meta);
|
||||
void reformatMargins();
|
||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||
void applyDataChangedHighlight(const QVector<LineMeta>& meta);
|
||||
void applyHeatmapHighlight(const QVector<LineMeta>& meta, const QVector<QString>& lineTexts);
|
||||
void applySymbolColoring(const QVector<LineMeta>& meta, const QVector<QString>& lineTexts);
|
||||
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||
void applyCommandRowPills();
|
||||
|
||||
@@ -143,6 +197,7 @@ private:
|
||||
int editEndCol() const;
|
||||
bool handleNormalKey(QKeyEvent* ke);
|
||||
bool handleEditKey(QKeyEvent* ke);
|
||||
bool handleHexEditKey(QKeyEvent* ke);
|
||||
void showTypeAutocomplete();
|
||||
void showSourcePicker();
|
||||
void showTypeListFiltered(const QString& filter);
|
||||
|
||||
1
src/examples/EPROCESS.rcx
Normal file
1316
src/examples/KUSER_SHARED_DATA.rcx
Normal file
616
src/examples/MMPFN.rcx
Normal file
@@ -0,0 +1,616 @@
|
||||
{
|
||||
"baseAddress": "FFFFCA8010000000",
|
||||
"nextId": "3000",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "100",
|
||||
"kind": "Struct",
|
||||
"name": "list_entry",
|
||||
"structTypeName": "_LIST_ENTRY",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "101",
|
||||
"kind": "Pointer64",
|
||||
"name": "Flink",
|
||||
"offset": 0,
|
||||
"parentId": "100",
|
||||
"refId": "100",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "102",
|
||||
"kind": "Pointer64",
|
||||
"name": "Blink",
|
||||
"offset": 8,
|
||||
"parentId": "100",
|
||||
"refId": "100",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "200",
|
||||
"kind": "Struct",
|
||||
"name": "balanced_node",
|
||||
"structTypeName": "_RTL_BALANCED_NODE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "210",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 0,
|
||||
"parentId": "200",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "211",
|
||||
"kind": "Pointer64",
|
||||
"name": "Left",
|
||||
"offset": 0,
|
||||
"parentId": "210",
|
||||
"refId": "200",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "212",
|
||||
"kind": "Pointer64",
|
||||
"name": "Right",
|
||||
"offset": 8,
|
||||
"parentId": "210",
|
||||
"refId": "200",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "220",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 16,
|
||||
"parentId": "200",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "221",
|
||||
"kind": "UInt64",
|
||||
"name": "ParentValue",
|
||||
"offset": 0,
|
||||
"parentId": "220"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "300",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte",
|
||||
"structTypeName": "_MMPTE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "301",
|
||||
"kind": "Struct",
|
||||
"name": "u",
|
||||
"classKeyword": "union",
|
||||
"offset": 0,
|
||||
"parentId": "300",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "302",
|
||||
"kind": "UInt64",
|
||||
"name": "Long",
|
||||
"offset": 0,
|
||||
"parentId": "301"
|
||||
},
|
||||
{
|
||||
"id": "303",
|
||||
"kind": "Struct",
|
||||
"name": "Hard",
|
||||
"structTypeName": "_MMPTE_HARDWARE",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "400",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "304",
|
||||
"kind": "Struct",
|
||||
"name": "Proto",
|
||||
"structTypeName": "_MMPTE_PROTOTYPE",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "600",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "305",
|
||||
"kind": "Struct",
|
||||
"name": "Soft",
|
||||
"structTypeName": "_MMPTE_SOFTWARE",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "500",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "306",
|
||||
"kind": "Struct",
|
||||
"name": "Trans",
|
||||
"structTypeName": "_MMPTE_TRANSITION",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "700",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "307",
|
||||
"kind": "Struct",
|
||||
"name": "Subsect",
|
||||
"structTypeName": "_MMPTE_SUBSECTION",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "800",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "308",
|
||||
"kind": "Struct",
|
||||
"name": "TimeStamp",
|
||||
"structTypeName": "_MMPTE_TIMESTAMP",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "900",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "309",
|
||||
"kind": "Struct",
|
||||
"name": "List",
|
||||
"structTypeName": "_MMPTE_LIST",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "1000",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "400",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_hardware",
|
||||
"structTypeName": "_MMPTE_HARDWARE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "401",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 Dirty1:1 Owner:1 WriteThrough:1 CacheDisable:1 Accessed:1 Dirty:1 LargePage:1 Global:1 CopyOnWrite:1 Unused:1 Write:1 PageFrameNumber:40 ReservedForSoftware:4 WsleAge:4 WsleProtection:3 NoExecute:1",
|
||||
"offset": 0,
|
||||
"parentId": "400"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "500",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_software",
|
||||
"structTypeName": "_MMPTE_SOFTWARE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "501",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 PageFileReserved:1 PageFileAllocated:1 ColdPage:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 UsedPageTableEntries:10 ShadowStack:1 OnStandbyLookaside:1 Unused:4 PageFileHigh:32",
|
||||
"offset": 0,
|
||||
"parentId": "500"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "600",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_prototype",
|
||||
"structTypeName": "_MMPTE_PROTOTYPE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "601",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 DemandFillProto:1 HiberVerifyConverted:1 ReadOnly:1 SwizzleBit:1 Protection:5 Prototype:1 Combined:1 Unused1:4 ProtoAddress:48",
|
||||
"offset": 0,
|
||||
"parentId": "600"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "700",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_transition",
|
||||
"structTypeName": "_MMPTE_TRANSITION",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "701",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 Write:1 OnStandbyLookaside:1 IoTracker:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFrameNumber:40 Unused:12",
|
||||
"offset": 0,
|
||||
"parentId": "700"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "800",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_subsection",
|
||||
"structTypeName": "_MMPTE_SUBSECTION",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "801",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 Unused0:2 OnStandbyLookaside:1 SwizzleBit:1 Protection:5 Prototype:1 ColdPage:1 Unused2:3 ExecutePrivilege:1 SubsectionAddress:48",
|
||||
"offset": 0,
|
||||
"parentId": "800"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "900",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_timestamp",
|
||||
"structTypeName": "_MMPTE_TIMESTAMP",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "901",
|
||||
"kind": "Hex64",
|
||||
"name": "MustBeZero:1 Unused:3 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 Reserved:16 GlobalTimeStamp:32",
|
||||
"offset": 0,
|
||||
"parentId": "900"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1000",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_list",
|
||||
"structTypeName": "_MMPTE_LIST",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1001",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 OneEntry:1 filler0:2 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 filler1:13 NextEntry:39",
|
||||
"offset": 0,
|
||||
"parentId": "1000"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1100",
|
||||
"kind": "Struct",
|
||||
"name": "mipfnflink",
|
||||
"structTypeName": "_MIPFNFLINK",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1101",
|
||||
"kind": "Hex64",
|
||||
"name": "Flink",
|
||||
"offset": 0,
|
||||
"parentId": "1100"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1200",
|
||||
"kind": "Struct",
|
||||
"name": "mipfnblink",
|
||||
"structTypeName": "_MIPFNBLINK",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1201",
|
||||
"kind": "Hex64",
|
||||
"name": "Blink",
|
||||
"offset": 0,
|
||||
"parentId": "1200"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1300",
|
||||
"kind": "Struct",
|
||||
"name": "mmpfnentry1",
|
||||
"structTypeName": "_MMPFNENTRY1",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1301",
|
||||
"kind": "Hex8",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1300"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1400",
|
||||
"kind": "Struct",
|
||||
"name": "mmpfnentry3",
|
||||
"structTypeName": "_MMPFNENTRY3",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1401",
|
||||
"kind": "Hex8",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1400"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1500",
|
||||
"kind": "Struct",
|
||||
"name": "mi_pfn_flags",
|
||||
"structTypeName": "_MI_PFN_FLAGS",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1501",
|
||||
"kind": "Hex32",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1500"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1600",
|
||||
"kind": "Struct",
|
||||
"name": "mi_pfn_flags4",
|
||||
"structTypeName": "_MI_PFN_FLAGS4",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1601",
|
||||
"kind": "Hex64",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1600"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1700",
|
||||
"kind": "Struct",
|
||||
"name": "mi_pfn_flags5",
|
||||
"structTypeName": "_MI_PFN_FLAGS5",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1701",
|
||||
"kind": "Hex32",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1700"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2000",
|
||||
"kind": "Struct",
|
||||
"name": "mmpfn",
|
||||
"structTypeName": "_MMPFN",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2001",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 0,
|
||||
"parentId": "2000",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2010",
|
||||
"kind": "Struct",
|
||||
"name": "ListEntry",
|
||||
"structTypeName": "_LIST_ENTRY",
|
||||
"offset": 0,
|
||||
"parentId": "2001",
|
||||
"refId": "100",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2011",
|
||||
"kind": "Struct",
|
||||
"name": "TreeNode",
|
||||
"structTypeName": "_RTL_BALANCED_NODE",
|
||||
"offset": 0,
|
||||
"parentId": "2001",
|
||||
"refId": "200",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2012",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"offset": 0,
|
||||
"parentId": "2001",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2013",
|
||||
"kind": "Struct",
|
||||
"name": "u1",
|
||||
"structTypeName": "_MIPFNFLINK",
|
||||
"offset": 0,
|
||||
"parentId": "2012",
|
||||
"refId": "1100",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2014",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 8,
|
||||
"parentId": "2012",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2015",
|
||||
"kind": "Pointer64",
|
||||
"name": "PteAddress",
|
||||
"offset": 0,
|
||||
"parentId": "2014",
|
||||
"refId": "300",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2016",
|
||||
"kind": "UInt64",
|
||||
"name": "PteLong",
|
||||
"offset": 0,
|
||||
"parentId": "2014"
|
||||
},
|
||||
{
|
||||
"id": "2017",
|
||||
"kind": "Struct",
|
||||
"name": "OriginalPte",
|
||||
"structTypeName": "_MMPTE",
|
||||
"offset": 16,
|
||||
"parentId": "2012",
|
||||
"refId": "300",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2020",
|
||||
"kind": "Struct",
|
||||
"name": "u2",
|
||||
"structTypeName": "_MIPFNBLINK",
|
||||
"offset": 24,
|
||||
"parentId": "2000",
|
||||
"refId": "1200",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2030",
|
||||
"kind": "Struct",
|
||||
"name": "u3",
|
||||
"classKeyword": "union",
|
||||
"offset": 32,
|
||||
"parentId": "2000",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2031",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"offset": 0,
|
||||
"parentId": "2030",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2032",
|
||||
"kind": "UInt16",
|
||||
"name": "ReferenceCount",
|
||||
"offset": 0,
|
||||
"parentId": "2031"
|
||||
},
|
||||
{
|
||||
"id": "2033",
|
||||
"kind": "Struct",
|
||||
"name": "e1",
|
||||
"structTypeName": "_MMPFNENTRY1",
|
||||
"offset": 2,
|
||||
"parentId": "2031",
|
||||
"refId": "1300",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2034",
|
||||
"kind": "Struct",
|
||||
"name": "e4",
|
||||
"structTypeName": "_MI_PFN_FLAGS",
|
||||
"offset": 0,
|
||||
"parentId": "2030",
|
||||
"refId": "1500",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2040",
|
||||
"kind": "Struct",
|
||||
"name": "u5",
|
||||
"structTypeName": "_MI_PFN_FLAGS5",
|
||||
"offset": 36,
|
||||
"parentId": "2000",
|
||||
"refId": "1700",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2050",
|
||||
"kind": "Struct",
|
||||
"name": "u4",
|
||||
"structTypeName": "_MI_PFN_FLAGS4",
|
||||
"offset": 40,
|
||||
"parentId": "2000",
|
||||
"refId": "1600",
|
||||
"collapsed": true
|
||||
}
|
||||
]
|
||||
}
|
||||
126
src/examples/PageTables.rcx
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"baseAddress": "0",
|
||||
"nextId": "2000",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "100",
|
||||
"kind": "Struct",
|
||||
"name": "pte",
|
||||
"structTypeName": "X64_PTE",
|
||||
"classKeyword": "bitfield",
|
||||
"elementKind": "UInt64",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true,
|
||||
"arrayLen": 1,
|
||||
"strLen": 64,
|
||||
"bitfieldMembers": [
|
||||
{"name": "Present", "bitOffset": 0, "bitWidth": 1},
|
||||
{"name": "ReadWrite", "bitOffset": 1, "bitWidth": 1},
|
||||
{"name": "UserSuper", "bitOffset": 2, "bitWidth": 1},
|
||||
{"name": "WriteThrough", "bitOffset": 3, "bitWidth": 1},
|
||||
{"name": "CacheDisable", "bitOffset": 4, "bitWidth": 1},
|
||||
{"name": "Accessed", "bitOffset": 5, "bitWidth": 1},
|
||||
{"name": "Dirty", "bitOffset": 6, "bitWidth": 1},
|
||||
{"name": "PageSize", "bitOffset": 7, "bitWidth": 1},
|
||||
{"name": "Global", "bitOffset": 8, "bitWidth": 1},
|
||||
{"name": "AVL", "bitOffset": 9, "bitWidth": 3},
|
||||
{"name": "PhysAddr", "bitOffset": 12, "bitWidth": 40},
|
||||
{"name": "Available", "bitOffset": 52, "bitWidth": 7},
|
||||
{"name": "ProtKey", "bitOffset": 59, "bitWidth": 4},
|
||||
{"name": "NX", "bitOffset": 63, "bitWidth": 1}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "200",
|
||||
"kind": "Struct",
|
||||
"name": "page_table",
|
||||
"structTypeName": "X64_PAGE_TABLE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "201",
|
||||
"kind": "Array",
|
||||
"name": "entries",
|
||||
"offset": 0,
|
||||
"parentId": "200",
|
||||
"refId": "100",
|
||||
"elementKind": "Struct",
|
||||
"arrayLen": 512,
|
||||
"strLen": 64,
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "300",
|
||||
"kind": "Struct",
|
||||
"name": "pde_2mb",
|
||||
"structTypeName": "X64_PDE_LARGE",
|
||||
"classKeyword": "bitfield",
|
||||
"elementKind": "UInt64",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true,
|
||||
"arrayLen": 1,
|
||||
"strLen": 64,
|
||||
"bitfieldMembers": [
|
||||
{"name": "Present", "bitOffset": 0, "bitWidth": 1},
|
||||
{"name": "ReadWrite", "bitOffset": 1, "bitWidth": 1},
|
||||
{"name": "UserSuper", "bitOffset": 2, "bitWidth": 1},
|
||||
{"name": "WriteThrough", "bitOffset": 3, "bitWidth": 1},
|
||||
{"name": "CacheDisable", "bitOffset": 4, "bitWidth": 1},
|
||||
{"name": "Accessed", "bitOffset": 5, "bitWidth": 1},
|
||||
{"name": "Dirty", "bitOffset": 6, "bitWidth": 1},
|
||||
{"name": "PageSize", "bitOffset": 7, "bitWidth": 1},
|
||||
{"name": "Global", "bitOffset": 8, "bitWidth": 1},
|
||||
{"name": "AVL", "bitOffset": 9, "bitWidth": 3},
|
||||
{"name": "PAT", "bitOffset": 12, "bitWidth": 1},
|
||||
{"name": "Reserved", "bitOffset": 13, "bitWidth": 8},
|
||||
{"name": "PhysAddr", "bitOffset": 21, "bitWidth": 31},
|
||||
{"name": "Available", "bitOffset": 52, "bitWidth": 7},
|
||||
{"name": "ProtKey", "bitOffset": 59, "bitWidth": 4},
|
||||
{"name": "NX", "bitOffset": 63, "bitWidth": 1}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"id": "400",
|
||||
"kind": "Struct",
|
||||
"name": "pdpte_1gb",
|
||||
"structTypeName": "X64_PDPTE_HUGE",
|
||||
"classKeyword": "bitfield",
|
||||
"elementKind": "UInt64",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true,
|
||||
"arrayLen": 1,
|
||||
"strLen": 64,
|
||||
"bitfieldMembers": [
|
||||
{"name": "Present", "bitOffset": 0, "bitWidth": 1},
|
||||
{"name": "ReadWrite", "bitOffset": 1, "bitWidth": 1},
|
||||
{"name": "UserSuper", "bitOffset": 2, "bitWidth": 1},
|
||||
{"name": "WriteThrough", "bitOffset": 3, "bitWidth": 1},
|
||||
{"name": "CacheDisable", "bitOffset": 4, "bitWidth": 1},
|
||||
{"name": "Accessed", "bitOffset": 5, "bitWidth": 1},
|
||||
{"name": "Dirty", "bitOffset": 6, "bitWidth": 1},
|
||||
{"name": "PageSize", "bitOffset": 7, "bitWidth": 1},
|
||||
{"name": "Global", "bitOffset": 8, "bitWidth": 1},
|
||||
{"name": "AVL", "bitOffset": 9, "bitWidth": 3},
|
||||
{"name": "PAT", "bitOffset": 12, "bitWidth": 1},
|
||||
{"name": "Reserved", "bitOffset": 13, "bitWidth": 17},
|
||||
{"name": "PhysAddr", "bitOffset": 30, "bitWidth": 22},
|
||||
{"name": "Available", "bitOffset": 52, "bitWidth": 7},
|
||||
{"name": "ProtKey", "bitOffset": 59, "bitWidth": 4},
|
||||
{"name": "NX", "bitOffset": 63, "bitWidth": 1}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rootIds": ["200"]
|
||||
}
|
||||
1
src/examples/Vergilius_25H2.rcx
Normal file
1
src/examples/WinSDK.rcx
Normal file
@@ -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": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "27",
|
||||
"kind": "Hex64",
|
||||
"name": "field_70",
|
||||
"offset": 112,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
},
|
||||
{
|
||||
"arrayLen": 1,
|
||||
"collapsed": false,
|
||||
"elementKind": "UInt8",
|
||||
"id": "28",
|
||||
"kind": "Hex64",
|
||||
"name": "field_78",
|
||||
"offset": 120,
|
||||
"parentId": "1",
|
||||
"refId": "0",
|
||||
"strLen": 64
|
||||
}
|
||||
]
|
||||
}
|
||||
10755
src/examples/t6zm.rcx
Normal file
42817
src/examples/windows-x86_64.h
Normal file
BIN
src/fonts/JetBrainsMono.ttf
Normal file
303
src/format.cpp
@@ -1,4 +1,6 @@
|
||||
#include "core.h"
|
||||
#include "addressparser.h"
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
@@ -21,6 +23,14 @@ static QString fit(QString s, int w) {
|
||||
return s.leftJustified(w, ' ');
|
||||
}
|
||||
|
||||
// Like fit() but overflows instead of truncating: if s exceeds w, return full string
|
||||
static QString fitOverflow(const QString& s, int w) {
|
||||
if (w <= 0) return {};
|
||||
if (s.size() <= w)
|
||||
return s.leftJustified(w, ' ');
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── Type name ──
|
||||
|
||||
// Override seam: injectable type-name provider
|
||||
@@ -41,13 +51,15 @@ QString typeName(NodeKind kind, int colType) {
|
||||
return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), colType);
|
||||
}
|
||||
|
||||
// Array type string: "uint32_t[16]" or "char[64]"
|
||||
QString arrayTypeName(NodeKind elemKind, int count) {
|
||||
auto* m = kindMeta(elemKind);
|
||||
QString elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
||||
// char[] for UInt8, wchar_t[] for UInt16
|
||||
if (elemKind == NodeKind::UInt8) elem = QStringLiteral("char");
|
||||
else if (elemKind == NodeKind::UInt16) elem = QStringLiteral("wchar_t");
|
||||
// Array type string: "uint32_t[16]" or "Material[2]"
|
||||
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName) {
|
||||
QString elem;
|
||||
if (elemKind == NodeKind::Struct && !structName.isEmpty())
|
||||
elem = structName;
|
||||
else {
|
||||
auto* m = kindMeta(elemKind);
|
||||
elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
||||
}
|
||||
return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]");
|
||||
}
|
||||
|
||||
@@ -61,29 +73,45 @@ QString pointerTypeName(NodeKind kind, const QString& targetName) {
|
||||
// ── Value formatting ──
|
||||
|
||||
static QString hexVal(uint64_t v) {
|
||||
return QStringLiteral("0x") + QString::number(v, 16);
|
||||
return QString::asprintf("0x%llx", (unsigned long long)v);
|
||||
}
|
||||
|
||||
static QString rawHex(uint64_t v, int digits) {
|
||||
return QString::number(v, 16).rightJustified(digits, '0');
|
||||
}
|
||||
|
||||
QString fmtInt8(int8_t v) { return hexVal((uint8_t)v); }
|
||||
QString fmtInt16(int16_t v) { return hexVal((uint16_t)v); }
|
||||
QString fmtInt32(int32_t v) { return hexVal((uint32_t)v); }
|
||||
QString fmtInt64(int64_t v) { return hexVal((uint64_t)v); }
|
||||
QString fmtInt8(int8_t v) { return QString::number(v); }
|
||||
QString fmtInt16(int16_t v) { return QString::number(v); }
|
||||
QString fmtInt32(int32_t v) { return QString::number(v); }
|
||||
QString fmtInt64(int64_t v) { return QString::number((qlonglong)v); }
|
||||
QString fmtUInt8(uint8_t v) { return hexVal(v); }
|
||||
QString fmtUInt16(uint16_t v) { return hexVal(v); }
|
||||
QString fmtUInt32(uint32_t v) { return hexVal(v); }
|
||||
QString fmtUInt64(uint64_t v) { return hexVal(v); }
|
||||
|
||||
QString fmtFloat(float v) {
|
||||
QString s = QString::number(v, 'g', 4);
|
||||
if (!s.contains('.') && !s.contains('e') && !s.contains('E'))
|
||||
s += QStringLiteral(".f");
|
||||
else
|
||||
s += QLatin1Char('f');
|
||||
return s;
|
||||
// Fixed 7-char body: digits + "." + decimals + "f"
|
||||
// Negative values get a '-' prefix (8 chars total), positive stay 7.
|
||||
if (std::isnan(v)) return QStringLiteral("NaN");
|
||||
if (std::isinf(v)) return v > 0 ? QStringLiteral("inff") : QStringLiteral("-inff");
|
||||
|
||||
float av = std::fabs(v);
|
||||
if (av >= 100000.f)
|
||||
return v < 0 ? QStringLiteral("-99999+f") : QStringLiteral("99999+f");
|
||||
|
||||
// body = digits + "." + decimals + "f", target exactly 7 chars.
|
||||
// Start with max decimals, reduce if integer part is wide or rounding overflows.
|
||||
for (int dec = 4; dec >= 0; dec--) {
|
||||
QString body = QString::number(av, 'f', dec);
|
||||
body += (dec == 0) ? QStringLiteral(".f") : QStringLiteral("f");
|
||||
if (body.size() == 7) {
|
||||
if (v < 0.f) body.prepend('-');
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
// Rounding pushed past 99999 — use overflow cap
|
||||
return v < 0 ? QStringLiteral("-99999+f") : QStringLiteral("99999+f");
|
||||
}
|
||||
QString fmtDouble(double v) {
|
||||
QString s = QString::number(v, 'g', 6);
|
||||
@@ -93,15 +121,8 @@ QString fmtDouble(double v) {
|
||||
}
|
||||
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
|
||||
|
||||
QString fmtPointer32(uint32_t v) {
|
||||
if (v == 0) return QStringLiteral("-> NULL");
|
||||
return QStringLiteral("-> ") + hexVal(v);
|
||||
}
|
||||
|
||||
QString fmtPointer64(uint64_t v) {
|
||||
if (v == 0) return QStringLiteral("-> NULL");
|
||||
return QStringLiteral("-> ") + hexVal(v);
|
||||
}
|
||||
QString fmtPointer32(uint32_t v) { return hexVal(v); }
|
||||
QString fmtPointer64(uint64_t v) { return hexVal(v); }
|
||||
|
||||
// ── Indentation ──
|
||||
|
||||
@@ -111,40 +132,52 @@ QString indent(int depth) {
|
||||
|
||||
// ── Offset margin ──
|
||||
|
||||
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation) {
|
||||
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits) {
|
||||
if (isContinuation) return QStringLiteral(" \u00B7 ");
|
||||
return QString::number(absoluteOffset, 16).toUpper() + QChar(' ');
|
||||
return QString::number(absoluteOffset, 16).toUpper()
|
||||
.rightJustified(hexDigits, '0') + QChar(' ');
|
||||
}
|
||||
|
||||
// ── Struct type name (for width calculation) ──
|
||||
|
||||
QString structTypeName(const Node& node) {
|
||||
// Full type string: "struct TypeName" or just "struct" if no typename
|
||||
QString base = typeName(node.kind).trimmed(); // "struct"
|
||||
// Named types: just the type name (e.g. "_LIST_ENTRY")
|
||||
// Anonymous: just the keyword (e.g. "union", "struct")
|
||||
if (!node.structTypeName.isEmpty())
|
||||
return base + QStringLiteral(" ") + node.structTypeName;
|
||||
return base;
|
||||
return node.structTypeName;
|
||||
return node.resolvedClassKeyword();
|
||||
}
|
||||
|
||||
// ── Struct header / footer ──
|
||||
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName) {
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName, bool compact) {
|
||||
// Columnar format: <type> <name> { (or no brace when collapsed)
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(structTypeName(node), colType);
|
||||
QString rawType = structTypeName(node);
|
||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||
if (node.name.isEmpty()) {
|
||||
// Anonymous struct/union: "union {" with no column padding
|
||||
return ind + rawType + SEP + suffix;
|
||||
}
|
||||
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
|
||||
return ind + type + SEP + node.name + SEP + suffix;
|
||||
}
|
||||
|
||||
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
||||
return indent(depth) + QStringLiteral("};");
|
||||
QString fmtStructFooter(const Node& node, int depth, int /*totalSize*/) {
|
||||
QString footer = indent(depth) + QStringLiteral("};");
|
||||
if (node.resolvedClassKeyword() == QStringLiteral("enum"))
|
||||
footer += QStringLiteral(" +10");
|
||||
else
|
||||
footer += QStringLiteral(" +10h +100h +1000h Trim");
|
||||
return footer;
|
||||
}
|
||||
|
||||
// ── Array header ──
|
||||
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
|
||||
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName) {
|
||||
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName, bool compact) {
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen), colType);
|
||||
QString rawType = arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
|
||||
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
|
||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||
return ind + type + SEP + node.name + SEP + suffix;
|
||||
}
|
||||
@@ -153,10 +186,16 @@ QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collap
|
||||
|
||||
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
||||
const Provider& prov, uint64_t addr,
|
||||
const QString& ptrTypeName, int colType, int colName) {
|
||||
const QString& ptrTypeName, int colType, int colName,
|
||||
bool compact) {
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(ptrTypeName, colType);
|
||||
bool overflow = compact && ptrTypeName.size() > colType;
|
||||
QString type = compact ? fitOverflow(ptrTypeName, colType) : fit(ptrTypeName, colType);
|
||||
if (collapsed) {
|
||||
if (overflow) {
|
||||
// Overflow: no column padding
|
||||
return ind + type + SEP + node.name + SEP + readValue(node, prov, addr, 0);
|
||||
}
|
||||
// Collapsed: show pointer value instead of brace (name padded for value alignment)
|
||||
QString name = fit(node.name, colName);
|
||||
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
|
||||
@@ -194,15 +233,18 @@ static QString bytesToAscii(const QByteArray& b, int slot) {
|
||||
return out;
|
||||
}
|
||||
|
||||
static const char kHexDigits[] = "0123456789ABCDEF";
|
||||
|
||||
static QString bytesToHex(const QByteArray& b, int slot) {
|
||||
QString out;
|
||||
out.reserve(slot * 3);
|
||||
QChar buf[64]; // max slot=8 → 8*3-1=23 chars; 64 is plenty
|
||||
int pos = 0;
|
||||
for (int i = 0; i < slot; ++i) {
|
||||
uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0;
|
||||
out += QString::asprintf("%02X", (unsigned)c);
|
||||
if (i + 1 < slot) out += ' ';
|
||||
buf[pos++] = QLatin1Char(kHexDigits[c >> 4]);
|
||||
buf[pos++] = QLatin1Char(kHexDigits[c & 0xF]);
|
||||
if (i + 1 < slot) buf[pos++] = QLatin1Char(' ');
|
||||
}
|
||||
return out;
|
||||
return QString(buf, pos);
|
||||
}
|
||||
|
||||
static QString fmtAsciiAndBytes(const Provider& prov, uint64_t addr,
|
||||
@@ -242,15 +284,55 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
if (!display) return rawHex(val, 8);
|
||||
QString s = fmtPointer32(val);
|
||||
QString sym = prov.getSymbol((uint64_t)val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::Pointer64: {
|
||||
uint64_t val = prov.readU64(addr);
|
||||
// Primitive pointer: dereference and show target value
|
||||
// (hex/ptr/fnptr targets fall through to plain void* display)
|
||||
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind) && val != 0) {
|
||||
uint64_t target = val;
|
||||
for (int d = 1; d < node.ptrDepth && target != 0; d++)
|
||||
target = prov.isReadable(target, 8) ? prov.readU64(target) : 0;
|
||||
if (target != 0 && prov.isReadable(target, sizeForKind(node.elementKind))) {
|
||||
// Create a temporary node of the target kind to format the value
|
||||
Node tmp;
|
||||
tmp.kind = node.elementKind;
|
||||
tmp.strLen = node.strLen;
|
||||
QString derefVal = readValueImpl(tmp, prov, target, 0, mode);
|
||||
if (display) {
|
||||
QString arrow = QStringLiteral("-> ");
|
||||
QString sym = prov.getSymbol(val);
|
||||
if (!sym.isEmpty())
|
||||
return arrow + derefVal + QStringLiteral(" // ") + sym;
|
||||
return arrow + derefVal;
|
||||
}
|
||||
return derefVal;
|
||||
}
|
||||
if (!display) return rawHex(val, 16);
|
||||
return fmtPointer64(val);
|
||||
}
|
||||
if (!display) return rawHex(val, 16);
|
||||
QString s = fmtPointer64(val);
|
||||
QString sym = prov.getSymbol(val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::FuncPtr32: {
|
||||
uint32_t val = prov.readU32(addr);
|
||||
if (!display) return rawHex(val, 8);
|
||||
QString s = fmtPointer32(val);
|
||||
QString sym = prov.getSymbol((uint64_t)val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::FuncPtr64: {
|
||||
uint64_t val = prov.readU64(addr);
|
||||
if (!display) return rawHex(val, 16);
|
||||
QString s = fmtPointer64(val);
|
||||
QString sym = prov.getSymbol(val);
|
||||
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
|
||||
return s;
|
||||
}
|
||||
case NodeKind::Vec2:
|
||||
@@ -259,21 +341,20 @@ static QString readValueImpl(const Node& node, const Provider& prov,
|
||||
int count = sizeForKind(node.kind) / 4;
|
||||
QStringList parts;
|
||||
for (int i = 0; i < count; i++)
|
||||
parts << fmtFloat(prov.readF32(addr + i * 4)).trimmed();
|
||||
parts << fmtFloat(prov.readF32(addr + i * 4));
|
||||
return parts.join(QStringLiteral(", "));
|
||||
}
|
||||
case NodeKind::Mat4x4: {
|
||||
if (!display) return {}; // not editable as single value
|
||||
if (subLine < 0 || subLine >= 4) return QStringLiteral("?");
|
||||
QString line = QStringLiteral("[");
|
||||
QString line = QStringLiteral("row%1 [").arg(subLine);
|
||||
for (int c = 0; c < 4; c++) {
|
||||
if (c > 0) line += QStringLiteral(", ");
|
||||
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4)).trimmed();
|
||||
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4));
|
||||
}
|
||||
line += QStringLiteral("]");
|
||||
return line;
|
||||
}
|
||||
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');
|
||||
@@ -306,48 +387,46 @@ QString readValue(const Node& node, const Provider& prov,
|
||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
uint64_t addr, int depth, int subLine,
|
||||
const QString& comment, int colType, int colName,
|
||||
const QString& typeOverride) {
|
||||
const QString& typeOverride, bool compact) {
|
||||
QString ind = indent(depth);
|
||||
QString type = typeOverride.isEmpty() ? typeName(node.kind, colType) : fit(typeOverride, colType);
|
||||
QString name = fit(node.name, colName);
|
||||
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
||||
const int prefixW = colType + colName + 2 * kSepWidth;
|
||||
|
||||
// Comment suffix (padded or empty)
|
||||
QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ')
|
||||
// Compute raw type string for overflow detection
|
||||
QString rawType = typeOverride.isEmpty() ? typeNameRaw(node.kind) : typeOverride;
|
||||
bool overflow = compact && rawType.size() > colType;
|
||||
|
||||
QString type = overflow ? fitOverflow(rawType, colType)
|
||||
: (typeOverride.isEmpty() ? typeName(node.kind, colType)
|
||||
: fit(typeOverride, colType));
|
||||
QString name = overflow ? node.name : fit(node.name, colName);
|
||||
|
||||
// Effective column width for this line (accounts for overflow, clamped to hard max)
|
||||
int effectiveColType = overflow ? rawType.size() : colType;
|
||||
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
||||
const int prefixW = effectiveColType + (overflow ? name.size() : colName) + 2 * kSepWidth;
|
||||
|
||||
// Comment suffix (only present when a comment is provided; no trailing padding)
|
||||
QString cmtSuffix = comment.isEmpty() ? QString()
|
||||
: fit(comment, COL_COMMENT);
|
||||
|
||||
// Mat4x4: subLine 0..3 = rows
|
||||
// Mat4x4: subLine 0..3 = rows — no truncation so large floats always display fully
|
||||
if (node.kind == NodeKind::Mat4x4) {
|
||||
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||
QString val = readValue(node, prov, addr, subLine);
|
||||
if (subLine == 0) return ind + type + SEP + name + SEP + val + cmtSuffix;
|
||||
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
||||
}
|
||||
|
||||
// Hex nodes and Padding: hex byte preview
|
||||
// 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);
|
||||
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 8 chars so hex column aligns
|
||||
const int sz = sizeForKind(node.kind);
|
||||
QByteArray b = prov.isReadable(addr, sz)
|
||||
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
||||
QString ascii = bytesToAscii(b, sz).leftJustified(8, ' ');
|
||||
QString ascii = bytesToAscii(b, sz).leftJustified(colName, ' ');
|
||||
QString hex = bytesToHex(b, sz).leftJustified(23, ' ');
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
}
|
||||
|
||||
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||
QString val = overflow ? readValue(node, prov, addr, subLine)
|
||||
: fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||
return ind + type + SEP + name + SEP + val + cmtSuffix;
|
||||
}
|
||||
|
||||
@@ -537,6 +616,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('"'))
|
||||
@@ -565,7 +652,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) {
|
||||
@@ -573,8 +661,10 @@ QString validateValue(NodeKind kind, const QString& text) {
|
||||
QString digits = hasHexPrefix ? s.mid(2) : s;
|
||||
|
||||
if (hasHexPrefix || isHexKind) {
|
||||
// Hex mode: only 0-9, a-f, A-F
|
||||
// Hex mode: only 0-9, a-f, A-F (spaces allowed for multi-byte hex kinds)
|
||||
bool isMultiByteHex = (kind >= NodeKind::Hex16 && kind <= NodeKind::Hex64);
|
||||
for (QChar c : digits) {
|
||||
if (c == ' ' && isMultiByteHex) continue;
|
||||
if (!c.isDigit() && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
|
||||
return QStringLiteral("invalid hex '%1'").arg(c);
|
||||
}
|
||||
@@ -609,43 +699,42 @@ QString validateValue(NodeKind kind, const QString& text) {
|
||||
return QStringLiteral("invalid");
|
||||
}
|
||||
|
||||
// ── Base address validation (supports simple +/- equations) ──
|
||||
// ── Base address validation (delegates to AddressParser) ──
|
||||
|
||||
QString validateBaseAddress(const QString& text) {
|
||||
QString s = text.trimmed();
|
||||
if (s.isEmpty()) return QStringLiteral("empty");
|
||||
//s.remove('`');
|
||||
return AddressParser::validate(s);
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
bool firstTerm = true;
|
||||
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW) {
|
||||
QString ind = indent(depth);
|
||||
return ind + name.leftJustified(nameW) + QStringLiteral(" = ") + QString::number(value);
|
||||
}
|
||||
|
||||
while (pos < s.size()) {
|
||||
// Skip whitespace
|
||||
while (pos < s.size() && s[pos].isSpace()) pos++;
|
||||
if (pos >= s.size()) break;
|
||||
// ── Bitfield member formatting ──
|
||||
|
||||
// Check for +/- operator (except first term)
|
||||
if (!firstTerm) {
|
||||
if (s[pos] == '+' || s[pos] == '-') pos++;
|
||||
else return QStringLiteral("invalid '%1'").arg(s[pos]);
|
||||
while (pos < s.size() && s[pos].isSpace()) pos++;
|
||||
}
|
||||
|
||||
// Skip 0x prefix if present
|
||||
if (pos + 1 < s.size() && s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X'))
|
||||
pos += 2;
|
||||
|
||||
// Must have at least one hex digit
|
||||
int numStart = pos;
|
||||
while (pos < s.size() && (s[pos].isDigit() ||
|
||||
(s[pos] >= 'a' && s[pos] <= 'f') ||
|
||||
(s[pos] >= 'A' && s[pos] <= 'F'))) pos++;
|
||||
|
||||
if (pos == numStart) return QStringLiteral("invalid");
|
||||
|
||||
firstTerm = false;
|
||||
uint64_t extractBits(const Provider& prov, uint64_t addr,
|
||||
NodeKind containerKind,
|
||||
uint8_t bitOffset, uint8_t bitWidth) {
|
||||
uint64_t container = 0;
|
||||
switch (containerKind) {
|
||||
case NodeKind::Hex8: container = prov.readU8(addr); break;
|
||||
case NodeKind::Hex16: container = prov.readU16(addr); break;
|
||||
case NodeKind::Hex32: container = prov.readU32(addr); break;
|
||||
default: container = prov.readU64(addr); break;
|
||||
}
|
||||
Q_ASSERT(bitOffset + bitWidth <= 64);
|
||||
if (bitWidth >= 64) return container >> bitOffset;
|
||||
return (container >> bitOffset) & ((1ULL << bitWidth) - 1);
|
||||
}
|
||||
|
||||
return {};
|
||||
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
|
||||
uint64_t value, int depth, int nameW) {
|
||||
QString ind = indent(depth);
|
||||
return ind + name.leftJustified(nameW)
|
||||
+ QStringLiteral(" : %1 = %2").arg(bitWidth).arg(value);
|
||||
}
|
||||
|
||||
} // namespace rcx::fmt
|
||||
|
||||
1540
src/generator.cpp
@@ -6,14 +6,82 @@
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Generate C++ struct definitions for a single root struct and all
|
||||
// nested/referenced types reachable from it.
|
||||
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr);
|
||||
// ── Code output format ──
|
||||
|
||||
// Generate C++ struct definitions for every root-level struct (full SDK).
|
||||
enum class CodeFormat : int {
|
||||
CppHeader = 0, // C/C++ struct definitions
|
||||
RustStruct, // Rust #[repr(C)] struct definitions
|
||||
DefineOffsets, // #define ClassName_FieldName 0xNN
|
||||
CSharpStruct, // C# [StructLayout] with [FieldOffset]
|
||||
PythonCtypes, // Python ctypes.Structure
|
||||
_Count
|
||||
};
|
||||
|
||||
enum class CodeScope : int {
|
||||
Current = 0, // Just the selected struct
|
||||
WithChildren, // Selected struct + all referenced types
|
||||
FullSdk, // All root-level structs
|
||||
_Count
|
||||
};
|
||||
|
||||
const char* codeFormatName(CodeFormat fmt);
|
||||
const char* codeFormatFileFilter(CodeFormat fmt);
|
||||
const char* codeScopeName(CodeScope scope);
|
||||
|
||||
// ── Format-aware dispatch (calls the appropriate backend) ──
|
||||
|
||||
QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
// Render rootStructId + all struct types reachable from it
|
||||
QString renderCodeTree(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderCodeAll(CodeFormat fmt, const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
// ── Individual backends ──
|
||||
|
||||
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderCppTree(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderCppAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr);
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderRust(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderRustTree(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderRustAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderDefines(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderDefinesTree(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderDefinesAll(const NodeTree& tree);
|
||||
|
||||
QString renderCSharp(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderCSharpTree(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderCSharpAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderPython(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderPythonTree(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderPythonAll(const NodeTree& tree);
|
||||
|
||||
// Null generator placeholder (returns empty string).
|
||||
QString renderNull(const NodeTree& tree, uint64_t rootStructId);
|
||||
|
||||
BIN
src/icons/class.icns
Normal file
BIN
src/icons/class.ico
Normal file
|
After Width: | Height: | Size: 970 B |
BIN
src/icons/class.png
Normal file
|
After Width: | Height: | Size: 918 B |
222
src/imports/export_reclass_xml.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#include "export_reclass_xml.h"
|
||||
#include <QFile>
|
||||
#include <QXmlStreamWriter>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Reverse type map: NodeKind -> ReClassEx V2016 XML Type integer
|
||||
static int xmlTypeForKind(NodeKind kind) {
|
||||
switch (kind) {
|
||||
case NodeKind::Struct: return 1; // ClassInstance
|
||||
case NodeKind::Hex32: return 4;
|
||||
case NodeKind::Hex64: return 5;
|
||||
case NodeKind::Hex16: return 6;
|
||||
case NodeKind::Hex8: return 7;
|
||||
case NodeKind::Pointer64: return 8; // ClassPointer
|
||||
case NodeKind::Pointer32: return 8;
|
||||
case NodeKind::Int64: return 9;
|
||||
case NodeKind::Int32: return 10;
|
||||
case NodeKind::Int16: return 11;
|
||||
case NodeKind::Int8: return 12;
|
||||
case NodeKind::Float: return 13;
|
||||
case NodeKind::Double: return 14;
|
||||
case NodeKind::UInt32: return 15;
|
||||
case NodeKind::UInt16: return 16;
|
||||
case NodeKind::UInt8: return 17;
|
||||
case NodeKind::UInt64: return 32;
|
||||
case NodeKind::UTF8: return 18;
|
||||
case NodeKind::UTF16: return 19;
|
||||
case NodeKind::Bool: return 17; // No native bool in ReClass, map to UInt8
|
||||
case NodeKind::Vec2: return 22;
|
||||
case NodeKind::Vec3: return 23;
|
||||
case NodeKind::Vec4: return 24;
|
||||
case NodeKind::Mat4x4: return 25;
|
||||
case NodeKind::Array: return 27; // ClassInstanceArray
|
||||
}
|
||||
return 7; // fallback to Hex8
|
||||
}
|
||||
|
||||
static int nodeSizeForExport(const Node& node) {
|
||||
switch (node.kind) {
|
||||
case NodeKind::UTF8: return node.strLen;
|
||||
case NodeKind::UTF16: return node.strLen * 2;
|
||||
case NodeKind::Array: {
|
||||
int elemSz = sizeForKind(node.elementKind);
|
||||
return node.arrayLen * (elemSz > 0 ? elemSz : 0);
|
||||
}
|
||||
default: return sizeForKind(node.kind);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a struct type name from a node ID
|
||||
static QString resolveStructName(const NodeTree& tree, uint64_t refId) {
|
||||
int idx = tree.indexOfId(refId);
|
||||
if (idx < 0) return {};
|
||||
const Node& ref = tree.nodes[idx];
|
||||
if (!ref.structTypeName.isEmpty()) return ref.structTypeName;
|
||||
return ref.name;
|
||||
}
|
||||
|
||||
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg) {
|
||||
if (tree.nodes.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No nodes to export");
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file for writing: ") + filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build child map
|
||||
QHash<uint64_t, QVector<int>> childMap;
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
childMap[tree.nodes[i].parentId].append(i);
|
||||
|
||||
QXmlStreamWriter xml(&file);
|
||||
xml.setAutoFormatting(true);
|
||||
xml.setAutoFormattingIndent(4);
|
||||
xml.writeStartDocument();
|
||||
|
||||
xml.writeStartElement(QStringLiteral("ReClass"));
|
||||
xml.writeComment(QStringLiteral("ReClassEx"));
|
||||
|
||||
// Get root structs
|
||||
QVector<int> roots = childMap.value(0);
|
||||
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
int classCount = 0;
|
||||
|
||||
for (int ri : roots) {
|
||||
const Node& root = tree.nodes[ri];
|
||||
if (root.kind != NodeKind::Struct) continue;
|
||||
|
||||
xml.writeStartElement(QStringLiteral("Class"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), root.name.isEmpty() ? root.structTypeName : root.name);
|
||||
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("28"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||
xml.writeAttribute(QStringLiteral("Offset"), QStringLiteral("0"));
|
||||
xml.writeAttribute(QStringLiteral("strOffset"), QStringLiteral("0"));
|
||||
xml.writeAttribute(QStringLiteral("Code"), QString());
|
||||
|
||||
// Get children sorted by offset
|
||||
QVector<int> children = childMap.value(root.id);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
int i = 0;
|
||||
while (i < children.size()) {
|
||||
const Node& child = tree.nodes[children[i]];
|
||||
|
||||
// Bitfield container: export as hex node (ReClassEx has no bitfield concept)
|
||||
if (child.kind == NodeKind::Struct
|
||||
&& child.resolvedClassKeyword() == QStringLiteral("bitfield")) {
|
||||
int sz = child.byteSize();
|
||||
if (sz <= 0) sz = 4;
|
||||
xml.writeStartElement(QStringLiteral("Node"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), child.name);
|
||||
NodeKind hexKind = (sz <= 1) ? NodeKind::Hex8 : (sz <= 2) ? NodeKind::Hex16
|
||||
: (sz <= 4) ? NodeKind::Hex32 : NodeKind::Hex64;
|
||||
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(hexKind)));
|
||||
xml.writeAttribute(QStringLiteral("Size"), QString::number(sz));
|
||||
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QStringLiteral("bitfield"));
|
||||
xml.writeEndElement();
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collapse consecutive hex nodes into a single Custom node (Type=21)
|
||||
if (isHexNode(child.kind)) {
|
||||
int runStart = child.offset;
|
||||
int runEnd = child.offset + child.byteSize();
|
||||
int j = i + 1;
|
||||
while (j < children.size()) {
|
||||
const Node& next = tree.nodes[children[j]];
|
||||
if (!isHexNode(next.kind)) break;
|
||||
if (next.offset < runEnd) break; // overlap
|
||||
runEnd = next.offset + next.byteSize();
|
||||
j++;
|
||||
}
|
||||
int totalSize = runEnd - runStart;
|
||||
xml.writeStartElement(QStringLiteral("Node"));
|
||||
// Use first hex node's name if it's a single node, otherwise generate
|
||||
QString hexName = (j - i == 1 && !child.name.isEmpty()) ? child.name : QString();
|
||||
xml.writeAttribute(QStringLiteral("Name"), hexName);
|
||||
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("21")); // Custom
|
||||
xml.writeAttribute(QStringLiteral("Size"), QString::number(totalSize));
|
||||
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||
xml.writeEndElement(); // Node
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
xml.writeStartElement(QStringLiteral("Node"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), child.name);
|
||||
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(child.kind)));
|
||||
xml.writeAttribute(QStringLiteral("Size"), QString::number(nodeSizeForExport(child)));
|
||||
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||
|
||||
// Pointer with target
|
||||
if ((child.kind == NodeKind::Pointer64 || child.kind == NodeKind::Pointer32) && child.refId != 0) {
|
||||
QString target = resolveStructName(tree, child.refId);
|
||||
if (!target.isEmpty())
|
||||
xml.writeAttribute(QStringLiteral("Pointer"), target);
|
||||
}
|
||||
|
||||
// Embedded struct instance
|
||||
if (child.kind == NodeKind::Struct) {
|
||||
QString instName = child.structTypeName.isEmpty() ? child.name : child.structTypeName;
|
||||
xml.writeAttribute(QStringLiteral("Instance"), instName);
|
||||
}
|
||||
|
||||
// Array: Total attribute and child <Array> element
|
||||
if (child.kind == NodeKind::Array) {
|
||||
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
|
||||
|
||||
// Resolve element type name
|
||||
QString elemName;
|
||||
if (child.elementKind == NodeKind::Struct && !child.structTypeName.isEmpty()) {
|
||||
elemName = child.structTypeName;
|
||||
} else if (child.refId != 0) {
|
||||
elemName = resolveStructName(tree, child.refId);
|
||||
}
|
||||
if (elemName.isEmpty())
|
||||
elemName = kindToString(child.elementKind);
|
||||
|
||||
xml.writeStartElement(QStringLiteral("Array"));
|
||||
xml.writeAttribute(QStringLiteral("Name"), elemName);
|
||||
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
|
||||
xml.writeEndElement(); // Array
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // Node
|
||||
i++;
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // Class
|
||||
classCount++;
|
||||
}
|
||||
|
||||
xml.writeEndElement(); // ReClass
|
||||
xml.writeEndDocument();
|
||||
file.close();
|
||||
|
||||
if (classCount == 0) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No struct classes found to export");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
10
src/imports/export_reclass_xml.h
Normal file
@@ -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
|
||||
1387
src/imports/import_pdb.cpp
Normal file
63
src/imports/import_pdb.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include <QVector>
|
||||
#include <functional>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── PDB Symbol Extraction ──
|
||||
|
||||
struct PdbSymbol {
|
||||
QString name;
|
||||
uint32_t rva;
|
||||
uint32_t typeIndex = 0; // TPI type index (0 = unknown / public symbol)
|
||||
};
|
||||
|
||||
struct PdbSymbolResult {
|
||||
QString moduleName; // derived from PDB filename (e.g. "ntoskrnl")
|
||||
QVector<PdbSymbol> symbols;
|
||||
};
|
||||
|
||||
// Extract public/global symbols (name → RVA) from a PDB file.
|
||||
// This reads the DBI stream's public and global symbol sub-streams.
|
||||
PdbSymbolResult extractPdbSymbols(const QString& pdbPath,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
// ── PDB Type Import ──
|
||||
|
||||
struct PdbTypeInfo {
|
||||
uint32_t typeIndex; // TPI type index
|
||||
QString name; // struct/class/union/enum name
|
||||
uint64_t size; // sizeof in bytes
|
||||
int childCount; // direct member count
|
||||
bool isUnion; // union vs struct/class
|
||||
bool isEnum = false; // enum type
|
||||
};
|
||||
|
||||
// Phase 1: Enumerate all UDT types in the PDB (fast scan, no recursive import).
|
||||
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
// Phase 2: Import selected types with full recursive child types.
|
||||
// progressCb is called with (current, total) for each top-level type;
|
||||
// return false from the callback to cancel the import.
|
||||
using ProgressCb = std::function<bool(int current, int total)>;
|
||||
NodeTree importPdbSelected(const QString& pdbPath,
|
||||
const QVector<uint32_t>& typeIndices,
|
||||
QString* errorMsg = nullptr,
|
||||
ProgressCb progressCb = {});
|
||||
|
||||
// Legacy single-call API: import one struct by name (or all if filter empty).
|
||||
NodeTree importPdb(const QString& pdbPath,
|
||||
const QString& structFilter = {},
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
// Import the type associated with a global symbol's typeIndex.
|
||||
// Opens the PDB, resolves the typeIndex to a UDT/enum, and returns the imported tree.
|
||||
// Returns empty tree if the symbol has no associated type or the type is a simple primitive.
|
||||
NodeTree importTypeForSymbol(const QString& pdbPath,
|
||||
uint32_t typeIndex,
|
||||
QString* typeName = nullptr,
|
||||
QString* errorMsg = nullptr);
|
||||
|
||||
} // namespace rcx
|
||||
184
src/imports/import_pdb_dialog.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#include "import_pdb_dialog.h"
|
||||
#include "import_pdb.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QCheckBox>
|
||||
#include <QListWidget>
|
||||
#include <QLabel>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QPushButton>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QApplication>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
PdbImportDialog::PdbImportDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle("Import from PDB");
|
||||
resize(520, 480);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
|
||||
// PDB path row
|
||||
auto* pathRow = new QHBoxLayout;
|
||||
pathRow->addWidget(new QLabel("PDB File:"));
|
||||
m_pathEdit = new QLineEdit;
|
||||
m_pathEdit->setPlaceholderText("Select a PDB file...");
|
||||
pathRow->addWidget(m_pathEdit);
|
||||
m_browseBtn = new QPushButton("...");
|
||||
m_browseBtn->setFixedWidth(32);
|
||||
pathRow->addWidget(m_browseBtn);
|
||||
layout->addLayout(pathRow);
|
||||
|
||||
// Filter row
|
||||
auto* filterRow = new QHBoxLayout;
|
||||
filterRow->addWidget(new QLabel("Filter:"));
|
||||
m_filterEdit = new QLineEdit;
|
||||
m_filterEdit->setPlaceholderText("Type name filter...");
|
||||
m_filterEdit->setEnabled(false);
|
||||
filterRow->addWidget(m_filterEdit);
|
||||
layout->addLayout(filterRow);
|
||||
|
||||
// Select all checkbox
|
||||
m_selectAll = new QCheckBox("Select All");
|
||||
m_selectAll->setEnabled(false);
|
||||
layout->addWidget(m_selectAll);
|
||||
|
||||
// Type list
|
||||
m_typeList = new QListWidget;
|
||||
m_typeList->setEnabled(false);
|
||||
layout->addWidget(m_typeList);
|
||||
|
||||
// Count label
|
||||
m_countLabel = new QLabel("No PDB loaded");
|
||||
layout->addWidget(m_countLabel);
|
||||
|
||||
// Buttons
|
||||
m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
m_buttons->button(QDialogButtonBox::Ok)->setText("Import");
|
||||
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
layout->addWidget(m_buttons);
|
||||
|
||||
connect(m_browseBtn, &QPushButton::clicked, this, &PdbImportDialog::browsePdb);
|
||||
connect(m_pathEdit, &QLineEdit::returnPressed, this, &PdbImportDialog::loadPdb);
|
||||
connect(m_filterEdit, &QLineEdit::textChanged, this, &PdbImportDialog::filterChanged);
|
||||
connect(m_selectAll, &QCheckBox::toggled, this, &PdbImportDialog::selectAllToggled);
|
||||
connect(m_typeList, &QListWidget::itemChanged, this, &PdbImportDialog::updateSelectionCount);
|
||||
connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
QString PdbImportDialog::pdbPath() const {
|
||||
return m_pathEdit->text();
|
||||
}
|
||||
|
||||
QVector<uint32_t> PdbImportDialog::selectedTypeIndices() const {
|
||||
QVector<uint32_t> result;
|
||||
for (int i = 0; i < m_typeList->count(); i++) {
|
||||
auto* item = m_typeList->item(i);
|
||||
if (item->checkState() == Qt::Checked) {
|
||||
uint32_t typeIndex = item->data(Qt::UserRole).toUInt();
|
||||
result.append(typeIndex);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PdbImportDialog::browsePdb() {
|
||||
QString path = QFileDialog::getOpenFileName(this,
|
||||
"Select PDB File", {},
|
||||
"PDB Files (*.pdb);;All Files (*)");
|
||||
if (path.isEmpty()) return;
|
||||
m_pathEdit->setText(path);
|
||||
loadPdb();
|
||||
}
|
||||
|
||||
void PdbImportDialog::loadPdb() {
|
||||
QString path = m_pathEdit->text();
|
||||
if (path.isEmpty()) return;
|
||||
|
||||
m_typeList->clear();
|
||||
m_allTypes.clear();
|
||||
m_countLabel->setText("Loading...");
|
||||
m_typeList->setEnabled(false);
|
||||
m_filterEdit->setEnabled(false);
|
||||
m_selectAll->setEnabled(false);
|
||||
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||
QApplication::processEvents();
|
||||
|
||||
QString error;
|
||||
QVector<PdbTypeInfo> types = enumeratePdbTypes(path, &error);
|
||||
|
||||
if (types.isEmpty()) {
|
||||
m_countLabel->setText(error.isEmpty() ? "No types found" : error);
|
||||
return;
|
||||
}
|
||||
|
||||
m_allTypes.reserve(types.size());
|
||||
for (const auto& t : types) {
|
||||
TypeItem item;
|
||||
item.typeIndex = t.typeIndex;
|
||||
item.name = t.name;
|
||||
item.childCount = t.childCount;
|
||||
item.isUnion = t.isUnion;
|
||||
m_allTypes.append(item);
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
std::sort(m_allTypes.begin(), m_allTypes.end(),
|
||||
[](const TypeItem& a, const TypeItem& b) { return a.name < b.name; });
|
||||
|
||||
m_filterEdit->setEnabled(true);
|
||||
m_selectAll->setEnabled(true);
|
||||
m_typeList->setEnabled(true);
|
||||
populateList();
|
||||
}
|
||||
|
||||
void PdbImportDialog::populateList() {
|
||||
m_typeList->blockSignals(true);
|
||||
m_typeList->clear();
|
||||
|
||||
QString filter = m_filterEdit->text();
|
||||
bool selectAll = m_selectAll->isChecked();
|
||||
|
||||
for (const auto& t : m_allTypes) {
|
||||
if (!filter.isEmpty() && !t.name.contains(filter, Qt::CaseInsensitive))
|
||||
continue;
|
||||
|
||||
QString label = QStringLiteral("%1 (%2 fields)")
|
||||
.arg(t.name).arg(t.childCount);
|
||||
auto* item = new QListWidgetItem(label, m_typeList);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(selectAll ? Qt::Checked : Qt::Unchecked);
|
||||
item->setData(Qt::UserRole, t.typeIndex);
|
||||
}
|
||||
|
||||
m_typeList->blockSignals(false);
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
void PdbImportDialog::filterChanged(const QString&) {
|
||||
populateList();
|
||||
}
|
||||
|
||||
void PdbImportDialog::selectAllToggled(bool) {
|
||||
populateList();
|
||||
}
|
||||
|
||||
void PdbImportDialog::updateSelectionCount() {
|
||||
int checked = 0;
|
||||
int total = m_typeList->count();
|
||||
for (int i = 0; i < total; i++) {
|
||||
if (m_typeList->item(i)->checkState() == Qt::Checked)
|
||||
checked++;
|
||||
}
|
||||
m_countLabel->setText(QStringLiteral("%1 of %2 types selected")
|
||||
.arg(checked).arg(m_allTypes.size()));
|
||||
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(checked > 0);
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
53
src/imports/import_pdb_dialog.h
Normal file
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QVector>
|
||||
#include <cstdint>
|
||||
|
||||
class QLineEdit;
|
||||
class QCheckBox;
|
||||
class QListWidget;
|
||||
class QLabel;
|
||||
class QDialogButtonBox;
|
||||
class QPushButton;
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct PdbTypeInfo;
|
||||
|
||||
class PdbImportDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PdbImportDialog(QWidget* parent = nullptr);
|
||||
|
||||
QString pdbPath() const;
|
||||
QVector<uint32_t> selectedTypeIndices() const;
|
||||
|
||||
private slots:
|
||||
void browsePdb();
|
||||
void loadPdb();
|
||||
void filterChanged(const QString& text);
|
||||
void selectAllToggled(bool checked);
|
||||
void updateSelectionCount();
|
||||
|
||||
private:
|
||||
QLineEdit* m_pathEdit;
|
||||
QPushButton* m_browseBtn;
|
||||
QLineEdit* m_filterEdit;
|
||||
QCheckBox* m_selectAll;
|
||||
QListWidget* m_typeList;
|
||||
QLabel* m_countLabel;
|
||||
QDialogButtonBox* m_buttons;
|
||||
|
||||
struct TypeItem {
|
||||
uint32_t typeIndex;
|
||||
QString name;
|
||||
int childCount;
|
||||
bool isUnion;
|
||||
};
|
||||
QVector<TypeItem> m_allTypes;
|
||||
|
||||
void populateList();
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
392
src/imports/import_reclass_xml.cpp
Normal file
@@ -0,0 +1,392 @@
|
||||
#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, int ptrSize = 8) {
|
||||
NodeKind k = NodeKind::Hex8;
|
||||
if (ver == XmlVersion::V2016) {
|
||||
for (const auto& e : kTypeMap2016)
|
||||
if (e.xmlType == xmlType) { k = e.kind; break; }
|
||||
} else {
|
||||
for (const auto& e : kTypeMap2013)
|
||||
if (e.xmlType == xmlType) { k = e.kind; break; }
|
||||
}
|
||||
// Remap pointer types for 32-bit targets
|
||||
if (ptrSize < 8 && k == NodeKind::Pointer64)
|
||||
k = NodeKind::Pointer32;
|
||||
return k;
|
||||
}
|
||||
|
||||
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
|
||||
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, int pointerSize) {
|
||||
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;
|
||||
tree.pointerSize = pointerSize;
|
||||
|
||||
// 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, pointerSize);
|
||||
|
||||
// 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.push_back(PendingRef{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.push_back(PendingRef{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.push_back(PendingRef{nodeId, n.structTypeName});
|
||||
} else {
|
||||
tree.addNode(n);
|
||||
}
|
||||
childOffset += nodeSize > 0 ? nodeSize : 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
tree.addNode(n);
|
||||
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (xml.hasError() && xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
|
||||
qDebug() << "[ImportXML] XML parse error at line" << xml.lineNumber() << ":" << xml.errorString();
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("XML parse error at line %1: %2")
|
||||
.arg(xml.lineNumber())
|
||||
.arg(xml.errorString());
|
||||
return {};
|
||||
}
|
||||
|
||||
qDebug() << "[ImportXML] Parsing complete. Total nodes:" << tree.nodes.size()
|
||||
<< "classes:" << classIds.size() << "pending refs:" << pendingRefs.size();
|
||||
|
||||
if (tree.nodes.isEmpty()) {
|
||||
qDebug() << "[ImportXML] ERROR: No classes found";
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No classes found in file");
|
||||
return {};
|
||||
}
|
||||
|
||||
// Resolve deferred pointer/struct references
|
||||
int resolved = 0, unresolved = 0;
|
||||
for (const auto& ref : pendingRefs) {
|
||||
int nodeIdx = tree.indexOfId(ref.nodeId);
|
||||
if (nodeIdx < 0) continue;
|
||||
|
||||
auto it = classIds.find(ref.className);
|
||||
if (it != classIds.end()) {
|
||||
tree.nodes[nodeIdx].refId = it.value();
|
||||
resolved++;
|
||||
} else {
|
||||
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
|
||||
unresolved++;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "[ImportXML] Refs resolved:" << resolved << "unresolved:" << unresolved;
|
||||
qDebug() << "[ImportXML] Import complete. Returning tree with" << tree.nodes.size() << "nodes";
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
13
src/imports/import_reclass_xml.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#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.
|
||||
// pointerSize: 4 for 32-bit targets, 8 for 64-bit (default).
|
||||
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr,
|
||||
int pointerSize = 8);
|
||||
|
||||
} // namespace rcx
|
||||