Compare commits

..

66 Commits

Author SHA1 Message Date
IChooseYou
48409d1d38 fix: guard __cdecl __debugbreak behind PDB_COMPILER_MSVC for Linux build
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 07:33:36 -07:00
IChooseYou
df1435d9b7 Merge remote-tracking branch 'origin/refactor-readme' 2026-02-22 07:30:00 -07:00
IChooseYou
5e11ff5496 feat: Remote Process Memory plugin, source menu icons, base address fix
- Remote Process Memory plugin: shared-memory IPC payload injected into
  target process (CreateRemoteThread on Win, ptrace+dlopen on Linux),
  VirtualQuery-based memory safety, PEB-based image base, batch reads
- Source dropdown: SVG icons per provider type, DLL filename shown
- Fix base address not updating when switching to a new source provider
- ProviderRegistry carries DLL filename from PluginManager

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 07:29:56 -07:00
Sen66
22842d9801 I'm too tired 2026-02-22 14:22:36 +01:00
Sen66
50acde60cb refactor readme 2026-02-22 14:21:12 +01:00
IChooseYou
1d7d384b93 feat: PDB import via RawPDB, no msdia140.dll dependency
Replace DIA SDK COM-based PDB importer with RawPDB (MolecularMatters)
which reads PDB files directly via memory-mapped I/O. Adds File menu
"Import PDB..." dialog with type filtering, selection, and progress.

- Vendor raw_pdb into third_party/
- Two-phase API: enumeratePdbTypes() + importPdbSelected()
- Full recursive import of structs/unions/arrays/pointers/bitfields
- PDB import dialog with name filter, select-all, type count
- Benchmark: 1654 types from ntkrnlmp.pdb in 16ms
- Reorganize import/export files into src/imports/
2026-02-21 17:18:24 -07:00
IChooseYou
3a76b03c85 fix: continuous top border on status bar tabs, baseline alignment, 15% taller
- ViewTabButton always paints 1px top border matching status bar hairline;
  selected tab's accent line paints over it
- Remove SegmentedContainer (caused gap on unselected tab)
- Shared baseline alignment between tab text and status label
- Status bar height * 1.15
2026-02-21 11:41:46 -07:00
IChooseYou
ac94855d6c feat: status bar visual upgrade, unified release job
Status bar: top hairline separator, vertical divider between toggle
and status text, segmented-control container with border/separators
around view buttons, accent line 2->3px, proper sizeHint with
breathing room, default system font instead of monospace override.

CI: replace per-job release uploads with a single release job that
waits for both windows and linux, then publishes both artifacts to
one GitHub release.
2026-02-21 11:09:28 -07:00
IChooseYou
d65b6c5a29 feat: address expression parser with module resolution and pointer deref
Merge branch 'address-parser'. Adds AddressParser supporting:
- Hex arithmetic with +-*/ and operator precedence
- Module base resolution via <Module.exe> syntax
- Pointer dereference via [addr] syntax with nesting
- WinDbg backtick-separated addresses (7ff6`6cce0000)
- Formula persistence in project files and source switching
2026-02-21 09:12:11 -07:00
IChooseYou
d45ee9e4c9 ci: install Qt-matching MinGW 13.1.0 to fix test segfaults
System MinGW on windows-latest is GCC 15.2 which has ABI mismatch
with Qt 6.8.1 (built with MinGW 13.1.0), causing all tests to
segfault. Install the matching toolchain via aqtinstall tools and
use it instead of the system compiler.
2026-02-21 09:07:09 -07:00
Sen66
31115014a5 ignore some more build directories 2026-02-21 17:04:59 +01:00
Sen66
8e88d588be Add AddressParser + tests, remove symbol from commandrow 2026-02-21 17:03:44 +01:00
IChooseYou
b089e20d36 ci: retrigger build 2026-02-20 17:01:57 -07:00
IChooseYou
5fa1dd0ab4 fix: add missing header declarations and editor scroll fixes
- mainwindow.h: add m_viewBtnGroup, m_btnReclass, m_btnRendered members,
  syncViewButtons() declaration, QButtonGroup/QPushButton includes,
  remove applyTabWidgetStyle() declaration
- editor.cpp: reset xOffset on applyDocument, clamp in restoreViewState
- test_editor.cpp: add horizontal scroll reset test
2026-02-20 13:22:23 -07:00
IChooseYou
3b1fe7ff35 fix: use findChild<QWidget*> for ResizeGrip to fix GCC 15 static_assert
GCC 15.2 on CI enforces Q_OBJECT requirement for findChild template
parameter. ResizeGrip is a local class without Q_OBJECT, so use
QWidget* with static_cast instead.
2026-02-20 13:18:03 -07:00
IChooseYou
4595b366e3 ci: use system MinGW from runner, drop tools_mingw1310 2026-02-20 13:05:13 -07:00
IChooseYou
33d7dc74cb ci: switch Windows CI from MSVC to MinGW, run Linux in parallel 2026-02-20 12:57:51 -07:00
IChooseYou
e118231bb1 docs: add screenshots to README 2026-02-20 12:32:04 -07:00
IChooseYou
0cfd7ad87a feat: sort primitives alphabetically in type chooser 2026-02-20 07:37:32 -07:00
IChooseYou
2d3ce63b54 ci: disable UI tests in CI, delete test_com_security
CI now passes -DBUILD_UI_TESTS=OFF so only headless tests
(core, format, compose, provider, command_row, generator,
import_xml, import_source, export_xml, disasm) build and run.

Removed xvfb-run and exclude-regex hacks from both Windows
and Linux CI — the CMake option handles it cleanly.

Deleted test_com_security (windbg-only, not needed in CI).
2026-02-20 07:27:23 -07:00
IChooseYou
0e087fa3a4 feat: primitive pointer modifiers, type chooser fixes, double-click to edit
Type chooser:
- Fix PointerTarget mode hiding primitives due to stale modifier state
- Preselect */[n] modifier buttons to reflect current node type
- Primitive pointer support: int32*, double**, etc with provider deref
- hex64*/ptr64* with * modifier falls back to void* (meaningless deref)
- isValidPrimitivePtrTarget guard in controller, compose, format
- Modifier toggle no longer resets list selection
- Primitive pointers open FieldType mode (not PointerTarget)
- Type edit requires double-click (was single-click, too easy to misclick)

Other:
- Custom dock titlebar with themed close button, no float button
- Status bar font synced at startup
- Resize grip reworked as direct MainWindow child, font-independent
- File menu "Source" renamed to "Current Tab Source"

Tests: 41 type_selector, 39 editor, 17 controller (200 total, 0 failures)
2026-02-20 07:21:02 -07:00
IChooseYou
c7afe363f3 feat: custom dock titlebar, resize grip symmetry fix, status bar font sync
- Replace default dock widget titlebar with custom label + themed ✕ close button
- Remove float/popout button from project tree dock
- Fix resize grip corner symmetry (bottom margin 4→0)
- Sync editor font to status bar and dock titlebar at startup
- Add testResizeGripCornerSymmetry test
2026-02-19 18:10:52 -07:00
IChooseYou
2a44d2ac57 fix: narrow inline editor selection for pointer values, resolve correct write address
resolvedSpanFor() now applies narrowPtrValueSpan() to trim the "// Module+offset"
symbol comment from the editable span, matching hitTestTarget(). Previously the
full value column text was selected, making the parser fail on commit (toULongLong
rejected the non-hex suffix), so pointer value saves were silently no-ops.

With the parse now succeeding, a second bug was exposed: setNodeValue() computed
write addresses via computeOffset() which sums tree offsets without dereferencing
pointers. For nodes inside expanded pointer targets (e.g. VTable entries), this
wrote to struct_base+child_offset instead of *ptr_value+child_offset, causing an
access violation crash. The fix passes lm->offsetAddr (the compose-resolved
absolute address) through the inlineEditCommitted signal so setNodeValue() uses
the correct dereferenced address.
2026-02-19 13:05:25 -07:00
IChooseYou
d989e2a947 feat: safe workspace tree deletion with reference cleanup and confirmation
- Add deleteRootStruct() that clears orphaned refId references before removal
- Show confirmation dialog listing all fields that reference the deleted type
- Auto-switch view to next root struct when the viewed one is deleted
- Entire operation is a single undo macro (Ctrl+Z restores everything)
2026-02-19 10:06:13 -07:00
IChooseYou
7678da033d feat: source management, cross-tab type visibility, default VS2022 theme
- Add clearSources() and File→Source submenu for provider management
- Fix type picker not showing newly created structs (empty structTypeName)
- Add cross-tab type visibility via shared project document list
- Import external types into local document on selection
- Default theme to VS2022 on first launch
- Add test_source_management and test_type_visibility test suites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:29:18 -07:00
IChooseYou
acc3ebf5db feat: track value changes toggle, hover scroll fix, ptr* convert, hex split 2026-02-19 06:32:58 -07:00
IChooseYou
26217f5de8 feat: switch provider addressing from RVA to absolute, add pointer expansion tests 2026-02-18 13:07:48 -07:00
IChooseYou
fa0d9a377b fix: type chooser updates colors when theme changes
Add applyTheme() to TypeSelectorPopup that refreshes palette and
stylesheets for all child widgets. Controller connects it to
ThemeManager::themeChanged on popup creation.
2026-02-18 09:59:50 -07:00
IChooseYou
b1d3e52204 fix: type chooser SVG icons and gutter scale with editor zoom level
Derive icon size, gutter width, and icon column width from font
metrics instead of hardcoded 16/10/20 pixel values. Popup width
calculation also scales with font.
2026-02-18 09:47:25 -07:00
IChooseYou
1cccd320b0 feat: simplify cmd bar keyword, add File menu class/struct/enum, remove Align Members
- Command bar shows static keyword (struct/class/enum) without dropdown or colon
- Right-click keyword in cmd bar for class↔struct conversion (enum blocked)
- File menu: New Class (Ctrl+N), New Struct (Ctrl+T), New Enum (Ctrl+E)
- Project explorer right-click: New Class/Struct/Enum on Project node
- Explorer right-click: Convert to Class/Struct on class/struct items
- Remove Align Members submenu, performRealignment, computeStructAlignment
- Remove screenshot code and screenshot.png

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:38:54 -07:00
IChooseYou
5b6e0473cb remove screenshot from README 2026-02-18 08:22:56 -07:00
IChooseYou
57d55456a8 fix: type chooser [n] array modifier now works for primitive types
Array count defaults to 1 when clicking the [n] toggle.
2026-02-18 08:16:02 -07:00
IChooseYou
bb466516ba fix: context menu shows full node menu when right-clicking past line text
Also add Mid theme and remove flaky test_theme test.
2026-02-18 07:57:49 -07:00
IChooseYou
444ba34fa3 feat: disasm popup, symbol separation, context menu improvements, RVA fixes
- Add Fadec x86 disassembler with hover popup for FuncPtr/void Pointer nodes
- Separate pointer symbol from address: // prefix, green comment coloring,
  independent hover/click zones (address triggers popup, symbol is passive)
- Fix RVA margin and inline local offset for pointer-expanded vtable children
  using ptrBase field threaded through composition
- Expand multi-select context menu with quick-convert, duplicate, copy address
- Remove Edit Value from hex node context menu
- Fix heatmap flickering on hex nodes (remove per-byte alternation)
- Fix popup repositioning when moving mouse between lines
- Truncate disasm popup to 6 lines with ... indicator
- Add BUILD_UI_TESTS option to skip widget tests on headless CI
- Add test_disasm with 35 test cases for disassembly and hex dump
- Add KUSER_SHARED_DATA example .rcx file
2026-02-18 07:10:13 -07:00
ichooseyou
91633169a0 fix: guard Windows-only selfTest code for Linux build
- Wrap DWORD/GetCurrentProcessId and KUSER_SHARED_DATA example behind Q_OS_WIN
- Linux selfTest falls back to project_new() with basic hex class
2026-02-17 12:32:44 -07:00
ichooseyou
f041761b62 feat: add FuncPtr32/FuncPtr64 node kinds, darken menu hover, remove refresh log
- Add FuncPtr32/FuncPtr64 types with display, parsing, validation, code generation
- Add quick-convert context menu actions between pointer and funcptr types
- Darken QMenu hover highlight from theme.border to theme.hover for better contrast
- Remove noisy [Refresh] reading debug log from console output
2026-02-17 12:29:51 -07:00
IChooseYou
1c3b4af045 feat: fix heatmap false-heat on offset shift, hover flicker, type chooser cleanup
- Clear value history when node offsets change (insert/delete/resize/
  manual offset edit) so stale values from old addresses don't show
  false heat coloring
- Invalidate in-flight async reads (bump refreshGen) when tree layout
  changes, preventing stale snapshot data from re-introducing heat
- Fix command bar hover cursor flicker: remove premature
  applyHoverCursor() from applyDocument() — runs correctly via
  applySelectionOverlays() after text is finalized
- Fix hover indicator survival: reorder refresh() so text-modifying
  passes (updateCommandRow) run before overlay passes
- Guard synthetic Leave events during setText() to preserve hover state
- Remove primitives from type chooser when pointer modifier (* / **)
  is active; remove primitives entirely in Root command bar mode
- Add test_editor and test_controller test coverage for heat clearing,
  hover survival, and mixed hex/non-hex type scenarios
2026-02-17 11:41:46 -07:00
IChooseYou
5ae9ca0979 feat: value history heatmap, write-fail guard, crash handler hardening
- Value history ring buffer (10 slots) tracks per-node change frequency
- Three-level heatmap: cold (blue), warm (amber), hot (red) via theme
- Heat persists indefinitely (no fade) — shows analysis history
- Calltip on hover shows previous values list
- Old themes auto-derive heat colors from existing palette
- Write failures no longer apply optimistic visual updates
- Crash handler: re-entrancy guard, context dump before risky APIs
2026-02-16 16:44:46 -07:00
Sen66
e064646c02 Added Reclass.NET plugin compatibility layer 2026-02-17 00:18:30 +01:00
IChooseYou
c6c56ffaee feat: default offset margin to relative (+0x) mode 2026-02-16 14:27:41 -07:00
IChooseYou
aba8e5cac9 feat: add Export ReClass XML and remove local-path tests
Adds Export ReClass XML menu item that writes NodeTree to ReClass .NET
compatible XML format with full round-trip fidelity. Removes test cases
that referenced local machine file paths.
2026-02-16 14:16:19 -07:00
IChooseYou
3a5d03fae0 feat: add Import from Source parser for C/C++ struct definitions
Adds a new "Import from Source..." menu item that opens a QScintilla
editor dialog where users can paste C/C++ struct definitions. The parser
tokenizes and parses the source using recursive descent, supporting
stdint.h types, Windows types (BYTE/DWORD/PVOID/etc), multi-word C
types, pointers, arrays, Vec2/3/4/Mat4x4 detection, unions (first
member), padding fields, typedefs, forward declarations, static_assert
size checks, and auto-detection of comment offset mode vs computed
offsets. Also removes the flaky test_editor cursor shape tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 14:08:12 -07:00
IChooseYou
df79da54e3 ci: remove windows-qt5 job entirely 2026-02-16 12:35:21 -07:00
IChooseYou
e3ff4dfe71 ci: snapshot releases with date tags and platform-specific names
- Tag: snapshot-DD-MM-YYYY instead of latest
- Assets: Reclass-win64-qt6.zip, Reclass-linux64-qt6.AppImage, Reclass-win64-qt5.zip
- Qt5 job now produces release artifacts
- Jobs serialized: windows → linux → windows-qt5
2026-02-16 11:34:14 -07:00
IChooseYou
735e4ea9f7 fix: exclude Qt5-incompatible tests from windows-qt5 CI job 2026-02-16 10:51:35 -07:00
IChooseYou
d937d2f42e fix: Qt5 compat - fix ambiguous QByteRef comparison in test_windbg_provider 2026-02-16 10:37:49 -07:00
IChooseYou
3685530287 fix: Qt5 compat - use toInt() instead of toInteger() for QJsonValue 2026-02-16 10:23:55 -07:00
IChooseYou
9e90f66ca0 fix: Qt5 compat - use pos() instead of position() for QMouseEvent 2026-02-16 09:12:17 -07:00
IChooseYou
f53fa84a15 fix: Qt5 compat - fix addAction wrapper, qHash for NodeKind, add windows-qt5 CI 2026-02-16 09:06:10 -07:00
IChooseYou
13e28e8791 Merge remote-tracking branch 'origin/qt5-compat' 2026-02-16 09:04:35 -07:00
IChooseYou
079b3121ce Revert "add Qt5 compatibility wrapper for addAction and linux-qt5 CI job"
This reverts commit 5e40349768.
2026-02-16 09:04:28 -07:00
IChooseYou
5e40349768 add Qt5 compatibility wrapper for addAction and linux-qt5 CI job 2026-02-16 08:58:09 -07:00
Sen66
8dd6110ec6 Try to fix Qt5 compat + no Qt6 deprec warning 2026-02-16 16:27:28 +01:00
IChooseYou
eb27fc7988 rename Close to Unload Project in File menu, remove icon 2026-02-15 14:39:14 -07:00
IChooseYou
85994d68b9 remove Node menu from menubar, actions available via editor right-click 2026-02-15 14:36:22 -07:00
IChooseYou
55dc5d5875 CI: serialize linux after windows to avoid release tag race condition 2026-02-15 14:29:01 -07:00
IChooseYou
3a92336132 fix: only package plugin DLLs/SOs, not build artifacts 2026-02-15 14:18:09 -07:00
IChooseYou
f9b33f2ba7 fix: options dialog test segfault from dangling ref to themes() temporary 2026-02-15 13:53:59 -07:00
IChooseYou
f2dab07870 fix: build ProcessMemory plugin on Linux, include Plugins in AppImage 2026-02-15 13:45:36 -07:00
IChooseYou
9d22a5ed69 fix: Options dialog - remove CSS overrides, fix title case, add show icon checkbox, add Generator page 2026-02-15 13:41:13 -07:00
IChooseYou
193ab81ecf docs: remove stale status notes from README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:12:36 -07:00
IChooseYou
aa0840b332 CI: consolidate win64+linux64 into single latest release, no pre-release
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:10:22 -07:00
IChooseYou
f3631f17ff fix: skip mv when AppImage already has correct name 2026-02-15 12:59:32 -07:00
IChooseYou
42e9bde7ba fix: find qmake via PATH/fallback for linuxdeploy Qt plugin 2026-02-15 12:52:25 -07:00
IChooseYou
07fedf0ae8 fix: derive QMAKE path from Qt6_DIR for linuxdeploy plugin 2026-02-15 12:44:42 -07:00
IChooseYou
2e02a01495 feat: project tree delete, close tab, Linux AppImage bundling
- Right-click delete on classes in Project Tree dock
- File > Close (Ctrl+W) to unload active project tab
- File > Open now replaces current project instead of merging
- Linux CI builds AppImage via linuxdeploy + Qt plugin so users
  don't need Qt installed (fixes libQt6Core.so.6 not found)
- Pin ubuntu-22.04 for broader glibc compatibility
2026-02-15 12:37:56 -07:00
200 changed files with 43226 additions and 1772 deletions

View File

@@ -18,46 +18,35 @@ jobs:
with:
submodules: recursive
- name: Install Qt6
- name: Install Qt6 and MinGW
uses: jurplel/install-qt-action@v4
with:
version: '6.8.1'
arch: 'win64_msvc2022_64'
arch: 'win64_mingw'
tools: 'tools_mingw1310,qt.tools.win64_mingw1310'
cache: true
aqtversion: '==3.1.21'
- uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Configure
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
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
run: cmake --build build
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
cmake --build build
- name: Test
run: ctest --test-dir build --output-on-failure --exclude-regex "test_editor|test_windbg_provider|test_com_security"
- name: Upload artifact
uses: actions/upload-artifact@v4
if: always()
with:
name: Reclass-Windows-x64
path: |
build/Reclass.exe
build/ReclassMcpBridge.exe
build/Plugins/
build/*.dll
build/platforms/
build/styles/
build/imageformats/
build/iconengines/
build/themes/
build/screenshot.png
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
ctest --test-dir build --output-on-failure
- name: Package release zip
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
shell: bash
run: |
mkdir -p release
@@ -68,28 +57,21 @@ jobs:
cp -r build/styles release/ 2>/dev/null || true
cp -r build/imageformats release/ 2>/dev/null || true
cp -r build/iconengines release/ 2>/dev/null || true
cp -r build/Plugins release/ 2>/dev/null || true
mkdir -p release/Plugins
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
cp -r build/themes release/ 2>/dev/null || true
cp -r build/examples release/ 2>/dev/null || true
cp build/screenshot.png release/ 2>/dev/null || true
cd release && 7z a ../win64-reclass-latest.zip *
cd release && 7z a ../Reclass-win64-qt6.zip *
- name: Update win64 release
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: softprops/action-gh-release@v2
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
tag_name: latest-win64
name: win64
body: |
Windows x64 build from main branch.
Commit: ${{ github.sha }}
prerelease: true
files: win64-reclass-latest.zip
make_latest: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: Reclass-win64-qt6
path: Reclass-win64-qt6.zip
linux:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
@@ -101,53 +83,87 @@ jobs:
with:
version: '6.8.1'
cache: true
aqtversion: '==3.1.21'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y ninja-build libgl1-mesa-dev
sudo apt-get install -y ninja-build libgl1-mesa-dev libfuse2 libxcb-cursor0
- name: Configure
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF
- name: Build
run: cmake --build build
- name: Test
run: xvfb-run ctest --test-dir build --output-on-failure --exclude-regex "test_editor"
env:
QT_QPA_PLATFORM: offscreen
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
if: always()
with:
name: Reclass-Linux-x64
path: |
build/Reclass
build/ReclassMcpBridge
name: Reclass-linux64-qt6
path: Reclass-linux64-qt6.AppImage
- name: Package release tarball
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
mkdir -p release
cp build/Reclass release/
cp build/ReclassMcpBridge release/
cp -r build/themes release/ 2>/dev/null || true
tar czf linux64-reclass-latest.tar.gz -C release .
release:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [windows, linux]
runs-on: ubuntu-latest
- name: Update linux64 release
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
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: latest-linux64
name: linux64
tag_name: snapshot-${{ steps.date.outputs.tag }}
name: Snapshot ${{ steps.date.outputs.tag }}
body: |
Linux x64 build from main branch.
Automated snapshot from main branch.
Commit: ${{ github.sha }}
prerelease: true
files: linux64-reclass-latest.tar.gz
make_latest: false
prerelease: false
files: |
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
artifacts/Reclass-linux64-qt6/Reclass-linux64-qt6.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@@ -11,3 +11,6 @@ build/
*.suo
.vs/
CMakeUserPresets.json
plugins/RcNetPluginCompatLayer/bridge/obj
plugins/RcNetPluginCompatLayer/bridge/bin
.cache

View File

@@ -1,8 +1,9 @@
cmake_minimum_required(VERSION 3.20)
project(Reclass VERSION 0.1 LANGUAGES CXX)
project(Reclass VERSION 0.1 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 11)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
@@ -30,6 +31,15 @@ endif()
find_package(QScintilla REQUIRED)
# RawPDB — direct PDB file reader (no DIA SDK / msdia140.dll dependency)
file(GLOB RAW_PDB_SRCS third_party/raw_pdb/src/*.cpp)
add_library(raw_pdb STATIC ${RAW_PDB_SRCS})
target_include_directories(raw_pdb PUBLIC third_party/raw_pdb/src)
target_compile_features(raw_pdb PRIVATE cxx_std_11)
if(WIN32)
target_link_libraries(raw_pdb PRIVATE rpcrt4)
endif()
add_executable(Reclass
src/main.cpp
src/editor.h
@@ -59,6 +69,16 @@ add_executable(Reclass
src/themes/thememanager.cpp
src/themes/themeeditor.h
src/themes/themeeditor.cpp
src/imports/import_reclass_xml.h
src/imports/import_reclass_xml.cpp
src/imports/import_source.h
src/imports/import_source.cpp
src/imports/export_reclass_xml.h
src/imports/export_reclass_xml.cpp
src/imports/import_pdb.h
src/imports/import_pdb.cpp
src/imports/import_pdb_dialog.h
src/imports/import_pdb_dialog.cpp
src/mainwindow.h
src/optionsdialog.h
src/optionsdialog.cpp
@@ -66,10 +86,16 @@ add_executable(Reclass
src/titlebar.cpp
src/mcp/mcp_bridge.h
src/mcp/mcp_bridge.cpp
src/addressparser.h
src/addressparser.cpp
src/disasm.h
src/disasm.cpp
third_party/fadec/decode.c
third_party/fadec/format.c
$<$<PLATFORM_ID:Windows>:src/app.rc>
)
target_include_directories(Reclass PRIVATE src)
target_include_directories(Reclass PRIVATE src third_party/fadec)
target_link_libraries(Reclass PRIVATE
${QT}::Widgets
@@ -81,7 +107,7 @@ target_link_libraries(Reclass PRIVATE
${_QT_WINEXTRAS}
)
if(WIN32)
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi)
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi raw_pdb)
endif()
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
@@ -95,16 +121,16 @@ foreach(_tf ${_theme_files})
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
endforeach()
# Copy example .rcx files to build directory
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
foreach(_ef ${_example_files})
get_filename_component(_name ${_ef} NAME)
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
endforeach()
include(deploy)
if(TARGET deploy)
add_custom_target(screenshot ALL
COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
DEPENDS Reclass deploy
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Capturing UI screenshot with class open..."
)
endif()
set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
file(WRITE ${_combine_script} "
@@ -121,7 +147,7 @@ foreach(_f
\"${CMAKE_SOURCE_DIR}/src/generator.cpp\"
\"${CMAKE_SOURCE_DIR}/src/main.cpp\")
file(READ \${_f} _content)
file(APPEND \${_out} \${_content})
file(APPEND \${_out} \"\${_content}\")
file(APPEND \${_out} \"\\n\")
endforeach()
message(STATUS \"Combined sources -> \${_out}\")
@@ -138,29 +164,26 @@ if(BUILD_TESTING)
find_package(${QT} REQUIRED COMPONENTS Test)
enable_testing()
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp)
# Disasm/Fadec sources needed by any test that links editor.cpp
set(DISASM_SRCS src/disasm.cpp third_party/fadec/decode.c third_party/fadec/format.c)
# ── Headless tests (Qt::Core only — safe for CI without a display) ──
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_core PRIVATE src)
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_core COMMAND test_core)
add_executable(test_format tests/test_format.cpp src/format.cpp)
add_executable(test_format tests/test_format.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_format PRIVATE src)
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_format COMMAND test_format)
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp)
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_compose PRIVATE src)
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_compose COMMAND test_compose)
add_executable(test_editor tests/test_editor.cpp src/editor.cpp src/compose.cpp src/format.cpp src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_editor PRIVATE src)
target_link_libraries(test_editor PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_editor COMMAND test_editor)
add_executable(test_provider tests/test_provider.cpp)
target_include_directories(test_provider PRIVATE src)
target_link_libraries(test_provider PRIVATE ${QT}::Core ${QT}::Test)
@@ -171,12 +194,68 @@ if(BUILD_TESTING)
target_link_libraries(test_command_row PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_command_row COMMAND test_command_row)
add_executable(test_generator tests/test_generator.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_generator PRIVATE src)
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_generator COMMAND test_generator)
add_executable(test_import_xml tests/test_import_xml.cpp
src/imports/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_xml PRIVATE src)
target_link_libraries(test_import_xml PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_import_xml COMMAND test_import_xml)
add_executable(test_import_source tests/test_import_source.cpp
src/imports/import_source.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_source PRIVATE src)
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_import_source COMMAND test_import_source)
add_executable(test_export_xml tests/test_export_xml.cpp
src/imports/export_reclass_xml.cpp src/imports/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_export_xml PRIVATE src)
target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_export_xml COMMAND test_export_xml)
add_executable(test_disasm tests/test_disasm.cpp
src/disasm.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
third_party/fadec/decode.c third_party/fadec/format.c)
target_include_directories(test_disasm PRIVATE src third_party/fadec)
target_link_libraries(test_disasm PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_disasm COMMAND test_disasm)
add_executable(test_addressparser tests/test_addressparser.cpp src/addressparser.cpp)
target_include_directories(test_addressparser PRIVATE src)
target_link_libraries(test_addressparser PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_addressparser COMMAND test_addressparser)
if(WIN32)
add_executable(test_import_pdb tests/test_import_pdb.cpp
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_pdb PRIVATE src)
target_link_libraries(test_import_pdb PRIVATE
${QT}::Core ${QT}::Test raw_pdb)
add_test(NAME test_import_pdb COMMAND test_import_pdb)
add_executable(bench_import_pdb tests/bench_import_pdb.cpp
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(bench_import_pdb PRIVATE src)
target_link_libraries(bench_import_pdb PRIVATE
${QT}::Core ${QT}::Test raw_pdb)
add_test(NAME bench_import_pdb COMMAND bench_import_pdb)
endif()
# ── UI tests (require Qt::Widgets / QScintilla / display — skip on headless CI) ──
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
if(BUILD_UI_TESTS)
add_executable(test_controller tests/test_controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_controller PRIVATE src)
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_controller PRIVATE src third_party/fadec)
target_link_libraries(test_controller PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
@@ -186,11 +265,11 @@ if(BUILD_TESTING)
add_test(NAME test_controller COMMAND test_controller)
add_executable(test_validation tests/test_validation.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_validation PRIVATE src)
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_validation PRIVATE src third_party/fadec)
target_link_libraries(test_validation PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
@@ -199,18 +278,12 @@ if(BUILD_TESTING)
endif()
add_test(NAME test_validation COMMAND test_validation)
add_executable(test_generator tests/test_generator.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
target_include_directories(test_generator PRIVATE src)
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_generator COMMAND test_generator)
add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_context_menu PRIVATE src)
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
target_link_libraries(test_context_menu PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
@@ -219,8 +292,32 @@ if(BUILD_TESTING)
endif()
add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_source_management tests/test_source_management.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_source_management PRIVATE src third_party/fadec)
target_link_libraries(test_source_management PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_source_management PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_source_management COMMAND test_source_management)
add_executable(test_editor tests/test_editor.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_editor PRIVATE src third_party/fadec)
target_link_libraries(test_editor PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_editor COMMAND test_editor)
add_executable(test_rendered_view tests/test_rendered_view.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_rendered_view PRIVATE src)
target_link_libraries(test_rendered_view PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
@@ -228,11 +325,11 @@ if(BUILD_TESTING)
add_test(NAME test_rendered_view COMMAND test_rendered_view)
add_executable(test_new_features tests/test_new_features.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_new_features PRIVATE src)
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_new_features PRIVATE src third_party/fadec)
target_link_libraries(test_new_features PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
@@ -242,11 +339,11 @@ if(BUILD_TESTING)
add_test(NAME test_new_features COMMAND test_new_features)
add_executable(test_type_selector tests/test_type_selector.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_type_selector PRIVATE src)
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
target_link_libraries(test_type_selector PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
@@ -255,11 +352,25 @@ if(BUILD_TESTING)
endif()
add_test(NAME test_type_selector COMMAND test_type_selector)
add_executable(test_theme tests/test_theme.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_theme PRIVATE src)
target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test)
add_test(NAME test_theme COMMAND test_theme)
add_executable(test_type_visibility tests/test_type_visibility.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_type_visibility PRIVATE src third_party/fadec)
target_link_libraries(test_type_visibility PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_type_visibility COMMAND test_type_visibility)
add_executable(test_options_dialog tests/test_options_dialog.cpp
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_options_dialog PRIVATE src)
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
add_test(NAME test_options_dialog COMMAND test_options_dialog)
if(WIN32)
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
@@ -270,14 +381,6 @@ if(BUILD_TESTING)
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
endif()
# Standalone test: proves whether CoInitializeSecurity is needed for DebugConnect
# Requires a running WinDbg debug server on port 5055
if(WIN32)
add_executable(test_com_security tests/test_com_security.cpp)
target_link_libraries(test_com_security PRIVATE dbgeng ole32 version)
add_test(NAME test_com_security COMMAND test_com_security)
endif()
# 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)
@@ -290,8 +393,12 @@ if(BUILD_TESTING)
COMMENT "Deploying Qt runtime DLLs for tests..."
)
endif()
endif() # BUILD_UI_TESTS
endif()
add_subdirectory(plugins/ProcessMemory)
add_subdirectory(plugins/RemoteProcessMemory)
if(WIN32)
add_subdirectory(plugins/ProcessMemory)
add_subdirectory(plugins/WinDbgMemory)
add_subdirectory(plugins/RcNetPluginCompatLayer)
endif()

140
README.md
View File

@@ -1,49 +1,123 @@
This tool helps you inspect raw bytes and interpret them as types (structs, arrays, primitives, pointers, padding) instead of just hex. It is essentially a debugging tool for figuring out unknown data structures either runtime or from some static source.
<div align="center">
![screenshot](screenshot.png)
# Reclass
## State
**A structured binary editor for reverse engineering — inspect raw bytes as typed structs, arrays, and pointers.<p>A complete overhaul of the popular "reclassing" tools**
- MCP (Model Context Protocol) bridge via `ReclassMcpBridge.exe`. The server starts by default and can be stopped from the File menu. It exposes all tool functionality to any MCP-compatible client (e.g. Claude Code) and falls back to UI prompts when the client requests something not yet covered by tools. To connect, add this to your MCP client config (e.g. `.mcp.json`):
```json
{
"mcpServers": {
"ReclassMcpBridge": {
"command": "path/to/build/ReclassMcpBridge.exe",
"args": []
}
[Download](https://github.com/IChooseYou/Reclass/releases) · [Build Instructions](#build) · [MCP Integration](#mcp-integration) · [Alternatives](#alternatives)
[![Build](https://github.com/IChooseYou/Reclass/actions/workflows/build.yml/badge.svg)](https://github.com/IChooseYou/Reclass/actions/workflows/build.yml)
[![License](https://img.shields.io/github/license/IChooseYou/Reclass)](LICENSE)
[![Release](https://img.shields.io/github/v/release/IChooseYou/Reclass?label=snapshot)](https://github.com/IChooseYou/Reclass/releases)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux-blue)]()
</div>
---
Reclass helps you inspect raw bytes and interpret them as types (structs, arrays, primitives, pointers, padding) instead of just hex. It is essentially a debugging tool for figuring out unknown data structures — either at runtime from a live process, or from a static source like a binary file or crash dump.
Built with C++17, Qt 6, and QScintilla. The entire editor surface is rendered as formatted plain text with inline editing, fold markers, and hex/ASCII previews.
---
## Features
- **Structured binary view** — render raw bytes as typed fields (integers, floats, pointers, vectors, matrices, strings, booleans, padding)
- **Struct & array nesting** — define nested structs and arrays with collapsible fold regions
- **Inline editing** — click to edit type names, field names, values, and base addresses directly in the editor
- **Undo/redo** — full undo history for all mutations via command stack
- **Split views** — multiple synchronized editor panes over the same document
- **Type autocomplete** — popup type picker when changing field kinds
- **Hex + ASCII margins** — raw byte previews alongside the structured view
- **MCP bridge** — expose all tool functionality to AI clients via Model Context Protocol
- **Plugin system** — extend with custom data source providers via DLL plugins; following ship by default
- **Process plugin** — access memory of live processes on Windows and Linux
- **WinDbg plugin** — access data sources live in WinDbg debugging sessions
- **ReClass.NET compatibility layer** — load existing .NET and native ReClass.NET plugins
---
## 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
- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions
---
## Screenshots
![Type chooser and struct inspection](docs/README_PIC1.png)
![VTable pointer expansion with disassembly preview](docs/README_PIC2.png)
![Split view with rendered C/C++ output](docs/README_PIC3.png)
---
## MCP Integration
Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `ReclassMcpBridge`. The server does not start by default and can be toggled from the File menu. It exposes all tool functionality to any MCP-compatible client (e.g. Claude Code) and falls back to UI prompts when the client requests something not yet covered by tools. To connect, add this to your MCP client config (e.g. `.mcp.json`):
```json
{
"mcpServers": {
"ReclassMcpBridge": {
"command": "path/to/build/ReclassMcpBridge",
"args": []
}
}
```
- Plugin system is partially implemented. Some UI bugs exist.
- Vector/Matrix improvements have been made but are not entirely complete.
- Every edit goes through a full undo/redo system.
}
```
---
## Build
1. Prerequisites
### Prerequisites
- Qt 6 with MinGW - Qt Online Installer https://doc.qt.io/qt-6/qt-online-installation.html , note to select MinGW kit + CMake/Ninja from Tools section (online installers index: https://download.qt.io/official_releases/online_installers/)
- CMake 3.20+ - https://cmake.org/download/ - bundled with Qt
- windeployqt docs - https://doc.qt.io/qt-6/windows-deployment.html
- **Qt 6** with MinGW — [Qt Online Installer](https://doc.qt.io/qt-6/qt-online-installation.html) (select MinGW kit + CMake/Ninja from the Tools section)
- **CMake 3.20+** — [cmake.org](https://cmake.org/download/) (bundled with Qt)
- **Ninja** — bundled with the Qt installer
2. Quick Build (relies on powershell| for manual build skip to step 3)
### Quick Build
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
cd Reclass
.\scripts\build_qscintilla.ps1
.\scripts\build.ps1
^ script above tries to autodetect Qt install (as we learned not everyone installs to C:/Qt/)
```bash
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
cd Reclass
.\scripts\build_qscintilla.ps1
.\scripts\build.ps1
```
3. Manual Build
The build script auto-detects your Qt install location.
Step by step for peoplewho want to run commands themselves:
1. Clone with --recurse-submodules (+ fallback git submodule update --init --recursive)
2. Build QScintilla: qmake + mingw32-make in third_party/qscintilla/src
3. CMake configure + build with -DCMAKE_PREFIX_PATH
4. optionallly windeployqt the exe
### Manual Build
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
2. Build QScintilla: `qmake` + `mingw32-make` in `third_party/qscintilla/src`
3. Configure and build:
```bash
cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/mingw_64
cmake --build build
```
4. Optionally run `windeployqt` on the output executable
### Running Tests
```bash
ctest --test-dir build --output-on-failure
```
---
## Alternatives
- ReClass.NET (reclass.net) - https://github.com/ReClassNET/ReClass.NET
- ReClassEx - https://github.com/ajkhoury/ReClassEx
- [ReClass.NET](https://github.com/ReClassNET/ReClass.NET)
- [ReClassEx](https://github.com/ajkhoury/ReClassEx)
---
<div align="center">
<sub>MIT License</sub>
</div>

8
deploy/Reclass.desktop Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
docs/README_PIC2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
docs/README_PIC3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@@ -65,7 +65,7 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
if (!m_handle || len <= 0) return false;
SIZE_T bytesRead = 0;
ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead);
ReadProcessMemory(m_handle, (LPCVOID)(addr), buf, (SIZE_T)len, &bytesRead);
if ((int)bytesRead < len)
memset((char*)buf + bytesRead, 0, len - bytesRead);
return bytesRead > 0;
@@ -76,7 +76,7 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
if (!m_handle || !m_writable || len <= 0) return false;
SIZE_T bytesWritten = 0;
if (WriteProcessMemory(m_handle, (LPVOID)(m_base + addr), buf, (SIZE_T)len, &bytesWritten))
if (WriteProcessMemory(m_handle, (LPVOID)(addr), buf, (SIZE_T)len, &bytesWritten))
return bytesWritten == (SIZE_T)len;
return false;
}
@@ -156,15 +156,13 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{
if (m_fd < 0 || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_readv first (faster, no fd seek contention)
struct iovec local;
local.iov_base = buf;
local.iov_len = static_cast<size_t>(len);
struct iovec remote;
remote.iov_base = reinterpret_cast<void*>(absAddr);
remote.iov_base = reinterpret_cast<void*>(addr);
remote.iov_len = static_cast<size_t>(len);
ssize_t nread = process_vm_readv(m_pid, &local, 1, &remote, 1, 0);
@@ -172,7 +170,7 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
return true;
// Fallback: pread on /proc/<pid>/mem
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
return nread == static_cast<ssize_t>(len);
}
@@ -180,15 +178,13 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
{
if (m_fd < 0 || !m_writable || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_writev first
struct iovec local;
local.iov_base = const_cast<void*>(buf);
local.iov_len = static_cast<size_t>(len);
struct iovec remote;
remote.iov_base = reinterpret_cast<void*>(absAddr);
remote.iov_base = reinterpret_cast<void*>(addr);
remote.iov_len = static_cast<size_t>(len);
ssize_t nwritten = process_vm_writev(m_pid, &local, 1, &remote, 1, 0);
@@ -196,7 +192,7 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
return true;
// Fallback: pwrite on /proc/<pid>/mem
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(addr));
return nwritten == static_cast<ssize_t>(len);
}
@@ -288,6 +284,15 @@ void ProcessMemoryProvider::cacheModules()
#endif // platform
uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const
{
for (const auto& mod : m_modules) {
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
return mod.base;
}
return 0;
}
ProcessMemoryProvider::~ProcessMemoryProvider()
{
#ifdef _WIN32

View File

@@ -24,14 +24,20 @@ public:
QString name() const override { return m_processName; }
QString kind() const override { return QStringLiteral("LocalProcess"); }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& name) const override;
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
bool isReadable(uint64_t, int len) const override {
#ifdef _WIN32
return m_handle && len >= 0;
#elif defined(__linux__)
return m_fd >= 0 && len >= 0;
#endif
}
// Process-specific helpers
uint32_t pid() const { return m_pid; }
uint64_t baseAddress() const { return m_base; }
void refreshModules() { m_modules.clear(); cacheModules(); }
private:

View 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"
)

View 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;
}

View 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;
};

View 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();
}

View 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();

View 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;
}

View 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;
};

View 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;
};

View 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 = "&lt;hex_address_of_RcNetFunctions&gt;|&lt;plugin_dll_path&gt;"
/// 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 { }
}
}
}

View 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>

View 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)

View File

@@ -0,0 +1,931 @@
#include "RemoteProcessMemoryPlugin.h"
#include "rcx_rpc_protocol.h"
#include "../../src/processpicker.h"
#include <QStyle>
#include <QApplication>
#include <QMessageBox>
#include <QInputDialog>
#include <QPushButton>
#include <QUuid>
#include <QDir>
#include <QFileInfo>
#include <QPixmap>
#include <QImage>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32)
#include <QtWin>
#endif
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
# include <tlhelp32.h>
# include <psapi.h>
# include <shellapi.h>
#else
# include <unistd.h>
# include <fcntl.h>
# include <dlfcn.h>
# include <sys/mman.h>
# include <sys/wait.h>
# include <sys/ptrace.h>
# include <sys/user.h>
# include <semaphore.h>
# include <signal.h>
# include <link.h>
# include <climits>
# include <cstring>
# include <fstream>
# include <sstream>
#endif
/* ══════════════════════════════════════════════════════════════════════
* IPC Client
* ══════════════════════════════════════════════════════════════════════ */
struct IpcClient {
#ifdef _WIN32
HANDLE hShm = nullptr;
HANDLE hReqEvent = nullptr;
HANDLE hRspEvent = nullptr;
#else
int shmFd = -1;
sem_t* reqSem = SEM_FAILED;
sem_t* rspSem = SEM_FAILED;
char shmNameBuf[128] = {};
char reqNameBuf[128] = {};
char rspNameBuf[128] = {};
#endif
void* mappedView = nullptr;
QMutex mutex;
bool connected = false;
~IpcClient() { disconnect(); }
/* ── connect / disconnect ──────────────────────────────────────── */
bool connect(uint32_t pid, const QByteArray& nonce, int timeoutMs = 5000)
{
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce.constData());
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce.constData());
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce.constData());
#ifdef _WIN32
/* poll for shared memory to appear (payload creating it) */
auto deadline = GetTickCount64() + (uint64_t)timeoutMs;
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
if (GetTickCount64() >= deadline) return false;
Sleep(10);
}
mappedView = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!mappedView) { CloseHandle(hShm); hShm = nullptr; return false; }
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
if (!hReqEvent || !hRspEvent) { disconnect(); return false; }
#else
strncpy(shmNameBuf, shmName, sizeof(shmNameBuf) - 1);
strncpy(reqNameBuf, reqName, sizeof(reqNameBuf) - 1);
strncpy(rspNameBuf, rspName, sizeof(rspNameBuf) - 1);
/* poll for shared memory */
auto start = std::chrono::steady_clock::now();
while (true) {
shmFd = shm_open(shmName, O_RDWR, 0);
if (shmFd >= 0) break;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeoutMs) return false;
usleep(10000);
}
mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, shmFd, 0);
if (mappedView == MAP_FAILED) { mappedView = nullptr; close(shmFd); shmFd = -1; return false; }
reqSem = sem_open(reqName, 0);
rspSem = sem_open(rspName, 0);
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) { disconnect(); return false; }
#endif
/* wait for payloadReady */
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
#ifdef _WIN32
while (!hdr->payloadReady) {
if (GetTickCount64() >= deadline) { disconnect(); return false; }
Sleep(5);
}
#else
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeoutMs) { disconnect(); return false; }
usleep(5000);
}
#endif
connected = true;
return true;
}
void disconnect()
{
#ifdef _WIN32
if (mappedView) { UnmapViewOfFile(mappedView); mappedView = nullptr; }
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
#else
if (mappedView) { munmap(mappedView, RCX_RPC_SHM_SIZE); mappedView = nullptr; }
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
#endif
connected = false;
}
/* ── low-level RPC round-trip ──────────────────────────────────── */
bool signalAndWait(int timeoutMs = 2000)
{
#ifdef _WIN32
SetEvent(hReqEvent);
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
#else
sem_post(reqSem);
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeoutMs / 1000;
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
return sem_timedwait(rspSem, &ts) == 0;
#endif
}
/* ── public API ────────────────────────────────────────────────── */
bool readSingle(uint64_t addr, void* buf, int len)
{
QMutexLocker lock(&mutex);
if (!connected || len <= 0) return false;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_READ_BATCH;
hdr->requestCount = 1;
hdr->status = RCX_RPC_STATUS_OK;
auto* entry = reinterpret_cast<RcxRpcReadEntry*>(data);
entry->address = addr;
entry->length = (uint32_t)len;
entry->dataOffset = sizeof(RcxRpcReadEntry);
if (!signalAndWait()) { connected = false; return false; }
memcpy(buf, data + entry->dataOffset, len);
return true;
}
bool writeSingle(uint64_t addr, const void* buf, int len)
{
QMutexLocker lock(&mutex);
if (!connected || len <= 0) return false;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_WRITE;
hdr->writeAddress = addr;
hdr->writeLength = (uint32_t)len;
hdr->status = RCX_RPC_STATUS_OK;
memcpy(data, buf, len);
if (!signalAndWait()) { connected = false; return false; }
return hdr->status == RCX_RPC_STATUS_OK;
}
QVector<RemoteProcessProvider::ModuleInfo> enumerateModules()
{
QVector<RemoteProcessProvider::ModuleInfo> result;
QMutexLocker lock(&mutex);
if (!connected) return result;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_ENUM_MODULES;
hdr->status = RCX_RPC_STATUS_OK;
if (!signalAndWait()) { connected = false; return result; }
if (hdr->status != RCX_RPC_STATUS_OK) return result;
uint32_t count = hdr->responseCount;
result.reserve((int)count);
for (uint32_t i = 0; i < count; ++i) {
auto* entry = reinterpret_cast<const RcxRpcModuleEntry*>(
data + i * sizeof(RcxRpcModuleEntry));
QString modName;
#ifdef _WIN32
modName = QString::fromWCharArray(
reinterpret_cast<const wchar_t*>(data + entry->nameOffset),
(int)(entry->nameLength / sizeof(wchar_t)));
#else
modName = QString::fromUtf8(
reinterpret_cast<const char*>(data + entry->nameOffset),
(int)entry->nameLength);
#endif
result.append({modName, entry->base, entry->size});
}
return result;
}
bool ping()
{
QMutexLocker lock(&mutex);
if (!connected) return false;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
hdr->command = RPC_CMD_PING;
hdr->status = RCX_RPC_STATUS_OK;
if (!signalAndWait()) { connected = false; return false; }
return true;
}
void shutdown()
{
QMutexLocker lock(&mutex);
if (!connected) return;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
hdr->command = RPC_CMD_SHUTDOWN;
hdr->status = RCX_RPC_STATUS_OK;
signalAndWait(500);
connected = false;
}
};
/* ══════════════════════════════════════════════════════════════════════
* RemoteProcessProvider
* ══════════════════════════════════════════════════════════════════════ */
RemoteProcessProvider::RemoteProcessProvider(
uint32_t pid, const QString& processName,
std::shared_ptr<IpcClient> ipc)
: m_pid(pid)
, m_processName(processName)
, m_connected(ipc && ipc->connected)
, m_base(0)
, m_ipc(std::move(ipc))
{
if (m_connected)
cacheModules();
}
RemoteProcessProvider::~RemoteProcessProvider() = default;
bool RemoteProcessProvider::read(uint64_t addr, void* buf, int len) const
{
if (!m_connected || len <= 0) return false;
bool ok = m_ipc->readSingle(addr, buf, len);
if (!ok) {
memset(buf, 0, (size_t)len);
/* update connectivity flag through mutable ipc */
const_cast<RemoteProcessProvider*>(this)->m_connected = m_ipc->connected;
}
return ok;
}
int RemoteProcessProvider::size() const
{
return m_connected ? 0x10000 : 0;
}
bool RemoteProcessProvider::write(uint64_t addr, const void* buf, int len)
{
if (!m_connected || len <= 0) return false;
bool ok = m_ipc->writeSingle(addr, buf, len);
if (!ok) m_connected = m_ipc->connected;
return ok;
}
QString RemoteProcessProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules) {
if (addr >= mod.base && addr < mod.base + mod.size) {
uint64_t off = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(off, 0, 16, QChar('0'));
}
}
return {};
}
uint64_t RemoteProcessProvider::symbolToAddress(const QString& n) const
{
for (const auto& mod : m_modules) {
if (mod.name.compare(n, Qt::CaseInsensitive) == 0)
return mod.base;
}
return 0;
}
void RemoteProcessProvider::cacheModules()
{
m_modules = m_ipc->enumerateModules();
if (!m_modules.isEmpty())
m_base = m_modules.first().base;
}
/* ══════════════════════════════════════════════════════════════════════
* Injection helpers
* ══════════════════════════════════════════════════════════════════════ */
namespace {
/* Resolve payload DLL/SO path next to this plugin DLL/SO */
static QString payloadPath()
{
#ifdef _WIN32
HMODULE hSelf = nullptr;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&payloadPath), &hSelf);
WCHAR buf[MAX_PATH];
GetModuleFileNameW(hSelf, buf, MAX_PATH);
QFileInfo fi(QString::fromWCharArray(buf));
return fi.absolutePath() + QStringLiteral("/rcx_payload.dll");
#else
Dl_info info;
dladdr(reinterpret_cast<void*>(&payloadPath), &info);
QFileInfo fi(QString::fromUtf8(info.dli_fname));
return fi.absolutePath() + QStringLiteral("/rcx_payload.so");
#endif
}
/* Create bootstrap shared memory with the nonce */
static bool createBootstrapShm(uint32_t pid, const QByteArray& nonce)
{
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
#ifdef _WIN32
HANDLE hBoot = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_BOOT_SIZE,
bootName);
if (!hBoot) return false;
auto* view = static_cast<RcxRpcBootHeader*>(
MapViewOfFile(hBoot, FILE_MAP_WRITE, 0, 0, RCX_RPC_BOOT_SIZE));
if (!view) { CloseHandle(hBoot); return false; }
memset(view, 0, RCX_RPC_BOOT_SIZE);
view->nonceLength = (uint32_t)nonce.size();
memcpy(view->nonce, nonce.constData(), qMin(nonce.size(), 59));
UnmapViewOfFile(view);
/* keep hBoot open until payload reads it (payload unlinks after reading) */
/* leak intentional: closed when process exits or payload consumes it */
return true;
#else
int fd = shm_open(bootName, O_CREAT | O_RDWR, 0600);
if (fd < 0) return false;
if (ftruncate(fd, RCX_RPC_BOOT_SIZE) != 0) { close(fd); return false; }
void* view = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd);
if (view == MAP_FAILED) return false;
auto* boot = static_cast<RcxRpcBootHeader*>(view);
memset(boot, 0, RCX_RPC_BOOT_SIZE);
boot->nonceLength = (uint32_t)nonce.size();
memcpy(boot->nonce, nonce.constData(), qMin(nonce.size(), 59));
munmap(view, RCX_RPC_BOOT_SIZE);
/* payload unlinks after consuming */
return true;
#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);
/* create remote thread calling LoadLibraryA(path) */
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);
/* check if LoadLibrary returned non-null */
DWORD exitCode = 0;
GetExitCodeThread(hThread, &exitCode);
CloseHandle(hThread);
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
CloseHandle(hProc);
if (exitCode == 0) {
if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n"
"Ensure rcx_payload.dll is in: %1").arg(path);
return false;
}
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, &regs);
ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr);
waitpid((pid_t)pid, &status, 0);
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, &regs);
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, &regs);
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, &regs);
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}:{nonce}:{name}" */
QStringList parts = target.split(':');
if (parts.size() < 4 || parts[0] != QStringLiteral("rpm")) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target;
return nullptr;
}
bool ok;
uint32_t pid = parts[1].toUInt(&ok);
QString nonce = parts[2];
QString name = parts.mid(3).join(':'); /* name may contain colons */
if (!ok || pid == 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target.");
return nullptr;
}
auto ipc = getOrCreateConnection(pid, nonce, 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() < 3 || 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) {
/* generate nonce */
QString nonce = QUuid::createUuid().toString(QUuid::Id128).left(16);
QByteArray nonceUtf8 = nonce.toUtf8();
/* create bootstrap, inject */
if (!createBootstrapShm(pid, nonceUtf8)) {
QMessageBox::critical(parent, QStringLiteral("Error"),
QStringLiteral("Failed to create bootstrap shared memory."));
return false;
}
QString injectErr;
if (!injectPayload(pid, &injectErr)) {
QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr);
return false;
}
*target = QStringLiteral("rpm:%1:%2:%3").arg(pid).arg(nonce, name);
return true;
}
else if (clicked == connectBtn) {
bool ok;
QString nonce = QInputDialog::getText(parent,
QStringLiteral("Connect to Payload"),
QStringLiteral("Enter the payload nonce:"),
QLineEdit::Normal, QString(), &ok);
if (!ok || nonce.isEmpty()) return false;
*target = QStringLiteral("rpm:%1:%2:%3").arg(pid).arg(nonce, 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, const QString& nonce, 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, nonce.toUtf8())) {
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();
}

View File

@@ -0,0 +1,86 @@
#pragma once
#include "../../src/iplugin.h"
#include "../../src/providers/provider.h"
#include <cstdint>
#include <memory>
#include <QMutex>
#include <QHash>
#include <QVector>
struct IpcClient; /* defined in .cpp */
/* ── Provider ─────────────────────────────────────────────────────── */
class RemoteProcessProvider : public rcx::Provider
{
public:
struct ModuleInfo { QString name; uint64_t base; uint64_t size; };
RemoteProcessProvider(uint32_t pid, const QString& processName,
std::shared_ptr<IpcClient> ipc);
~RemoteProcessProvider() override;
/* required */
bool read(uint64_t addr, void* buf, int len) const override;
int size() const override;
/* optional */
bool write(uint64_t addr, const void* buf, int len) override;
bool isWritable() const override { return m_connected; }
QString name() const override { return m_processName; }
QString kind() const override { return QStringLiteral("RemoteProcess"); }
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
bool isReadable(uint64_t, int len) const override { return m_connected && len >= 0; }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& n) const override;
uint32_t pid() const { return m_pid; }
private:
void cacheModules();
uint32_t m_pid;
QString m_processName;
bool m_connected;
uint64_t m_base;
mutable std::shared_ptr<IpcClient> m_ipc;
QVector<ModuleInfo> m_modules;
};
/* ── Plugin ───────────────────────────────────────────────────────── */
class RemoteProcessMemoryPlugin : public IProviderPlugin
{
public:
RemoteProcessMemoryPlugin();
~RemoteProcessMemoryPlugin() override;
std::string Name() const override { return "Remote Process Memory"; }
std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "Reclass"; }
std::string Description() const override {
return "Read/write memory via injected payload (shared-memory IPC)";
}
k_ELoadType LoadType() const override { return k_ELoadTypeManual; }
QIcon Icon() const override;
bool canHandle(const QString& target) const override;
std::unique_ptr<rcx::Provider> createProvider(const QString& target,
QString* errorMsg) override;
uint64_t getInitialBaseAddress(const QString& target) const override;
bool selectTarget(QWidget* parent, QString* target) override;
bool providesProcessList() const override { return true; }
QVector<PluginProcessInfo> enumerateProcesses() override;
private:
std::shared_ptr<IpcClient> getOrCreateConnection(
uint32_t pid, const QString& nonce, QString* errorMsg);
mutable QMutex m_connectionsMutex;
QHash<uint32_t, std::shared_ptr<IpcClient>> m_connections;
};
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();

View File

@@ -0,0 +1,623 @@
/*
* rcx_payload -- injected into target process.
*
* Pure Win32 / POSIX, NO Qt, minimal footprint.
* Reads a nonce from bootstrap shared memory, creates the main IPC
* channel (shared memory + events/semaphores), and runs a server
* thread that handles RPC commands from the editor plugin.
*/
#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_hThread = nullptr;
static volatile LONG g_shutdown = 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;
}
/* ── server thread ────────────────────────────────────────────────── */
static DWORD WINAPI ServerThread(LPVOID)
{
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
/* signal readiness */
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
while (!InterlockedCompareExchange(&g_shutdown, 0, 0)) {
DWORD rc = WaitForSingleObject(g_hReqEvent, 250);
if (rc == WAIT_TIMEOUT)
continue;
if (rc != WAIT_OBJECT_0)
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:
InterlockedExchange(&g_shutdown, 1);
break;
default:
hdr->status = RCX_RPC_STATUS_ERROR;
break;
}
SetEvent(g_hRspEvent);
if (static_cast<RcxRpcCommand>(hdr->command) == RPC_CMD_SHUTDOWN)
break;
}
/* mark not-ready so the host process can detect shutdown */
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
return 0;
}
/* ── cleanup ──────────────────────────────────────────────────────── */
static void Cleanup(bool waitThread)
{
InterlockedExchange(&g_shutdown, 1);
/* wake the thread if it's blocked on REQ */
if (g_hReqEvent) SetEvent(g_hReqEvent);
if (waitThread && g_hThread) {
WaitForSingleObject(g_hThread, 2000);
}
if (g_hThread) { CloseHandle(g_hThread); g_hThread = nullptr; }
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; }
}
/* ── DllMain ──────────────────────────────────────────────────────── */
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID reserved)
{
if (reason == DLL_PROCESS_ATTACH) {
uint32_t pid = GetCurrentProcessId();
/* ── read nonce from bootstrap shm ── */
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
HANDLE hBoot = OpenFileMappingA(FILE_MAP_READ, FALSE, bootName);
if (!hBoot) return TRUE; /* no bootstrap = nothing to do */
auto* bootView = static_cast<const RcxRpcBootHeader*>(
MapViewOfFile(hBoot, FILE_MAP_READ, 0, 0, RCX_RPC_BOOT_SIZE));
if (!bootView) { CloseHandle(hBoot); return TRUE; }
char nonce[64] = {};
uint32_t nLen = bootView->nonceLength;
if (nLen > 59) nLen = 59;
memcpy(nonce, bootView->nonce, nLen);
nonce[nLen] = '\0';
UnmapViewOfFile(bootView);
CloseHandle(hBoot);
/* ── create main shared memory ── */
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce);
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce);
g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName);
if (!g_hShm) return TRUE;
g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!g_mappedView) { CloseHandle(g_hShm); g_hShm = nullptr; return TRUE; }
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
hdr->version = RCX_RPC_VERSION;
/* image base from PEB: gs:[0x60] → PEB, +0x18 → Ldr, Flink → first entry, +0x30 → DllBase */
{
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); /* InLoadOrderModuleList.Flink */
hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30); /* DllBase */
}
/* ── create events ── */
g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName);
g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName);
if (!g_hReqEvent || !g_hRspEvent) { Cleanup(false); return TRUE; }
/* ── start server thread (payloadReady set by the thread) ── */
g_hThread = CreateThread(nullptr, 0, ServerThread, nullptr, 0, nullptr);
if (!g_hThread) { Cleanup(false); return TRUE; }
}
else if (reason == DLL_PROCESS_DETACH) {
/* reserved != NULL → process is terminating (threads already dead) */
Cleanup(reserved == nullptr);
}
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();
/* ── read nonce from bootstrap shm ── */
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
int bootFd = shm_open(bootName, O_RDONLY, 0);
if (bootFd < 0) return;
void* bootView = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ,
MAP_SHARED, bootFd, 0);
close(bootFd);
if (bootView == MAP_FAILED) return;
auto* boot = static_cast<const RcxRpcBootHeader*>(bootView);
char nonce[64] = {};
uint32_t nLen = boot->nonceLength;
if (nLen > 59) nLen = 59;
memcpy(nonce, boot->nonce, nLen);
nonce[nLen] = '\0';
munmap(bootView, RCX_RPC_BOOT_SIZE);
/* one-shot, unlink bootstrap */
shm_unlink(bootName);
/* ── open /proc/self/mem for safe access ── */
g_memFd = open("/proc/self/mem", O_RDWR);
if (g_memFd < 0) return;
/* ── create main shared memory ── */
rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid, nonce);
rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid, nonce);
rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid, nonce);
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 */

View File

@@ -0,0 +1,129 @@
/*
* 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)
#define RCX_RPC_BOOT_SIZE 64
/* status codes */
#define RCX_RPC_STATUS_OK 0
#define RCX_RPC_STATUS_ERROR 1
#define RCX_RPC_STATUS_PARTIAL 2
/* ── commands ──────────────────────────────────────────────────────── */
#ifdef __cplusplus
enum RcxRpcCommand : uint32_t {
#else
typedef uint32_t RcxRpcCommand;
enum {
#endif
RPC_CMD_NONE = 0,
RPC_CMD_READ_BATCH = 1, /* batch read: N {address, length} pairs */
RPC_CMD_WRITE = 2, /* single write */
RPC_CMD_ENUM_MODULES = 3, /* enumerate loaded modules */
RPC_CMD_PING = 4, /* heartbeat */
RPC_CMD_SHUTDOWN = 5, /* graceful teardown */
};
/* ── wire structs (natural alignment, verified by static_assert) ─── */
struct RcxRpcReadEntry {
uint64_t address;
uint32_t length;
uint32_t dataOffset; /* offset into data region for response bytes */
};
struct RcxRpcModuleEntry {
uint64_t base;
uint64_t size;
uint32_t nameOffset; /* offset into data region, UTF-16 on Win, UTF-8 on Linux */
uint32_t nameLength; /* in bytes */
};
/*
* Header -- lives at shared-memory offset 0, padded to 4096 bytes.
*
* offset field
* ------ -----
* 0 version (4)
* 4 payloadReady (4)
* 8 command (4)
* 12 requestCount (4)
* 16 writeAddress (8)
* 24 writeLength (4)
* 28 status (4)
* 32 responseCount (4)
* 36 totalDataUsed (4)
* 40 imageBase (8) -- main module base from PEB / procfs
* 48 _pad[4048]
*/
struct RcxRpcHeader {
uint32_t version;
uint32_t payloadReady; /* payload sets to 1 after init */
uint32_t command; /* RcxRpcCommand */
uint32_t requestCount;
uint64_t writeAddress;
uint32_t writeLength;
uint32_t status; /* RCX_RPC_STATUS_* */
uint32_t responseCount;
uint32_t totalDataUsed;
uint64_t imageBase; /* main module base (PEB on Win, /proc on Linux) */
uint8_t _pad[RCX_RPC_HEADER_SIZE - 48];
};
/* Bootstrap shm -- 64 bytes, carries the nonce from plugin to payload */
struct RcxRpcBootHeader {
uint32_t nonceLength;
char nonce[60];
};
/* ── name formatting helpers ───────────────────────────────────────── */
static inline void rcx_rpc_boot_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_BOOT_%u", pid);
#else
snprintf(buf, n, "/rcx_boot_%u", pid);
#endif
}
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid, const char* nonce) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_SHM_%u_%s", pid, nonce);
#else
snprintf(buf, n, "/rcx_shm_%u_%s", pid, nonce);
#endif
}
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid, const char* nonce) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_REQ_%u_%s", pid, nonce);
#else
snprintf(buf, n, "/rcx_req_%u_%s", pid, nonce);
#endif
}
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid, const char* nonce) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_RSP_%u_%s", pid, nonce);
#else
snprintf(buf, n, "/rcx_rsp_%u_%s", pid, nonce);
#endif
}
#ifdef __cplusplus
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
static_assert(sizeof(RcxRpcBootHeader) <= RCX_RPC_BOOT_SIZE, "Boot header must fit 64 bytes");
#endif

View File

@@ -0,0 +1,595 @@
/*
* 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> <nonce> [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, const char* nonce, int timeoutMs = 5000)
{
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce);
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce);
#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, char* outNonce,
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 nonce=Y testbuf=0xZ testlen=N */
unsigned long long tbuf = 0;
unsigned tlen = 0;
if (sscanf(line, "READY pid=%u nonce=%63s testbuf=0x%llx testlen=%u",
outPid, outNonce, &tbuf, &tlen) < 2) {
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;
char nonce[64] = {};
uint64_t testBuf = 0;
uint32_t testLen = 0;
bool autoMode = false;
if (argc >= 3) {
pid = (uint32_t)atoi(argv[1]);
strncpy(nonce, argv[2], 63);
if (argc >= 5) {
testBuf = (uint64_t)strtoull(argv[3], nullptr, 0);
testLen = (uint32_t)atoi(argv[4]);
}
} else {
autoMode = true;
printf("Auto-spawning test_rpc_host...\n");
if (!spawn_host(&pid, nonce, &testBuf, &testLen)) return 1;
}
printf("Connecting to PID=%u nonce=%s testbuf=0x%llx testlen=%u\n\n",
pid, nonce, (unsigned long long)testBuf, testLen);
/* ── connect ── */
TestIpcClient ipc;
if (!ipc.connect(pid, nonce)) {
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;
}

View File

@@ -0,0 +1,224 @@
/*
* test_rpc_host -- loads rcx_payload in-process, acts as the "target".
*
* Usage: test_rpc_host [nonce]
*
* 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;
}
/* Create bootstrap shared memory with the nonce */
static int create_bootstrap(uint32_t pid, const char* nonce)
{
char bootName[128];
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
#ifdef _WIN32
HANDLE h = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_BOOT_SIZE, bootName);
if (!h) return -1;
void* v = MapViewOfFile(h, FILE_MAP_WRITE, 0, 0, RCX_RPC_BOOT_SIZE);
if (!v) { CloseHandle(h); return -1; }
RcxRpcBootHeader* boot = (RcxRpcBootHeader*)v;
memset(boot, 0, RCX_RPC_BOOT_SIZE);
boot->nonceLength = (uint32_t)strlen(nonce);
strncpy(boot->nonce, nonce, 59);
UnmapViewOfFile(v);
/* keep h open for payload to read */
return 0;
#else
int fd = shm_open(bootName, O_CREAT | O_RDWR, 0600);
if (fd < 0) return -1;
if (ftruncate(fd, RCX_RPC_BOOT_SIZE) != 0) { close(fd); return -1; }
void* v = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd);
if (v == MAP_FAILED) return -1;
RcxRpcBootHeader* boot = (RcxRpcBootHeader*)v;
memset(boot, 0, RCX_RPC_BOOT_SIZE);
boot->nonceLength = (uint32_t)strlen(nonce);
strncpy(boot->nonce, nonce, 59);
munmap(v, RCX_RPC_BOOT_SIZE);
return 0;
#endif
}
/* Open the main shared memory (read-only, just to monitor payloadReady) */
static void* open_main_shm(uint32_t pid, const char* nonce)
{
char shmName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce);
#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 argc, char** argv)
{
const char* nonce = (argc > 1) ? argv[1] : "test0001";
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);
/* create bootstrap shm */
if (create_bootstrap(pid, nonce) != 0) {
fprintf(stderr, "ERROR: failed to create bootstrap shm\n");
return 1;
}
/* 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;
}
#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, nonce);
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 nonce=%s testbuf=0x%llx testlen=%u\n",
pid, nonce,
(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 server thread a moment to exit */
Sleep(200);
FreeLibrary(hPayload);
if (shmView) UnmapViewOfFile(shmView);
#else
usleep(200000);
dlclose(hPayload);
if (shmView) munmap(shmView, sizeof(RcxRpcHeader));
#endif
return 0;
}

View File

@@ -304,7 +304,7 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
bool result = false;
dispatchToOwner([&]() {
ULONG bytesRead = 0;
HRESULT hr = m_dataSpaces->ReadVirtual(m_base + addr, buf, (ULONG)len, &bytesRead);
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
if (FAILED(hr) || (int)bytesRead < len)
memset((char*)buf + bytesRead, 0, len - bytesRead);
result = bytesRead > 0;
@@ -324,7 +324,7 @@ bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len)
bool result = false;
dispatchToOwner([&]() {
ULONG bytesWritten = 0;
HRESULT hr = m_dataSpaces->WriteVirtual(m_base + addr, const_cast<void*>(buf),
HRESULT hr = m_dataSpaces->WriteVirtual(addr, const_cast<void*>(buf),
(ULONG)len, &bytesWritten);
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
});
@@ -364,7 +364,7 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
char nameBuf[512] = {};
ULONG nameSize = 0;
ULONG64 displacement = 0;
HRESULT hr = m_symbols->GetNameByOffset(m_base + addr, nameBuf, sizeof(nameBuf),
HRESULT hr = m_symbols->GetNameByOffset(addr, nameBuf, sizeof(nameBuf),
&nameSize, &displacement);
if (SUCCEEDED(hr) && nameSize > 0) {
result = QString::fromUtf8(nameBuf);

View File

@@ -62,7 +62,6 @@ public:
bool isLive() const override { return m_isLive; }
uint64_t base() const override { return m_base; }
void setBase(uint64_t b) override { m_base = b; }
private:
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

300
src/addressparser.cpp Normal file
View File

@@ -0,0 +1,300 @@
#include "addressparser.h"
namespace rcx {
// ── Address Expression Parser ──────────────────────────────────────────
//
// Parses expressions like:
// "7FF66CCE0000" → plain hex address
// "0x100 + 0x200" → arithmetic on hex values
// "<Program.exe> + 0xDE" → module base + offset
// "[<Program.exe> + 0xDE] - AB" → dereference pointer, then subtract
// "7ff6`6cce0000" → WinDbg-style backtick separator (stripped before parsing)
//
// Grammar (standard operator precedence: *, / bind tighter than +, -):
//
// expr = term (('+' | '-') term)*
// term = unary (('*' | '/') unary)*
// unary = '-' unary | atom
// atom = '[' expr ']' -- read pointer at address (dereference)
// | '<' moduleName '>' -- resolve module base address
// | '(' expr ')' -- grouping
// | hexLiteral -- hex number, optional 0x prefix
//
// All numeric literals are hexadecimal (base 16).
// Module names and pointer reads are resolved via optional callbacks.
// Without callbacks, modules and dereferences evaluate to 0 (syntax-check mode).
class ExpressionParser {
public:
ExpressionParser(const QString& input, const AddressParserCallbacks* callbacks)
: m_input(input), m_callbacks(callbacks) {}
AddressParseResult parse() {
skipSpaces();
if (atEnd())
return error("empty expression");
uint64_t value = 0;
if (!parseExpression(value))
return error(m_error);
skipSpaces();
if (!atEnd())
return error(QStringLiteral("unexpected '%1'").arg(m_input[m_pos]));
return {true, value, {}, -1};
}
private:
const QString& m_input;
const AddressParserCallbacks* m_callbacks;
int m_pos = 0;
QString m_error;
int m_errorPos = 0;
// ── Helpers ──
bool atEnd() const { return m_pos >= m_input.size(); }
QChar peek() const { return atEnd() ? QChar('\0') : m_input[m_pos]; }
void advance() { m_pos++; }
void skipSpaces() {
while (!atEnd() && m_input[m_pos].isSpace())
m_pos++;
}
AddressParseResult error(const QString& msg) const {
return {false, 0, msg, m_errorPos};
}
bool fail(const QString& msg) {
m_error = msg;
m_errorPos = m_pos;
return false;
}
bool expect(QChar ch) {
skipSpaces();
if (peek() != ch)
return fail(QStringLiteral("expected '%1'").arg(ch));
advance();
return true;
}
static bool isHexDigit(QChar ch) {
return (ch >= '0' && ch <= '9')
|| (ch >= 'a' && ch <= 'f')
|| (ch >= 'A' && ch <= 'F');
}
// ── Recursive descent parsing ──
// expr = term (('+' | '-') term)*
bool parseExpression(uint64_t& result) {
if (!parseTerm(result))
return false;
for (;;) {
skipSpaces();
QChar op = peek();
if (op != '+' && op != '-')
break;
advance();
uint64_t rhs = 0;
if (!parseTerm(rhs))
return false;
result = (op == '+') ? result + rhs : result - rhs;
}
return true;
}
// term = unary (('*' | '/') unary)*
bool parseTerm(uint64_t& result) {
if (!parseUnary(result))
return false;
for (;;) {
skipSpaces();
QChar op = peek();
if (op != '*' && op != '/')
break;
advance();
uint64_t rhs = 0;
if (!parseUnary(rhs))
return false;
if (op == '*') {
result *= rhs;
} else {
if (rhs == 0)
return fail("division by zero");
result /= rhs;
}
}
return true;
}
// unary = '-' unary | atom
bool parseUnary(uint64_t& result) {
skipSpaces();
if (peek() == '-') {
advance();
uint64_t inner = 0;
if (!parseUnary(inner))
return false;
result = static_cast<uint64_t>(-static_cast<int64_t>(inner));
return true;
}
return parseAtom(result);
}
// atom = '[' expr ']' | '<' name '>' | '(' expr ')' | hexLiteral
bool parseAtom(uint64_t& result) {
skipSpaces();
if (atEnd())
return fail("unexpected end of expression");
QChar ch = peek();
if (ch == '[') return parseDereference(result);
if (ch == '<') return parseModuleName(result);
if (ch == '(') return parseGrouping(result);
return parseHexNumber(result);
}
// '[' expr ']' — read the pointer value at the computed address
bool parseDereference(uint64_t& result) {
advance(); // skip '['
uint64_t address = 0;
if (!parseExpression(address))
return false;
if (!expect(']'))
return false;
// Without a callback, just return 0 (syntax-check mode)
if (!m_callbacks || !m_callbacks->readPointer) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->readPointer(address, &ok);
if (!ok)
return fail(QStringLiteral("failed to read memory at 0x%1").arg(address, 0, 16));
return true;
}
// '<' moduleName '>' — resolve a module's base address (e.g. <Program.exe>)
bool parseModuleName(uint64_t& result) {
advance(); // skip '<'
int nameStart = m_pos;
while (!atEnd() && peek() != '>')
advance();
if (atEnd())
return fail("expected '>'");
QString name = m_input.mid(nameStart, m_pos - nameStart).trimmed();
advance(); // skip '>'
if (name.isEmpty())
return fail("empty module name");
// Without a callback, just return 0 (syntax-check mode)
if (!m_callbacks || !m_callbacks->resolveModule) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->resolveModule(name, &ok);
if (!ok)
return fail(QStringLiteral("module '%1' not found").arg(name));
return true;
}
// '(' expr ')' — parenthesized sub-expression for grouping
bool parseGrouping(uint64_t& result) {
advance(); // skip '('
if (!parseExpression(result))
return false;
return expect(')');
}
// Hex number with optional "0x" prefix. All literals are base-16.
bool parseHexNumber(uint64_t& result) {
skipSpaces();
if (atEnd())
return fail("unexpected end of expression");
int start = m_pos;
// Skip optional 0x/0X prefix
if (m_pos + 1 < m_input.size()
&& m_input[m_pos] == '0'
&& (m_input[m_pos + 1] == 'x' || m_input[m_pos + 1] == 'X'))
m_pos += 2;
// Consume hex digits
int digitsStart = m_pos;
while (!atEnd() && isHexDigit(peek()))
advance();
if (m_pos == digitsStart) {
m_errorPos = start;
return fail("expected hex number");
}
QString digits = m_input.mid(digitsStart, m_pos - digitsStart);
bool ok = false;
result = digits.toULongLong(&ok, 16);
if (!ok) {
m_errorPos = start;
return fail("invalid hex number");
}
return true;
}
};
// ── Public API ─────────────────────────────────────────────────────────
AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize,
const AddressParserCallbacks* cb)
{
Q_UNUSED(ptrSize);
// WinDbg displays 64-bit addresses with backtick separators for readability,
// e.g. "00007ff6`1a2b3c4d". Strip them so users can paste directly.
// Also remove ' in case user uses it
QString cleaned = formula;
cleaned.remove('`');
cleaned.remove('\'');
ExpressionParser parser(cleaned, cb);
return parser.parse();
}
QString AddressParser::validate(const QString& formula)
{
QString cleaned = formula;
cleaned.remove('`');
cleaned.remove('\'');
cleaned = cleaned.trimmed();
if (cleaned.isEmpty())
return QStringLiteral("empty");
// Parse with no callbacks — modules and dereferences succeed but return 0.
// This checks syntax only.
ExpressionParser parser(cleaned, nullptr);
auto result = parser.parse();
return result.ok ? QString() : result.error;
}
} // namespace rcx

27
src/addressparser.h Normal file
View File

@@ -0,0 +1,27 @@
#pragma once
#include <QString>
#include <cstdint>
#include <functional>
namespace rcx {
struct AddressParseResult {
bool ok;
uint64_t value;
QString error;
int errorPos;
};
struct AddressParserCallbacks {
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
};
class AddressParser {
public:
static AddressParseResult evaluate(const QString& formula, int ptrSize = 8,
const AddressParserCallbacks* cb = nullptr);
static QString validate(const QString& formula);
};
} // namespace rcx

View File

@@ -22,6 +22,7 @@ struct ComposeState {
int nameW = kColName; // global name column width (fallback)
int offsetHexDigits = 8; // hex digit tier for offset margin
bool baseEmitted = false; // only first root struct shows base address
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
// Precomputed for O(1) lookups
QHash<uint64_t, QVector<int>> childMap;
@@ -77,12 +78,6 @@ static QString resolvePointerTarget(const NodeTree& tree, uint64_t refId) {
return ref.structTypeName.isEmpty() ? ref.name : ref.structTypeName;
}
static inline uint64_t ptrToProviderAddr(const NodeTree& tree, uint64_t ptr) {
if (tree.baseAddress == 0) return ptr;
if (ptr >= tree.baseAddress) return ptr - tree.baseAddress;
return UINT64_MAX; // Invalid: ptr below base address
}
static int64_t relOffsetFromRoot(const NodeTree& tree, int idx, uint64_t rootId) {
int64_t total = 0;
QSet<uint64_t> visited;
@@ -124,8 +119,17 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
QString ptrTypeOverride;
QString ptrTargetName;
if (node.kind == NodeKind::Pointer32 || node.kind == NodeKind::Pointer64) {
ptrTargetName = resolvePointerTarget(tree, node.refId);
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind)) {
// Primitive pointer: e.g. "int32*" or "f64**"
const auto* meta = kindMeta(node.elementKind);
QString baseName = meta ? QString::fromLatin1(meta->typeName)
: QStringLiteral("void");
QString stars = (node.ptrDepth >= 2) ? QStringLiteral("**") : QStringLiteral("*");
ptrTypeOverride = baseName + stars;
} else {
ptrTargetName = resolvePointerTarget(tree, node.refId);
ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
}
}
for (int sub = 0; sub < numLines; sub++) {
@@ -139,8 +143,9 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
lm.isContinuation = isCont;
lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field;
lm.nodeKind = node.kind;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
lm.foldLevel = computeFoldLevel(depth, false);
lm.effectiveTypeW = typeW;
@@ -185,8 +190,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Field;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
lm.foldLevel = computeFoldLevel(depth, false);
@@ -203,8 +209,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::ArrayElementSeparator;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
@@ -232,8 +239,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.isRootHeader = false;
lm.foldHead = true;
@@ -295,8 +303,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.lineKind = LineKind::Field;
lm.nodeKind = node.elementKind;
lm.isArrayElement = true;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + elemAddr;
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = elemAddr;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
lm.foldLevel = computeFoldLevel(childDepth, false);
lm.effectiveTypeW = eTW;
@@ -347,9 +356,10 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.depth = childDepth;
lm.lineKind = LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(
tree.baseAddress + absAddr + child.offset, false,
absAddr + child.offset, false,
state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr + child.offset;
lm.offsetAddr = absAddr + child.offset;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = child.kind;
lm.foldHead = true;
lm.foldCollapsed = true;
@@ -392,8 +402,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
int sz = tree.structSpan(node.id, &state.childMap);
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr + sz;
lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = absAddr + sz;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
}
@@ -437,8 +448,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
lm.nodeId = node.id;
lm.depth = depth;
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
lm.offsetAddr = absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.foldHead = true;
lm.foldCollapsed = effectiveCollapsed;
@@ -463,23 +475,21 @@ void composeNode(ComposeState& state, const NodeTree& tree,
// Treat sentinel values as invalid pointers
if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
ptrVal = 0;
else {
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
}
}
}
// Determine if pointer target is actually readable
uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0;
// Pointer target address is used directly (absolute)
uint64_t pBase = ptrVal;
bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1);
// For invalid/unreadable pointers: use NullProvider (shows zeros)
// and reset margin offsets (unsigned wrap cancels baseAddress)
static NullProvider s_nullProv;
const Provider& childProv = ptrReadable ? prov : static_cast<const Provider&>(s_nullProv);
if (!ptrReadable)
pBase = (uint64_t)0 - tree.baseAddress;
pBase = 0;
uint64_t savedPtrBase = state.currentPtrBase;
state.currentPtrBase = pBase;
if (hasMaterialized) {
// Render materialized children at the pointer target address.
@@ -519,6 +529,8 @@ void composeNode(ComposeState& state, const NodeTree& tree,
}
}
state.currentPtrBase = savedPtrBase;
// Footer for pointer fold
{
LineMeta lm;
@@ -552,16 +564,16 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
for (int i = 0; i < tree.nodes.size(); i++)
state.childMap[tree.nodes[i].parentId].append(i);
// Precompute absolute offsets
// Precompute absolute offsets (baseAddress + structure-relative offset)
state.absOffsets.resize(tree.nodes.size());
for (int i = 0; i < tree.nodes.size(); i++)
state.absOffsets[i] = tree.computeOffset(i);
state.absOffsets[i] = tree.baseAddress + tree.computeOffset(i);
// Compute hex digit tier from max absolute address
{
uint64_t maxAddr = tree.baseAddress;
for (int i = 0; i < tree.nodes.size(); i++) {
uint64_t addr = tree.baseAddress + (uint64_t)state.absOffsets[i];
uint64_t addr = (uint64_t)state.absOffsets[i];
if (addr > maxAddr) maxAddr = addr;
}
if (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4;
@@ -657,7 +669,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
}
// Emit CommandRow as line 0 (combined: source + address + root class type + name)
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct\u25BE NoName {");
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct NoName {");
{
LineMeta lm;
lm.nodeIdx = -1;
@@ -668,6 +680,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
lm.foldHead = false;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = 0;
lm.effectiveTypeW = state.typeW;
lm.effectiveNameW = state.nameW;
@@ -728,20 +741,5 @@ QSet<uint64_t> NodeTree::normalizePreferDescendants(const QSet<uint64_t>& ids) c
return result;
}
int NodeTree::computeStructAlignment(uint64_t structId) const {
int idx = indexOfId(structId);
if (idx < 0) return 1;
int maxAlign = 1;
QVector<int> kids = childrenOf(structId);
for (int ci : kids) {
const Node& c = nodes[ci];
if (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) {
maxAlign = qMax(maxAlign, computeStructAlignment(c.id));
} else {
maxAlign = qMax(maxAlign, alignmentFor(c.kind));
}
}
return maxAlign;
}
} // namespace rcx

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,7 @@ struct SavedSourceEntry {
QString filePath; // for File sources
QString providerTarget; // for plugin providers (e.g. "pid:name")
uint64_t baseAddress = 0;
QString baseAddressFormula;
};
// ── Controller ──
@@ -85,20 +86,27 @@ public:
void removeSplitEditor(RcxEditor* editor);
QList<RcxEditor*> editors() const { return m_editors; }
void convertRootKeyword(const QString& newKeyword);
void changeNodeKind(int nodeIdx, NodeKind newKind);
void renameNode(int nodeIdx, const QString& newName);
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
void removeNode(int nodeIdx);
void toggleCollapse(int nodeIdx);
void materializeRefChildren(int nodeIdx);
void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false);
void setNodeValue(int nodeIdx, int subLine, const QString& text,
bool isAscii = false, uint64_t resolvedAddr = 0);
void duplicateNode(int nodeIdx);
void convertToTypedPointer(uint64_t nodeId);
void splitHexNode(uint64_t nodeId);
void 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 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,
@@ -113,6 +121,7 @@ public:
RcxDocument* document() const { return m_doc; }
void setEditorFont(const QString& fontName);
void setRefreshInterval(int ms);
// MCP bridge accessors
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
@@ -120,6 +129,18 @@ public:
const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; }
int activeSourceIndex() const { return m_activeSourceIdx; }
void switchSource(int idx) { switchToSavedSource(idx); }
void clearSources();
void selectSource(const QString& text);
// Value tracking toggle (per-tab, off by default)
bool trackValues() const { return m_trackValues; }
void setTrackValues(bool on);
// Cross-tab type visibility: point at the project's full document list
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
// Test accessor
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
signals:
void nodeSelected(int nodeIdx);
@@ -148,18 +169,20 @@ private:
std::unique_ptr<SnapshotProvider> m_snapshotProv;
PageMap m_prevPages;
QSet<int64_t> m_changedOffsets;
QHash<uint64_t, ValueHistory> m_valueHistory;
bool m_trackValues = false;
uint64_t m_refreshGen = 0;
uint64_t m_readGen = 0;
bool m_readInFlight = false;
QVector<RcxDocument*>* m_projectDocs = nullptr;
void connectEditor(RcxEditor* editor);
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
void updateCommandRow();
void performRealignment(uint64_t structId, int targetAlign);
void switchToSavedSource(int idx);
void pushSavedSourcesToEditors();
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
TypeSelectorPopup* ensurePopup(RcxEditor* editor);
// ── Auto-refresh methods ──

View File

@@ -8,6 +8,7 @@
#include <QHash>
#include <QSet>
#include <cstdint>
#include <array>
#include <memory>
#include <variant>
@@ -25,6 +26,7 @@ enum class NodeKind : uint8_t {
UInt8, UInt16, UInt32, UInt64,
Float, Double, Bool,
Pointer32, Pointer64,
FuncPtr32, FuncPtr64,
Vec2, Vec3, Vec4, Mat4x4,
UTF8, UTF16,
Struct, Array
@@ -32,7 +34,7 @@ enum class NodeKind : uint8_t {
} // namespace rcx (temporarily close for qHash)
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return ::qHash(static_cast<uint8_t>(key), seed); }
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return qHash(static_cast<int>(key), seed); }
#endif
namespace rcx { // reopen
@@ -77,6 +79,8 @@ 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},
@@ -135,6 +139,18 @@ inline constexpr bool isVectorKind(NodeKind k) {
inline constexpr bool isMatrixKind(NodeKind k) {
return k == NodeKind::Mat4x4;
}
inline constexpr bool isFuncPtr(NodeKind k) {
return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64;
}
// Hex types, pointer types, function pointers, and containers are not meaningful
// primitive-pointer targets — dereferencing them produces the same output as void*.
inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) {
if (isHexNode(k)) return false;
if (k == NodeKind::Pointer32 || k == NodeKind::Pointer64) return false;
if (isFuncPtr(k)) return false;
if (k == NodeKind::Struct || k == NodeKind::Array) return false;
return true;
}
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
QStringList out;
@@ -177,7 +193,8 @@ struct Node {
int strLen = 64;
bool collapsed = false;
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
NodeKind elementKind = NodeKind::UInt8; // Array: element type
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
int viewIndex = 0; // Array: current view offset (transient)
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
@@ -210,6 +227,8 @@ struct Node {
o["collapsed"] = collapsed;
o["refId"] = QString::number(refId);
o["elementKind"] = kindToString(elementKind);
if (ptrDepth > 0)
o["ptrDepth"] = ptrDepth;
return o;
}
static Node fromJson(const QJsonObject& o) {
@@ -226,6 +245,7 @@ struct Node {
n.collapsed = o["collapsed"].toBool(false);
n.refId = o["refId"].toString("0").toULongLong();
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
return n;
}
@@ -247,6 +267,7 @@ struct Node {
struct NodeTree {
QVector<Node> nodes;
uint64_t baseAddress = 0x00400000;
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
uint64_t m_nextId = 1;
mutable QHash<uint64_t, int> m_idCache;
@@ -373,9 +394,6 @@ struct NodeTree {
return qMax(declaredSize, maxEnd);
}
// Compute natural alignment of a struct (max alignment of direct children)
int computeStructAlignment(uint64_t structId) const;
// Batch selection normalizers
QSet<uint64_t> normalizePreferAncestors(const QSet<uint64_t>& ids) const;
QSet<uint64_t> normalizePreferDescendants(const QSet<uint64_t>& ids) const;
@@ -383,6 +401,8 @@ struct NodeTree {
QJsonObject toJson() const {
QJsonObject o;
o["baseAddress"] = QString::number(baseAddress, 16);
if (!baseAddressFormula.isEmpty())
o["baseAddressFormula"] = baseAddressFormula;
o["nextId"] = QString::number(m_nextId);
QJsonArray arr;
for (const auto& n : nodes) arr.append(n.toJson());
@@ -393,6 +413,7 @@ struct NodeTree {
static NodeTree fromJson(const QJsonObject& o) {
NodeTree t;
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
t.baseAddressFormula = o["baseAddressFormula"].toString();
t.m_nextId = o["nextId"].toString("1").toULongLong();
QJsonArray arr = o["nodes"].toArray();
for (const auto& v : arr) {
@@ -405,6 +426,49 @@ struct NodeTree {
};
// ── Value History (ring buffer for heatmap) ──
struct ValueHistory {
static constexpr int kCapacity = 10;
std::array<QString, kCapacity> values;
int count = 0; // total unique values recorded
int head = 0; // next write position in ring
void record(const QString& v) {
if (count > 0) {
int last = (head + kCapacity - 1) % kCapacity;
if (values[last] == v) return; // no change
}
values[head] = v;
head = (head + 1) % kCapacity;
if (count < INT_MAX) count++;
}
int uniqueCount() const { return qMin(count, kCapacity); }
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
int heatLevel() const {
if (count <= 1) return 0;
if (count == 2) return 1;
if (count <= 4) return 2;
return 3;
}
QString last() const {
if (count == 0) return {};
return values[(head + kCapacity - 1) % kCapacity];
}
// Iterate from oldest to newest (up to uniqueCount entries)
template<typename Fn>
void forEach(Fn&& fn) const {
int n = uniqueCount();
int start = (head + kCapacity - n) % kCapacity;
for (int i = 0; i < n; i++)
fn(values[(start + i) % kCapacity]);
}
};
// ── LineMeta ──
enum class LineKind : uint8_t {
@@ -437,8 +501,10 @@ struct LineMeta {
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
QString offsetText;
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
uint64_t ptrBase = 0; // Pointer expansion base (non-zero = use for RVA)
uint32_t markerMask = 0;
bool dataChanged = false; // true if any byte in this node changed since last refresh
int heatLevel = 0; // 0=static, 1=cold, 2=warm, 3=hot (from ValueHistory)
QVector<int> changedByteIndices; // Hex preview: which byte indices (0-based) changed on this line
int lineByteCount = 0; // Hex preview: actual data byte count on this line
int effectiveTypeW = 14; // Per-line type column width used for rendering
@@ -479,7 +545,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;
@@ -601,23 +667,27 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
int tag = lineText.indexOf(QStringLiteral(" \u00B7"));
if (tag < 0) return {};
int start = tag + 3; // after " · "
int end = start;
while (end < lineText.size() && !lineText[end].isSpace()) end++;
// Scan to next " · " separator (or end of line) to support formulas with spaces
int nextSep = lineText.indexOf(QStringLiteral(" \u00B7"), start);
int end = (nextSep >= 0) ? nextSep : lineText.size();
// Trim trailing whitespace
while (end > start && lineText[end - 1].isSpace()) end--;
if (end <= start) return {};
return {start, end, true};
}
// ── CommandRow root-class spans ──
// Combined CommandRow format ends with: " struct ClassName {"
// Combined CommandRow format ends with: " struct ClassName {"
inline int commandRowRootStart(const QString& lineText) {
int best = -1;
int i;
i = lineText.lastIndexOf(QStringLiteral("struct\u25BE"));
// Match "struct " / "class " / "enum " as whole words before the class name
i = lineText.lastIndexOf(QStringLiteral("struct "));
if (i > best) best = i;
i = lineText.lastIndexOf(QStringLiteral("class\u25BE"));
i = lineText.lastIndexOf(QStringLiteral("class "));
if (i > best) best = i;
i = lineText.lastIndexOf(QStringLiteral("enum\u25BE"));
i = lineText.lastIndexOf(QStringLiteral("enum "));
if (i > best) best = i;
return best;
}
@@ -626,8 +696,7 @@ inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) {
int start = commandRowRootStart(lineText);
if (start < 0) return {};
int end = start;
while (end < lineText.size() && lineText[end] != QChar(' ')
&& lineText[end] != QChar(0x25BE)) end++;
while (end < lineText.size() && lineText[end] != QChar(' ')) end++;
if (end <= start) return {};
return {start, end, true};
}

76
src/disasm.cpp Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ public:
void restoreViewState(const ViewState& vs);
QsciScintilla* scintilla() const { return m_sci; }
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
const LineMeta* metaForLine(int line) const;
int currentNodeIndex() const;
void scrollToNodeId(uint64_t nodeId);
@@ -54,6 +55,10 @@ public:
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
QString textWithMargins() const;
void setCustomTypeNames(const QStringList& names);
void setValueHistoryRef(const QHash<uint64_t, ValueHistory>* ref) { m_valueHistory = ref; }
void setProviderRef(const Provider* prov, const Provider* realProv, const NodeTree* tree) {
m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree;
}
// Saved sources for quick-switch in source picker
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
@@ -61,9 +66,11 @@ public:
signals:
void marginClicked(int margin, int line, Qt::KeyboardModifiers mods);
void contextMenuRequested(int line, int nodeIdx, int subLine, QPoint globalPos);
void keywordConvertRequested(const QString& newKeyword);
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
void inlineEditCommitted(int nodeIdx, int subLine,
EditTarget target, const QString& text);
EditTarget target, const QString& text,
uint64_t resolvedAddr = 0);
void inlineEditCancelled();
void typeSelectorRequested();
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
@@ -78,7 +85,7 @@ private:
LayoutInfo m_layout; // cached from ComposeResult
// ── Toggle: absolute vs relative offset margin
bool m_relativeOffsets = false;
bool m_relativeOffsets = true;
int m_marginStyleBase = -1;
int m_hintLine = -1;
@@ -129,7 +136,17 @@ private:
// ── Saved sources for quick-switch ──
QVector<SavedSourceDisplay> m_savedSourceDisplay;
// ── Value history ref (owned by controller) ──
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
QWidget* m_structPreviewPopup = nullptr; // StructPreviewPopup (file-local class in editor.cpp)
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
const NodeTree* m_disasmTree = nullptr;
// ── Reentrancy guards ──
bool m_applyingDocument = false;
bool m_clampingSelection = false;
bool m_updatingComment = false;
@@ -145,7 +162,8 @@ private:
void applyMarkers(const QVector<LineMeta>& meta);
void applyFoldLevels(const QVector<LineMeta>& meta);
void applyHexDimming(const QVector<LineMeta>& meta);
void applyDataChangedHighlight(const QVector<LineMeta>& meta);
void applyHeatmapHighlight(const QVector<LineMeta>& meta);
void applySymbolColoring(const QVector<LineMeta>& meta);
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
void applyCommandRowPills();

File diff suppressed because it is too large Load Diff

View 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": 4,
"collapsed": false,
"elementKind": "Float",
"id": "27",
"kind": "Array",
"name": "scores",
"offset": 112,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 2,
"collapsed": false,
"elementKind": "Struct",
"id": "28",
"kind": "Array",
"name": "materials",
"offset": 128,
"parentId": "1",
"refId": "20",
"strLen": 64
}
]
}

View File

@@ -1,4 +1,5 @@
#include "core.h"
#include "addressparser.h"
#include <cmath>
#include <cstring>
#include <limits>
@@ -262,15 +263,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:
@@ -543,6 +584,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('"'))
@@ -571,7 +620,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) {
@@ -615,43 +665,13 @@ 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");
int pos = 0;
bool firstTerm = true;
while (pos < s.size()) {
// Skip whitespace
while (pos < s.size() && s[pos].isSpace()) pos++;
if (pos >= s.size()) break;
// Check for +/- operator (except first term)
if (!firstTerm) {
if (s[pos] == '+' || s[pos] == '-') pos++;
else return QStringLiteral("invalid '%1'").arg(s[pos]);
while (pos < s.size() && s[pos].isSpace()) pos++;
}
// Skip 0x prefix if present
if (pos + 1 < s.size() && s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X'))
pos += 2;
// Must have at least one hex digit
int numStart = pos;
while (pos < s.size() && (s[pos].isDigit() ||
(s[pos] >= 'a' && s[pos] <= 'f') ||
(s[pos] >= 'A' && s[pos] <= 'F'))) pos++;
if (pos == numStart) return QStringLiteral("invalid");
firstTerm = false;
}
return {};
//s.remove('`');
return AddressParser::validate(s);
}
} // namespace rcx::fmt

View File

@@ -44,6 +44,8 @@ static QString cTypeName(NodeKind kind) {
case NodeKind::Bool: return QStringLiteral("bool");
case NodeKind::Pointer32: return QStringLiteral("uint32_t");
case NodeKind::Pointer64: return QStringLiteral("uint64_t");
case NodeKind::FuncPtr32: return QStringLiteral("uint32_t");
case NodeKind::FuncPtr64: return QStringLiteral("uint64_t");
case NodeKind::Vec2: return QStringLiteral("float");
case NodeKind::Vec3: return QStringLiteral("float");
case NodeKind::Vec4: return QStringLiteral("float");
@@ -143,6 +145,10 @@ static QString emitField(GenContext& ctx, const Node& node) {
}
return QStringLiteral(" void* %1;").arg(name) + oc;
}
case NodeKind::FuncPtr32:
return QStringLiteral(" void (*%1)();").arg(name) + oc;
case NodeKind::FuncPtr64:
return QStringLiteral(" void (*%1)();").arg(name) + oc;
default:
return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name) + oc;
}

View File

@@ -0,0 +1,204 @@
#include "export_reclass_xml.h"
#include <QFile>
#include <QXmlStreamWriter>
#include <QHash>
#include <QVector>
#include <algorithm>
namespace rcx {
// Reverse type map: NodeKind -> ReClassEx V2016 XML Type integer
static int xmlTypeForKind(NodeKind kind) {
switch (kind) {
case NodeKind::Struct: return 1; // ClassInstance
case NodeKind::Hex32: return 4;
case NodeKind::Hex64: return 5;
case NodeKind::Hex16: return 6;
case NodeKind::Hex8: return 7;
case NodeKind::Pointer64: return 8; // ClassPointer
case NodeKind::Pointer32: return 8;
case NodeKind::Int64: return 9;
case NodeKind::Int32: return 10;
case NodeKind::Int16: return 11;
case NodeKind::Int8: return 12;
case NodeKind::Float: return 13;
case NodeKind::Double: return 14;
case NodeKind::UInt32: return 15;
case NodeKind::UInt16: return 16;
case NodeKind::UInt8: return 17;
case NodeKind::UInt64: return 32;
case NodeKind::UTF8: return 18;
case NodeKind::UTF16: return 19;
case NodeKind::Bool: return 17; // No native bool in ReClass, map to UInt8
case NodeKind::Vec2: return 22;
case NodeKind::Vec3: return 23;
case NodeKind::Vec4: return 24;
case NodeKind::Mat4x4: return 25;
case NodeKind::Array: return 27; // ClassInstanceArray
}
return 7; // fallback to Hex8
}
static int nodeSizeForExport(const Node& node) {
switch (node.kind) {
case NodeKind::UTF8: return node.strLen;
case NodeKind::UTF16: return node.strLen * 2;
case NodeKind::Array: {
int elemSz = sizeForKind(node.elementKind);
return node.arrayLen * (elemSz > 0 ? elemSz : 0);
}
default: return sizeForKind(node.kind);
}
}
// Resolve a struct type name from a node ID
static QString resolveStructName(const NodeTree& tree, uint64_t refId) {
int idx = tree.indexOfId(refId);
if (idx < 0) return {};
const Node& ref = tree.nodes[idx];
if (!ref.structTypeName.isEmpty()) return ref.structTypeName;
return ref.name;
}
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg) {
if (tree.nodes.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("No nodes to export");
return false;
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file for writing: ") + filePath;
return false;
}
// Build child map
QHash<uint64_t, QVector<int>> childMap;
for (int i = 0; i < tree.nodes.size(); i++)
childMap[tree.nodes[i].parentId].append(i);
QXmlStreamWriter xml(&file);
xml.setAutoFormatting(true);
xml.setAutoFormattingIndent(4);
xml.writeStartDocument();
xml.writeStartElement(QStringLiteral("ReClass"));
xml.writeComment(QStringLiteral("ReClassEx"));
// Get root structs
QVector<int> roots = childMap.value(0);
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
int classCount = 0;
for (int ri : roots) {
const Node& root = tree.nodes[ri];
if (root.kind != NodeKind::Struct) continue;
xml.writeStartElement(QStringLiteral("Class"));
xml.writeAttribute(QStringLiteral("Name"), root.name.isEmpty() ? root.structTypeName : root.name);
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("28"));
xml.writeAttribute(QStringLiteral("Comment"), QString());
xml.writeAttribute(QStringLiteral("Offset"), QStringLiteral("0"));
xml.writeAttribute(QStringLiteral("strOffset"), QStringLiteral("0"));
xml.writeAttribute(QStringLiteral("Code"), QString());
// Get children sorted by offset
QVector<int> children = childMap.value(root.id);
std::sort(children.begin(), children.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
int i = 0;
while (i < children.size()) {
const Node& child = tree.nodes[children[i]];
// Collapse consecutive hex nodes into a single Custom node (Type=21)
if (isHexNode(child.kind)) {
int runStart = child.offset;
int runEnd = child.offset + child.byteSize();
int j = i + 1;
while (j < children.size()) {
const Node& next = tree.nodes[children[j]];
if (!isHexNode(next.kind)) break;
if (next.offset < runEnd) break; // overlap
runEnd = next.offset + next.byteSize();
j++;
}
int totalSize = runEnd - runStart;
xml.writeStartElement(QStringLiteral("Node"));
// Use first hex node's name if it's a single node, otherwise generate
QString hexName = (j - i == 1 && !child.name.isEmpty()) ? child.name : QString();
xml.writeAttribute(QStringLiteral("Name"), hexName);
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("21")); // Custom
xml.writeAttribute(QStringLiteral("Size"), QString::number(totalSize));
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
xml.writeAttribute(QStringLiteral("Comment"), QString());
xml.writeEndElement(); // Node
i = j;
continue;
}
xml.writeStartElement(QStringLiteral("Node"));
xml.writeAttribute(QStringLiteral("Name"), child.name);
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(child.kind)));
xml.writeAttribute(QStringLiteral("Size"), QString::number(nodeSizeForExport(child)));
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
xml.writeAttribute(QStringLiteral("Comment"), QString());
// Pointer with target
if ((child.kind == NodeKind::Pointer64 || child.kind == NodeKind::Pointer32) && child.refId != 0) {
QString target = resolveStructName(tree, child.refId);
if (!target.isEmpty())
xml.writeAttribute(QStringLiteral("Pointer"), target);
}
// Embedded struct instance
if (child.kind == NodeKind::Struct) {
QString instName = child.structTypeName.isEmpty() ? child.name : child.structTypeName;
xml.writeAttribute(QStringLiteral("Instance"), instName);
}
// Array: Total attribute and child <Array> element
if (child.kind == NodeKind::Array) {
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
// Resolve element type name
QString elemName;
if (child.elementKind == NodeKind::Struct && !child.structTypeName.isEmpty()) {
elemName = child.structTypeName;
} else if (child.refId != 0) {
elemName = resolveStructName(tree, child.refId);
}
if (elemName.isEmpty())
elemName = kindToString(child.elementKind);
xml.writeStartElement(QStringLiteral("Array"));
xml.writeAttribute(QStringLiteral("Name"), elemName);
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
xml.writeEndElement(); // Array
}
xml.writeEndElement(); // Node
i++;
}
xml.writeEndElement(); // Class
classCount++;
}
xml.writeEndElement(); // ReClass
xml.writeEndDocument();
file.close();
if (classCount == 0) {
if (errorMsg) *errorMsg = QStringLiteral("No struct classes found to export");
return false;
}
return true;
}
} // namespace rcx

View 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

971
src/imports/import_pdb.cpp Normal file
View File

@@ -0,0 +1,971 @@
#include "import_pdb.h"
#ifdef _WIN32
#include <windows.h>
#include <QFile>
#include <QHash>
#include <QPair>
#include <QSet>
// ── RawPDB headers ──
#include "PDB.h"
#include "PDB_RawFile.h"
#include "PDB_TPIStream.h"
#include "PDB_TPITypes.h"
#include "PDB_DBIStream.h"
#include "PDB_InfoStream.h"
#include "PDB_CoalescedMSFStream.h"
#include "Foundation/PDB_Memory.h"
namespace rcx {
// ── Memory-mapped file (mirrors ExampleMemoryMappedFile) ──
struct MappedFile {
HANDLE hFile = INVALID_HANDLE_VALUE;
HANDLE hMapping = nullptr;
const void* base = nullptr;
size_t size = 0;
bool open(const QString& path) {
hFile = CreateFileW(reinterpret_cast<const wchar_t*>(path.utf16()),
GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
if (hFile == INVALID_HANDLE_VALUE) return false;
hMapping = CreateFileMappingW(hFile, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (!hMapping) { close(); return false; }
base = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (!base) { close(); return false; }
BY_HANDLE_FILE_INFORMATION info;
if (!GetFileInformationByHandle(hFile, &info)) { close(); return false; }
size = (static_cast<size_t>(info.nFileSizeHigh) << 32) | info.nFileSizeLow;
return true;
}
void close() {
if (base) { UnmapViewOfFile(base); base = nullptr; }
if (hMapping) { CloseHandle(hMapping); hMapping = nullptr; }
if (hFile != INVALID_HANDLE_VALUE) { CloseHandle(hFile); hFile = INVALID_HANDLE_VALUE; }
size = 0;
}
~MappedFile() { close(); }
MappedFile() = default;
MappedFile(const MappedFile&) = delete;
MappedFile& operator=(const MappedFile&) = delete;
};
// ── TypeTable (mirrors ExampleTypeTable) ──
// Builds an O(1) lookup table from type index → record pointer.
class TypeTable {
public:
explicit TypeTable(const PDB::TPIStream& tpiStream) {
m_firstIndex = tpiStream.GetFirstTypeIndex();
m_lastIndex = tpiStream.GetLastTypeIndex();
m_count = tpiStream.GetTypeRecordCount();
const PDB::DirectMSFStream& ds = tpiStream.GetDirectMSFStream();
m_stream = PDB::CoalescedMSFStream(ds, ds.GetSize(), 0u);
m_records = PDB_NEW_ARRAY(const PDB::CodeView::TPI::Record*, m_count);
uint32_t idx = 0;
tpiStream.ForEachTypeRecordHeaderAndOffset(
[this, &idx](const PDB::CodeView::TPI::RecordHeader&, size_t offset) {
m_records[idx++] = m_stream.GetDataAtOffset<const PDB::CodeView::TPI::Record>(offset);
});
}
~TypeTable() { PDB_DELETE_ARRAY(m_records); }
uint32_t firstIndex() const { return m_firstIndex; }
uint32_t lastIndex() const { return m_lastIndex; }
size_t count() const { return m_count; }
const PDB::CodeView::TPI::Record* get(uint32_t typeIndex) const {
if (typeIndex < m_firstIndex || typeIndex >= m_lastIndex) return nullptr;
return m_records[typeIndex - m_firstIndex];
}
private:
uint32_t m_firstIndex = 0;
uint32_t m_lastIndex = 0;
size_t m_count = 0;
const PDB::CodeView::TPI::Record** m_records = nullptr;
PDB::CoalescedMSFStream m_stream;
TypeTable(const TypeTable&) = delete;
TypeTable& operator=(const TypeTable&) = delete;
};
// ── Leaf numeric helpers (variable-length integer encoding) ──
using TRK = PDB::CodeView::TPI::TypeRecordKind;
static uint8_t leafSize(TRK kind) {
if (kind < TRK::LF_NUMERIC) return sizeof(TRK); // value is the kind itself
switch (kind) {
case TRK::LF_CHAR: return sizeof(TRK) + sizeof(uint8_t);
case TRK::LF_SHORT:
case TRK::LF_USHORT: return sizeof(TRK) + sizeof(uint16_t);
case TRK::LF_LONG:
case TRK::LF_ULONG: return sizeof(TRK) + sizeof(uint32_t);
case TRK::LF_QUADWORD:
case TRK::LF_UQUADWORD: return sizeof(TRK) + sizeof(uint64_t);
default: return sizeof(TRK);
}
}
static const char* leafName(const char* data, TRK kind) {
return data + leafSize(kind);
}
static uint64_t leafValue(const char* data, TRK kind) {
if (kind < TRK::LF_NUMERIC) {
return static_cast<uint16_t>(kind);
}
const char* p = data + sizeof(TRK);
switch (kind) {
case TRK::LF_CHAR: return *reinterpret_cast<const uint8_t*>(p);
case TRK::LF_SHORT: return *reinterpret_cast<const int16_t*>(p);
case TRK::LF_USHORT: return *reinterpret_cast<const uint16_t*>(p);
case TRK::LF_LONG: return *reinterpret_cast<const int32_t*>(p);
case TRK::LF_ULONG: return *reinterpret_cast<const uint32_t*>(p);
case TRK::LF_QUADWORD: return *reinterpret_cast<const int64_t*>(p);
case TRK::LF_UQUADWORD: return *reinterpret_cast<const uint64_t*>(p);
default: return 0;
}
}
// ── Primitive type index mapping (< 0x1000) ──
static NodeKind mapPrimitiveType(uint32_t typeIndex) {
uint32_t base = typeIndex & 0xFF;
switch (base) {
// void
case 0x03: return NodeKind::Hex8;
// signed char
case 0x10: return NodeKind::Int8;
// unsigned char
case 0x20: return NodeKind::UInt8;
// real char
case 0x70: return NodeKind::Int8;
// wchar
case 0x71: return NodeKind::UInt16;
// char8
case 0x7c: return NodeKind::UInt8;
// char16
case 0x7a: return NodeKind::UInt16;
// char32
case 0x7b: return NodeKind::UInt32;
// short
case 0x11: return NodeKind::Int16;
// ushort
case 0x21: return NodeKind::UInt16;
// long
case 0x12: return NodeKind::Int32;
// ulong
case 0x22: return NodeKind::UInt32;
// int8
case 0x68: return NodeKind::Int8;
// uint8
case 0x69: return NodeKind::UInt8;
// int16
case 0x72: return NodeKind::Int16;
// uint16
case 0x73: return NodeKind::UInt16;
// int32
case 0x74: return NodeKind::Int32;
// uint32
case 0x75: return NodeKind::UInt32;
// quad (int64)
case 0x13: return NodeKind::Int64;
// uquad (uint64)
case 0x23: return NodeKind::UInt64;
// int64
case 0x76: return NodeKind::Int64;
// uint64
case 0x77: return NodeKind::UInt64;
// float
case 0x40: return NodeKind::Float;
// double
case 0x41: return NodeKind::Double;
// bool
case 0x30: return NodeKind::Bool;
case 0x31: return NodeKind::UInt16; // bool16
case 0x32: return NodeKind::UInt32; // bool32
case 0x33: return NodeKind::UInt64; // bool64
// HRESULT
case 0x08: return NodeKind::UInt32;
// bit
case 0x60: return NodeKind::UInt8;
// int128 / uint128 approximation
case 0x78: return NodeKind::Hex64; // int128 → Hex64 (best we can do)
case 0x79: return NodeKind::Hex64; // uint128
default: return NodeKind::Hex32;
}
}
static NodeKind hexForSize(uint64_t len) {
switch (len) {
case 1: return NodeKind::Hex8;
case 2: return NodeKind::Hex16;
case 4: return NodeKind::Hex32;
case 8: return NodeKind::Hex64;
default: return NodeKind::Hex32;
}
}
// ── Helper: read the leaf kind from the start of LF_UNION.data ──
// (LF_UNION lacks the lfEasy member that LF_CLASS has)
static TRK unionLeafKind(const char* data) {
return *reinterpret_cast<const TRK*>(data);
}
// ── Import context ──
struct PdbCtx {
NodeTree tree;
const TypeTable* tt = nullptr;
QHash<uint32_t, uint64_t> typeCache; // typeIndex → nodeId
uint64_t importUDT(uint32_t typeIndex);
void importFieldList(uint32_t fieldListIndex, uint64_t parentId);
void importMemberType(uint32_t typeIndex, int offset, const QString& name, uint64_t parentId);
// Resolve LF_MODIFIER chain to underlying type index
uint32_t unwrapModifier(uint32_t typeIndex) const {
if (typeIndex < tt->firstIndex()) return typeIndex;
const auto* rec = tt->get(typeIndex);
if (!rec) return typeIndex;
if (rec->header.kind == TRK::LF_MODIFIER)
return rec->data.LF_MODIFIER.type;
return typeIndex;
}
};
uint64_t PdbCtx::importUDT(uint32_t typeIndex) {
if (typeIndex < tt->firstIndex()) return 0;
auto it = typeCache.find(typeIndex);
if (it != typeCache.end()) return it.value();
const auto* rec = tt->get(typeIndex);
if (!rec) return 0;
const char* name = nullptr;
uint32_t fieldListIndex = 0;
uint16_t fieldCount = 0;
bool isUnion = false;
const char* sizeData = nullptr;
if (rec->header.kind == TRK::LF_STRUCTURE || rec->header.kind == TRK::LF_CLASS) {
// Skip forward references — find the definition
if (rec->data.LF_CLASS.property.fwdref) return 0;
fieldCount = rec->data.LF_CLASS.count;
fieldListIndex = rec->data.LF_CLASS.field;
sizeData = rec->data.LF_CLASS.data;
name = leafName(sizeData, rec->data.LF_CLASS.lfEasy.kind);
} else if (rec->header.kind == TRK::LF_UNION) {
if (rec->data.LF_UNION.property.fwdref) return 0;
isUnion = true;
fieldCount = rec->data.LF_UNION.count;
fieldListIndex = rec->data.LF_UNION.field;
sizeData = rec->data.LF_UNION.data;
name = leafName(sizeData, unionLeafKind(sizeData));
} else {
return 0;
}
(void)fieldCount;
QString qname = name ? QString::fromUtf8(name) : QStringLiteral("<anon>");
Node s;
s.kind = NodeKind::Struct;
s.name = qname;
s.structTypeName = qname;
s.classKeyword = isUnion ? QStringLiteral("union") : QStringLiteral("struct");
s.parentId = 0;
s.collapsed = true;
int idx = tree.addNode(s);
uint64_t nodeId = tree.nodes[idx].id;
typeCache[typeIndex] = nodeId;
importFieldList(fieldListIndex, nodeId);
return nodeId;
}
void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) {
const auto* rec = tt->get(fieldListIndex);
if (!rec || rec->header.kind != TRK::LF_FIELDLIST) return;
auto maximumSize = rec->header.size - sizeof(uint16_t);
QSet<QPair<int,int>> bitfieldSlots;
for (size_t i = 0; i < maximumSize; ) {
auto* field = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(
reinterpret_cast<const uint8_t*>(&rec->data.LF_FIELD.list) + i);
if (field->kind == TRK::LF_MEMBER) {
// Extract offset from variable-length leaf
uint16_t offset = 0;
if (field->data.LF_MEMBER.lfEasy.kind < TRK::LF_NUMERIC)
offset = *reinterpret_cast<const uint16_t*>(field->data.LF_MEMBER.offset);
else
offset = static_cast<uint16_t>(leafValue(field->data.LF_MEMBER.offset,
field->data.LF_MEMBER.lfEasy.kind));
const char* memberName = leafName(field->data.LF_MEMBER.offset,
field->data.LF_MEMBER.lfEasy.kind);
uint32_t memberType = field->data.LF_MEMBER.index;
QString qname = memberName ? QString::fromUtf8(memberName) : QString();
// Check for bitfield type
uint32_t resolvedType = unwrapModifier(memberType);
const auto* typeRec = tt->get(resolvedType);
if (typeRec && typeRec->header.kind == TRK::LF_BITFIELD) {
uint32_t underlying = typeRec->data.LF_BITFIELD.type;
uint8_t bitLen = typeRec->data.LF_BITFIELD.length;
(void)bitLen;
// Determine slot size from underlying type
uint64_t slotSize = 4;
if (underlying < tt->firstIndex()) {
NodeKind k = mapPrimitiveType(underlying);
slotSize = sizeForKind(k);
}
auto key = qMakePair((int)offset, (int)slotSize);
if (!bitfieldSlots.contains(key)) {
bitfieldSlots.insert(key);
Node n;
n.kind = hexForSize(slotSize);
n.name = qname;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
}
} else {
importMemberType(memberType, offset, qname, parentId);
}
// Advance past this LF_MEMBER
i += static_cast<size_t>(memberName - reinterpret_cast<const char*>(field));
i += strnlen(memberName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3); // align to 4
}
else if (field->kind == TRK::LF_BCLASS) {
const char* leafEnd = leafName(field->data.LF_BCLASS.offset,
field->data.LF_BCLASS.lfEasy.kind);
i += static_cast<size_t>(leafEnd - reinterpret_cast<const char*>(field));
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_VBCLASS || field->kind == TRK::LF_IVBCLASS) {
TRK vbpKind = *reinterpret_cast<const TRK*>(field->data.LF_IVBCLASS.vbpOffset);
uint8_t vbpSize1 = leafSize(vbpKind);
TRK vbtKind = *reinterpret_cast<const TRK*>(field->data.LF_IVBCLASS.vbpOffset + vbpSize1);
uint8_t vbpSize2 = leafSize(vbtKind);
i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_VBCLASS) + vbpSize1 + vbpSize2;
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_INDEX) {
// Continuation of field list in another record
importFieldList(field->data.LF_INDEX.type, parentId);
i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_INDEX);
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_VFUNCTAB) {
i += sizeof(PDB::CodeView::TPI::FieldList::Data::LF_VFUNCTAB);
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_NESTTYPE) {
const char* nestName = field->data.LF_NESTTYPE.name;
i += static_cast<size_t>(nestName - reinterpret_cast<const char*>(field));
i += strnlen(nestName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_STMEMBER) {
const char* smName = field->data.LF_STMEMBER.name;
i += static_cast<size_t>(smName - reinterpret_cast<const char*>(field));
i += strnlen(smName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_METHOD) {
const char* mName = field->data.LF_METHOD.name;
i += static_cast<size_t>(mName - reinterpret_cast<const char*>(field));
i += strnlen(mName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_ONEMETHOD) {
// Determine if it has a vbaseoff field
auto prop = static_cast<PDB::CodeView::TPI::MethodProperty>(
field->data.LF_ONEMETHOD.attributes.mprop);
const char* mName;
if (prop == PDB::CodeView::TPI::MethodProperty::Intro ||
prop == PDB::CodeView::TPI::MethodProperty::PureIntro)
mName = reinterpret_cast<const char*>(field->data.LF_ONEMETHOD.vbaseoff) + sizeof(uint32_t);
else
mName = reinterpret_cast<const char*>(field->data.LF_ONEMETHOD.vbaseoff);
i += static_cast<size_t>(mName - reinterpret_cast<const char*>(field));
i += strnlen(mName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
else if (field->kind == TRK::LF_ENUMERATE) {
const char* eName = leafName(field->data.LF_ENUMERATE.value,
field->data.LF_ENUMERATE.lfEasy.kind);
i += static_cast<size_t>(eName - reinterpret_cast<const char*>(field));
i += strnlen(eName, maximumSize - i - 1) + 1;
i = (i + 3) & ~size_t(3);
}
else {
break; // unknown field kind, stop
}
}
}
void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& name, uint64_t parentId) {
// Handle primitive type indices (< 0x1000)
if (typeIndex < tt->firstIndex()) {
uint32_t ptrMode = (typeIndex >> 8) & 0xF;
if (ptrMode == 0x04 || ptrMode == 0x05) {
// 32-bit pointer to a base type
Node n;
n.kind = NodeKind::Pointer32;
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.collapsed = true;
tree.addNode(n);
return;
}
if (ptrMode == 0x06) {
// 64-bit pointer to a base type
Node n;
n.kind = NodeKind::Pointer64;
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.collapsed = true;
tree.addNode(n);
return;
}
if (ptrMode != 0x00) {
// Some other pointer mode (near, far, huge) — treat as 32-bit
Node n;
n.kind = NodeKind::Pointer32;
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.collapsed = true;
tree.addNode(n);
return;
}
// Direct base type
Node n;
n.kind = mapPrimitiveType(typeIndex);
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
return;
}
const auto* rec = tt->get(typeIndex);
if (!rec) {
Node n;
n.kind = NodeKind::Hex32;
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
return;
}
switch (rec->header.kind) {
case TRK::LF_MODIFIER:
importMemberType(rec->data.LF_MODIFIER.type, offset, name, parentId);
break;
case TRK::LF_POINTER: {
uint32_t ptrSize = rec->data.LF_POINTER.attr.size;
uint32_t pointee = rec->data.LF_POINTER.utype;
// Unwrap modifier on pointee
uint32_t realPointee = unwrapModifier(pointee);
Node n;
n.kind = (ptrSize <= 4) ? NodeKind::Pointer32 : NodeKind::Pointer64;
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.collapsed = true;
// Check if pointee is a UDT
if (realPointee >= tt->firstIndex()) {
const auto* pointeeRec = tt->get(realPointee);
if (pointeeRec) {
if (pointeeRec->header.kind == TRK::LF_STRUCTURE ||
pointeeRec->header.kind == TRK::LF_CLASS ||
pointeeRec->header.kind == TRK::LF_UNION) {
// If this is a forward ref, search for the definition
uint32_t defIndex = realPointee;
bool isFwd = false;
if (pointeeRec->header.kind == TRK::LF_UNION)
isFwd = pointeeRec->data.LF_UNION.property.fwdref;
else
isFwd = pointeeRec->data.LF_CLASS.property.fwdref;
if (isFwd) {
// Need to find the non-fwdref definition by name
const char* typeName = nullptr;
if (pointeeRec->header.kind == TRK::LF_UNION)
typeName = leafName(pointeeRec->data.LF_UNION.data, unionLeafKind(pointeeRec->data.LF_UNION.data));
else
typeName = leafName(pointeeRec->data.LF_CLASS.data,
pointeeRec->data.LF_CLASS.lfEasy.kind);
if (typeName) {
// Linear scan for the definition (cached after first import)
for (uint32_t ti = tt->firstIndex(); ti < tt->lastIndex(); ti++) {
const auto* candidate = tt->get(ti);
if (!candidate) continue;
if (candidate->header.kind != pointeeRec->header.kind) continue;
bool candidateFwd;
const char* candidateName;
if (candidate->header.kind == TRK::LF_UNION) {
candidateFwd = candidate->data.LF_UNION.property.fwdref;
candidateName = leafName(candidate->data.LF_UNION.data, unionLeafKind(candidate->data.LF_UNION.data));
} else {
candidateFwd = candidate->data.LF_CLASS.property.fwdref;
candidateName = leafName(candidate->data.LF_CLASS.data,
candidate->data.LF_CLASS.lfEasy.kind);
}
if (!candidateFwd && candidateName && strcmp(candidateName, typeName) == 0) {
defIndex = ti;
break;
}
}
}
}
n.refId = importUDT(defIndex);
} else if (pointeeRec->header.kind == TRK::LF_PROCEDURE ||
pointeeRec->header.kind == TRK::LF_MFUNCTION) {
n.kind = (ptrSize <= 4) ? NodeKind::FuncPtr32 : NodeKind::FuncPtr64;
}
}
}
tree.addNode(n);
break;
}
case TRK::LF_STRUCTURE:
case TRK::LF_CLASS:
case TRK::LF_UNION: {
// Embedded struct/union
uint32_t defIndex = typeIndex;
// Handle forward reference
bool isFwd = false;
if (rec->header.kind == TRK::LF_UNION)
isFwd = rec->data.LF_UNION.property.fwdref;
else
isFwd = rec->data.LF_CLASS.property.fwdref;
if (isFwd) {
const char* typeName = nullptr;
if (rec->header.kind == TRK::LF_UNION)
typeName = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
else
typeName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
if (typeName) {
for (uint32_t ti = tt->firstIndex(); ti < tt->lastIndex(); ti++) {
const auto* candidate = tt->get(ti);
if (!candidate) continue;
if (candidate->header.kind != rec->header.kind) continue;
bool candidateFwd;
const char* candidateName;
if (candidate->header.kind == TRK::LF_UNION) {
candidateFwd = candidate->data.LF_UNION.property.fwdref;
candidateName = leafName(candidate->data.LF_UNION.data, unionLeafKind(candidate->data.LF_UNION.data));
} else {
candidateFwd = candidate->data.LF_CLASS.property.fwdref;
candidateName = leafName(candidate->data.LF_CLASS.data,
candidate->data.LF_CLASS.lfEasy.kind);
}
if (!candidateFwd && candidateName && strcmp(candidateName, typeName) == 0) {
defIndex = ti;
break;
}
}
}
}
uint64_t refId = importUDT(defIndex);
const char* typeName = nullptr;
bool isUnion = (rec->header.kind == TRK::LF_UNION);
if (isUnion)
typeName = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
else
typeName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
Node n;
n.kind = NodeKind::Struct;
n.name = name;
n.structTypeName = typeName ? QString::fromUtf8(typeName) : QString();
n.classKeyword = isUnion ? QStringLiteral("union") : QStringLiteral("struct");
n.parentId = parentId;
n.offset = offset;
n.refId = refId;
n.collapsed = true;
tree.addNode(n);
break;
}
case TRK::LF_ARRAY: {
uint32_t elemType = rec->data.LF_ARRAY.elemtype;
uint64_t totalSize = leafValue(rec->data.LF_ARRAY.data,
*reinterpret_cast<const TRK*>(rec->data.LF_ARRAY.data));
// Get element size
uint64_t elemSize = 0;
uint32_t realElemType = unwrapModifier(elemType);
if (realElemType < tt->firstIndex()) {
NodeKind ek = mapPrimitiveType(realElemType);
elemSize = sizeForKind(ek);
} else {
const auto* elemRec = tt->get(realElemType);
if (elemRec) {
if (elemRec->header.kind == TRK::LF_STRUCTURE || elemRec->header.kind == TRK::LF_CLASS) {
const char* sizeData = elemRec->data.LF_CLASS.data;
elemSize = leafValue(sizeData, elemRec->data.LF_CLASS.lfEasy.kind);
} else if (elemRec->header.kind == TRK::LF_UNION) {
const char* sizeData = elemRec->data.LF_UNION.data;
elemSize = leafValue(sizeData, *reinterpret_cast<const TRK*>(sizeData));
} else if (elemRec->header.kind == TRK::LF_POINTER) {
elemSize = elemRec->data.LF_POINTER.attr.size;
} else if (elemRec->header.kind == TRK::LF_ENUM) {
// Size of enum's underlying type
uint32_t ut = elemRec->data.LF_ENUM.utype;
if (ut < tt->firstIndex()) {
NodeKind ek = mapPrimitiveType(ut);
elemSize = sizeForKind(ek);
} else {
elemSize = 4;
}
} else if (elemRec->header.kind == TRK::LF_ARRAY) {
// Nested array — get total size
elemSize = leafValue(elemRec->data.LF_ARRAY.data,
*reinterpret_cast<const TRK*>(elemRec->data.LF_ARRAY.data));
}
}
}
int count = (elemSize > 0) ? static_cast<int>(totalSize / elemSize) : 1;
Node n;
n.kind = NodeKind::Array;
n.name = name;
n.parentId = parentId;
n.offset = offset;
n.arrayLen = count;
// Determine element kind
if (realElemType < tt->firstIndex()) {
n.elementKind = mapPrimitiveType(realElemType);
} else {
const auto* elemRec = tt->get(realElemType);
if (elemRec) {
if (elemRec->header.kind == TRK::LF_STRUCTURE ||
elemRec->header.kind == TRK::LF_CLASS ||
elemRec->header.kind == TRK::LF_UNION) {
n.elementKind = NodeKind::Struct;
n.refId = importUDT(realElemType);
const char* tn = nullptr;
if (elemRec->header.kind == TRK::LF_UNION)
tn = leafName(elemRec->data.LF_UNION.data, unionLeafKind(elemRec->data.LF_UNION.data));
else
tn = leafName(elemRec->data.LF_CLASS.data, elemRec->data.LF_CLASS.lfEasy.kind);
if (tn) n.structTypeName = QString::fromUtf8(tn);
} else if (elemRec->header.kind == TRK::LF_POINTER) {
uint32_t sz = elemRec->data.LF_POINTER.attr.size;
n.elementKind = (sz <= 4) ? NodeKind::Pointer32 : NodeKind::Pointer64;
} else {
n.elementKind = hexForSize(elemSize);
}
}
}
tree.addNode(n);
break;
}
case TRK::LF_ENUM: {
// Map enum to its underlying integer type
uint32_t utype = rec->data.LF_ENUM.utype;
Node n;
if (utype < tt->firstIndex()) {
n.kind = mapPrimitiveType(utype);
} else {
n.kind = NodeKind::UInt32; // fallback
}
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
break;
}
case TRK::LF_PROCEDURE:
case TRK::LF_MFUNCTION: {
Node n;
n.kind = NodeKind::Hex64;
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
break;
}
case TRK::LF_BITFIELD: {
uint32_t underlying = rec->data.LF_BITFIELD.type;
uint64_t slotSize = 4;
if (underlying < tt->firstIndex()) {
NodeKind k = mapPrimitiveType(underlying);
slotSize = sizeForKind(k);
}
Node n;
n.kind = hexForSize(slotSize);
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
break;
}
default: {
// Unknown complex type — emit as Hex32
Node n;
n.kind = NodeKind::Hex32;
n.name = name;
n.parentId = parentId;
n.offset = offset;
tree.addNode(n);
break;
}
}
}
// ── Helper: open PDB and build type table ──
struct PdbFile {
MappedFile mapped;
PDB::RawFile* rawFile = nullptr;
PDB::TPIStream* tpiStream = nullptr;
TypeTable* typeTable = nullptr;
~PdbFile() {
delete typeTable;
delete tpiStream;
delete rawFile;
}
bool open(const QString& pdbPath, QString* errorMsg) {
auto setErr = [&](const QString& msg) { if (errorMsg) *errorMsg = msg; };
if (!QFile::exists(pdbPath)) {
setErr(QStringLiteral("PDB file not found: ") + pdbPath);
return false;
}
if (!mapped.open(pdbPath)) {
setErr(QStringLiteral("Failed to memory-map PDB file: ") + pdbPath);
return false;
}
if (PDB::ValidateFile(mapped.base, mapped.size) != PDB::ErrorCode::Success) {
setErr(QStringLiteral("Invalid PDB file: ") + pdbPath);
return false;
}
rawFile = new PDB::RawFile(PDB::CreateRawFile(mapped.base));
if (PDB::HasValidTPIStream(*rawFile) != PDB::ErrorCode::Success) {
setErr(QStringLiteral("PDB has no valid TPI stream: ") + pdbPath);
return false;
}
tpiStream = new PDB::TPIStream(PDB::CreateTPIStream(*rawFile));
typeTable = new TypeTable(*tpiStream);
return true;
}
};
// ── Public API: enumeratePdbTypes ──
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg) {
PdbFile pdb;
if (!pdb.open(pdbPath, errorMsg)) return {};
const TypeTable& tt = *pdb.typeTable;
QVector<PdbTypeInfo> result;
for (uint32_t ti = tt.firstIndex(); ti < tt.lastIndex(); ti++) {
const auto* rec = tt.get(ti);
if (!rec) continue;
bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE ||
rec->header.kind == TRK::LF_CLASS ||
rec->header.kind == TRK::LF_UNION);
if (!isUDT) continue;
const char* name = nullptr;
uint16_t fieldCount = 0;
bool isUnion = false;
uint64_t size = 0;
if (rec->header.kind == TRK::LF_UNION) {
if (rec->data.LF_UNION.property.fwdref) continue;
isUnion = true;
fieldCount = rec->data.LF_UNION.count;
const char* sizeData = rec->data.LF_UNION.data;
TRK sizeKind = *reinterpret_cast<const TRK*>(sizeData);
size = leafValue(sizeData, sizeKind);
name = leafName(sizeData, sizeKind);
} else {
if (rec->data.LF_CLASS.property.fwdref) continue;
fieldCount = rec->data.LF_CLASS.count;
const char* sizeData = rec->data.LF_CLASS.data;
size = leafValue(sizeData, rec->data.LF_CLASS.lfEasy.kind);
name = leafName(sizeData, rec->data.LF_CLASS.lfEasy.kind);
}
if (!name || name[0] == '\0') continue;
// Skip anonymous types with compiler-generated names
if (name[0] == '<') continue;
PdbTypeInfo info;
info.typeIndex = ti;
info.name = QString::fromUtf8(name);
info.size = size;
info.childCount = fieldCount;
info.isUnion = isUnion;
result.append(info);
}
return result;
}
// ── Public API: importPdbSelected ──
NodeTree importPdbSelected(const QString& pdbPath,
const QVector<uint32_t>& typeIndices,
QString* errorMsg,
ProgressCb progressCb) {
PdbFile pdb;
if (!pdb.open(pdbPath, errorMsg)) return {};
PdbCtx ctx;
ctx.tt = pdb.typeTable;
int total = typeIndices.size();
for (int i = 0; i < total; i++) {
ctx.importUDT(typeIndices[i]);
if (progressCb && !progressCb(i + 1, total)) {
if (errorMsg) *errorMsg = QStringLiteral("Import cancelled");
return ctx.tree; // return partial result
}
}
if (ctx.tree.nodes.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("No types imported");
}
return ctx.tree;
}
// ── Public API: importPdb (legacy) ──
NodeTree importPdb(const QString& pdbPath, const QString& structFilter, QString* errorMsg) {
PdbFile pdb;
if (!pdb.open(pdbPath, errorMsg)) return {};
const TypeTable& tt = *pdb.typeTable;
PdbCtx ctx;
ctx.tt = &tt;
for (uint32_t ti = tt.firstIndex(); ti < tt.lastIndex(); ti++) {
const auto* rec = tt.get(ti);
if (!rec) continue;
bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE ||
rec->header.kind == TRK::LF_CLASS ||
rec->header.kind == TRK::LF_UNION);
if (!isUDT) continue;
bool fwdref = false;
const char* name = nullptr;
if (rec->header.kind == TRK::LF_UNION) {
fwdref = rec->data.LF_UNION.property.fwdref;
name = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
} else {
fwdref = rec->data.LF_CLASS.property.fwdref;
name = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
}
if (fwdref) continue;
if (!name) continue;
if (!structFilter.isEmpty()) {
if (QString::fromUtf8(name) != structFilter) continue;
}
ctx.importUDT(ti);
// If filtering to a single struct, stop after finding it
if (!structFilter.isEmpty()) break;
}
if (ctx.tree.nodes.isEmpty()) {
if (!structFilter.isEmpty()) {
if (errorMsg) *errorMsg = QStringLiteral("Type '") + structFilter +
QStringLiteral("' not found in PDB");
} else {
if (errorMsg) *errorMsg = QStringLiteral("No types found in PDB");
}
}
return ctx.tree;
}
} // namespace rcx
#else // !_WIN32
namespace rcx {
QVector<PdbTypeInfo> enumeratePdbTypes(const QString&, QString* errorMsg) {
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
return {};
}
NodeTree importPdbSelected(const QString&, const QVector<uint32_t>&,
QString* errorMsg, ProgressCb) {
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
return {};
}
NodeTree importPdb(const QString&, const QString&, QString* errorMsg) {
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
return {};
}
} // namespace rcx
#endif

34
src/imports/import_pdb.h Normal file
View File

@@ -0,0 +1,34 @@
#pragma once
#include "core.h"
#include <QVector>
#include <functional>
namespace rcx {
struct PdbTypeInfo {
uint32_t typeIndex; // TPI type index
QString name; // struct/class/union name
uint64_t size; // sizeof in bytes
int childCount; // direct member count
bool isUnion; // union vs struct/class
};
// Phase 1: Enumerate all UDT types in the PDB (fast scan, no recursive import).
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath,
QString* errorMsg = nullptr);
// Phase 2: Import selected types with full recursive child types.
// progressCb is called with (current, total) for each top-level type;
// return false from the callback to cancel the import.
using ProgressCb = std::function<bool(int current, int total)>;
NodeTree importPdbSelected(const QString& pdbPath,
const QVector<uint32_t>& typeIndices,
QString* errorMsg = nullptr,
ProgressCb progressCb = {});
// Legacy single-call API: import one struct by name (or all if filter empty).
NodeTree importPdb(const QString& pdbPath,
const QString& structFilter = {},
QString* errorMsg = nullptr);
} // namespace rcx

View 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

View 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

View File

@@ -0,0 +1,388 @@
#include "import_reclass_xml.h"
#include <QFile>
#include <QXmlStreamReader>
#include <QHash>
#include <QVector>
#include <QDebug>
namespace rcx {
// ── Version-specific type maps ──
// Maps XML Type attribute (integer) → NodeKind.
// Entries with no rcx equivalent use Hex8 as fallback.
enum class XmlVersion { V2013, V2016 };
// 2016 / ReClassEx / MemeClsEx type map (35 entries, index = XML Type value)
static const struct { int xmlType; NodeKind kind; } kTypeMap2016[] = {
// 0: null (unused)
{ 1, NodeKind::Struct }, // ClassInstance
// 2,3: null
{ 4, NodeKind::Hex32 },
{ 5, NodeKind::Hex64 },
{ 6, NodeKind::Hex16 },
{ 7, NodeKind::Hex8 },
{ 8, NodeKind::Pointer64 }, // ClassPointer
{ 9, NodeKind::Int64 },
{ 10, NodeKind::Int32 },
{ 11, NodeKind::Int16 },
{ 12, NodeKind::Int8 },
{ 13, NodeKind::Float },
{ 14, NodeKind::Double },
{ 15, NodeKind::UInt32 },
{ 16, NodeKind::UInt16 },
{ 17, NodeKind::UInt8 },
{ 18, NodeKind::UTF8 }, // UTF8Text
{ 19, NodeKind::UTF16 }, // UTF16Text
{ 20, NodeKind::Pointer64 }, // FunctionPtr
{ 21, NodeKind::Hex8 }, // Custom (expanded by Size)
{ 22, NodeKind::Vec2 },
{ 23, NodeKind::Vec3 },
{ 24, NodeKind::Vec4 },
{ 25, NodeKind::Mat4x4 },
{ 26, NodeKind::Pointer64 }, // VTable
{ 27, NodeKind::Array }, // ClassInstanceArray
// 28: null (used for Class elements, not nodes)
{ 29, NodeKind::Pointer64 }, // UTF8TextPtr
{ 30, NodeKind::Pointer64 }, // UTF16TextPtr
// 31: BitField → UInt8 fallback
{ 31, NodeKind::UInt8 },
{ 32, NodeKind::UInt64 },
{ 33, NodeKind::Pointer64 }, // Function
};
// 2013 / ReClass 2011 type map (31 entries)
static const struct { int xmlType; NodeKind kind; } kTypeMap2013[] = {
{ 1, NodeKind::Struct }, // ClassInstance
{ 4, NodeKind::Hex32 },
{ 5, NodeKind::Hex16 },
{ 6, NodeKind::Hex8 },
{ 7, NodeKind::Pointer64 }, // ClassPointer
{ 8, NodeKind::Int32 },
{ 9, NodeKind::Int16 },
{ 10, NodeKind::Int8 },
{ 11, NodeKind::Float },
{ 12, NodeKind::UInt32 },
{ 13, NodeKind::UInt16 },
{ 14, NodeKind::UInt8 },
{ 15, NodeKind::UTF8 }, // UTF8Text
{ 16, NodeKind::Pointer64 }, // FunctionPtr
{ 17, NodeKind::Hex8 }, // Custom
{ 18, NodeKind::Vec2 },
{ 19, NodeKind::Vec3 },
{ 20, NodeKind::Vec4 },
{ 21, NodeKind::Mat4x4 },
{ 22, NodeKind::Pointer64 }, // VTable
{ 23, NodeKind::Array }, // ClassInstanceArray
{ 27, NodeKind::Int64 },
{ 28, NodeKind::Double },
{ 29, NodeKind::UTF16 }, // UTF16Text
{ 30, NodeKind::Array }, // ClassPointerArray
};
static NodeKind lookupKind(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) {
for (const auto& e : kTypeMap2016)
if (e.xmlType == xmlType) return e.kind;
} else {
for (const auto& e : kTypeMap2013)
if (e.xmlType == xmlType) return e.kind;
}
return NodeKind::Hex8; // fallback
}
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
static bool isPointerType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016)
return xmlType == 8 || xmlType == 20 || xmlType == 26 || xmlType == 29 || xmlType == 30 || xmlType == 33;
else
return xmlType == 7 || xmlType == 16 || xmlType == 22;
}
// Is this XML type a ClassInstance (embedded struct)?
static bool isClassInstanceType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 1;
else return xmlType == 1;
}
// Is this XML type a ClassInstanceArray?
static bool isClassInstanceArrayType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 27;
else return xmlType == 23 || xmlType == 30;
}
// Is this XML type a text node?
static bool isTextType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 18 || xmlType == 19;
else return xmlType == 15 || xmlType == 29;
}
// Is this XML type a UTF16 text node?
static bool isUtf16TextType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 19;
else return xmlType == 29;
}
// Is this XML type a Custom node (expanded to hex)?
static bool isCustomType(int xmlType, XmlVersion ver) {
if (ver == XmlVersion::V2016) return xmlType == 21;
else return xmlType == 17;
}
// Deferred pointer resolution entry
struct PendingRef {
uint64_t nodeId;
QString className;
};
NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
qDebug() << "[ImportXML] Opening file:" << filePath;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "[ImportXML] ERROR: Cannot open file";
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file: ") + filePath;
return {};
}
qDebug() << "[ImportXML] File size:" << file.size() << "bytes";
QXmlStreamReader xml(&file);
XmlVersion version = XmlVersion::V2016; // default to 2016 (most common)
NodeTree tree;
tree.baseAddress = 0x00400000;
// Class name → struct node ID (for pointer resolution)
QHash<QString, uint64_t> classIds;
// Deferred pointer refs to resolve after all classes are parsed
QVector<PendingRef> pendingRefs;
// Detect version from first comment
bool versionDetected = false;
while (!xml.atEnd()) {
xml.readNext();
// Detect version from XML comments
if (!versionDetected && xml.isComment()) {
QString comment = xml.text().toString().trimmed();
if (comment.contains(QStringLiteral("ReClassEx"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("MemeClsEx"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("2016"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("2015"), Qt::CaseInsensitive)) {
version = XmlVersion::V2016;
} else if (comment.contains(QStringLiteral("2013"), Qt::CaseInsensitive) ||
comment.contains(QStringLiteral("2011"), Qt::CaseInsensitive)) {
version = XmlVersion::V2013;
}
// else keep default V2016
versionDetected = true;
qDebug() << "[ImportXML] Detected version:" << (version == XmlVersion::V2016 ? "V2016" : "V2013");
}
if (!xml.isStartElement()) continue;
if (xml.name() == QStringLiteral("Class")) {
// Parse a class element into a root Struct node
QString className = xml.attributes().value(QStringLiteral("Name")).toString();
QString strOffset = xml.attributes().value(QStringLiteral("strOffset")).toString();
// Create root struct node (collapsed by default for large files)
Node structNode;
structNode.kind = NodeKind::Struct;
structNode.name = className;
structNode.structTypeName = className;
structNode.parentId = 0; // root level
structNode.offset = 0;
structNode.collapsed = true;
int structIdx = tree.addNode(structNode);
uint64_t structId = tree.nodes[structIdx].id;
classIds[className] = structId;
qDebug() << "[ImportXML] Class:" << className << "id:" << structId;
// Parse child Node elements
int childOffset = 0;
while (!xml.atEnd()) {
xml.readNext();
if (xml.isEndElement() && xml.name() == QStringLiteral("Class"))
break;
if (!xml.isStartElement() || xml.name() != QStringLiteral("Node"))
continue;
int xmlType = xml.attributes().value(QStringLiteral("Type")).toInt();
QString nodeName = xml.attributes().value(QStringLiteral("Name")).toString();
int nodeSize = xml.attributes().value(QStringLiteral("Size")).toInt();
QString ptrClass = xml.attributes().value(QStringLiteral("Pointer")).toString();
QString instClass = xml.attributes().value(QStringLiteral("Instance")).toString();
qDebug() << "[ImportXML] Node:" << nodeName << "type:" << xmlType
<< "size:" << nodeSize << "ptr:" << ptrClass << "inst:" << instClass;
// Handle Custom type: expand to appropriate hex nodes
if (isCustomType(xmlType, version) && nodeSize > 0) {
// Pick best-fit hex kind
NodeKind hexKind;
int hexSize;
if (nodeSize >= 8 && nodeSize % 8 == 0) {
hexKind = NodeKind::Hex64; hexSize = 8;
} else if (nodeSize >= 4 && nodeSize % 4 == 0) {
hexKind = NodeKind::Hex32; hexSize = 4;
} else if (nodeSize >= 2 && nodeSize % 2 == 0) {
hexKind = NodeKind::Hex16; hexSize = 2;
} else {
hexKind = NodeKind::Hex8; hexSize = 1;
}
int count = nodeSize / hexSize;
for (int i = 0; i < count; i++) {
Node n;
n.kind = hexKind;
n.name = (count == 1) ? nodeName : QString();
n.parentId = structId;
n.offset = childOffset;
tree.addNode(n);
childOffset += hexSize;
}
continue;
}
NodeKind kind = lookupKind(xmlType, version);
// Handle ClassInstanceArray: read child <Array> element
if (isClassInstanceArrayType(xmlType, version)) {
qDebug() << "[ImportXML] -> ClassInstanceArray";
int total = xml.attributes().value(QStringLiteral("Total")).toInt();
if (total <= 0)
total = xml.attributes().value(QStringLiteral("Count")).toInt();
if (total <= 0) total = 1;
// Read child <Array> element for class name
QString arrayClassName;
while (!xml.atEnd()) {
xml.readNext();
if (xml.isEndElement() && xml.name() == QStringLiteral("Node"))
break;
if (xml.isStartElement() && xml.name() == QStringLiteral("Array")) {
arrayClassName = xml.attributes().value(QStringLiteral("Name")).toString();
int arrayTotal = xml.attributes().value(QStringLiteral("Total")).toInt();
if (arrayTotal <= 0)
arrayTotal = xml.attributes().value(QStringLiteral("Count")).toInt();
if (arrayTotal > 0) total = arrayTotal;
}
}
// Create an Array node wrapping Struct elements
Node arrNode;
arrNode.kind = NodeKind::Array;
arrNode.name = nodeName;
arrNode.parentId = structId;
arrNode.offset = childOffset;
arrNode.arrayLen = total;
arrNode.elementKind = NodeKind::Struct;
if (!arrayClassName.isEmpty())
arrNode.structTypeName = arrayClassName;
int arrIdx = tree.addNode(arrNode);
uint64_t arrId = tree.nodes[arrIdx].id;
// Defer ref resolution if array references a class
if (!arrayClassName.isEmpty()) {
pendingRefs.append({arrId, arrayClassName});
}
childOffset += nodeSize > 0 ? nodeSize : 0;
continue;
}
Node n;
n.kind = kind;
n.name = nodeName;
n.parentId = structId;
n.offset = childOffset;
// Handle text nodes
if (isTextType(xmlType, version)) {
if (isUtf16TextType(xmlType, version))
n.strLen = qMax(1, nodeSize / 2);
else
n.strLen = qMax(1, nodeSize);
}
// Handle pointer types
if (isPointerType(xmlType, version) && !ptrClass.isEmpty()) {
qDebug() << "[ImportXML] -> Pointer to class:" << ptrClass;
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
pendingRefs.append({nodeId, ptrClass});
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
continue;
}
// Handle embedded class instance
if (isClassInstanceType(xmlType, version)) {
QString resolvedClass = instClass.isEmpty() ? ptrClass : instClass;
qDebug() << "[ImportXML] -> ClassInstance:" << resolvedClass;
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
n.structTypeName = resolvedClass;
if (!n.structTypeName.isEmpty()) {
int nodeIdx = tree.addNode(n);
uint64_t nodeId = tree.nodes[nodeIdx].id;
pendingRefs.append({nodeId, n.structTypeName});
} else {
tree.addNode(n);
}
childOffset += nodeSize > 0 ? nodeSize : 0;
continue;
}
tree.addNode(n);
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
}
}
}
if (xml.hasError() && xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
qDebug() << "[ImportXML] XML parse error at line" << xml.lineNumber() << ":" << xml.errorString();
if (errorMsg)
*errorMsg = QStringLiteral("XML parse error at line %1: %2")
.arg(xml.lineNumber())
.arg(xml.errorString());
return {};
}
qDebug() << "[ImportXML] Parsing complete. Total nodes:" << tree.nodes.size()
<< "classes:" << classIds.size() << "pending refs:" << pendingRefs.size();
if (tree.nodes.isEmpty()) {
qDebug() << "[ImportXML] ERROR: No classes found";
if (errorMsg) *errorMsg = QStringLiteral("No classes found in file");
return {};
}
// Resolve deferred pointer/struct references
int resolved = 0, unresolved = 0;
for (const auto& ref : pendingRefs) {
int nodeIdx = tree.indexOfId(ref.nodeId);
if (nodeIdx < 0) continue;
auto it = classIds.find(ref.className);
if (it != classIds.end()) {
tree.nodes[nodeIdx].refId = it.value();
tree.invalidateIdCache();
resolved++;
} else {
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
unresolved++;
}
}
qDebug() << "[ImportXML] Refs resolved:" << resolved << "unresolved:" << unresolved;
qDebug() << "[ImportXML] Import complete. Returning tree with" << tree.nodes.size() << "nodes";
return tree;
}
} // namespace rcx

View File

@@ -0,0 +1,11 @@
#pragma once
#include "core.h"
namespace rcx {
// Import a ReClass XML file (.reclass, .MemeCls, etc.) into a NodeTree.
// Supports ReClassEx, MemeClsEx, ReClass 2011/2013/2016 XML formats.
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr);
} // namespace rcx

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
#pragma once
#include "core.h"
namespace rcx {
// Import C/C++ struct definitions from source code into a NodeTree.
// Supports two modes (auto-detected):
// 1. With comment offsets (// 0xNN) - trusts the offset values
// 2. Without comment offsets - computes offsets from type sizes
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr);
} // namespace rcx

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,8 @@
#include <QTreeView>
#include <QStandardItemModel>
#include <QMap>
#include <QButtonGroup>
#include <QPushButton>
#include <Qsci/qsciscintilla.h>
namespace rcx {
@@ -25,13 +27,14 @@ public:
explicit MainWindow(QWidget* parent = nullptr);
private slots:
void newFile();
void newDocument();
void newClass();
void newStruct();
void newEnum();
void selfTest();
void openFile();
void saveFile();
void saveFileAs();
void closeFile();
void addNode();
void removeNode();
@@ -47,13 +50,17 @@ private slots:
void toggleMcp();
void setEditorFont(const QString& fontName);
void exportCpp();
void exportReclassXmlAction();
void importFromSource();
void importReclassXml();
void importPdb();
void showTypeAliasesDialog();
void editTheme();
void showOptionsDialog();
public:
// Project Lifecycle API
QMdiSubWindow* project_new();
QMdiSubWindow* project_new(const QString& classKeyword = QString());
QMdiSubWindow* project_open(const QString& path = {});
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
void project_close(QMdiSubWindow* sub = nullptr);
@@ -63,11 +70,15 @@ private:
QMdiArea* m_mdiArea;
QLabel* m_statusLabel;
QButtonGroup* m_viewBtnGroup = nullptr;
QPushButton* m_btnReclass = nullptr;
QPushButton* m_btnRendered = nullptr;
TitleBarWidget* m_titleBar = nullptr;
QWidget* m_borderOverlay = nullptr;
PluginManager m_pluginManager;
McpBridge* m_mcp = nullptr;
QAction* m_mcpAction = nullptr;
QMenu* m_sourceMenu = nullptr;
struct SplitPane {
QTabWidget* tabWidget = nullptr;
@@ -85,11 +96,13 @@ private:
int activePaneIdx = 0;
};
QMap<QMdiSubWindow*, TabState> m_tabs;
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
void rebuildAllDocs();
void createMenus();
void createStatusBar();
void showPluginsDialog();
void populateSourceMenu();
QIcon makeIcon(const QString& svgPath);
RcxController* activeController() const;
@@ -107,8 +120,8 @@ private:
SplitPane createSplitPane(TabState& tab);
void applyTheme(const Theme& theme);
void applyTabWidgetStyle(QTabWidget* tw);
void styleTabCloseButtons();
void syncViewButtons(ViewMode mode);
SplitPane* findPaneByTabWidget(QTabWidget* tw);
SplitPane* findActiveSplitPane();
RcxEditor* activePaneEditor();
@@ -117,6 +130,8 @@ private:
QDockWidget* m_workspaceDock = nullptr;
QTreeView* m_workspaceTree = nullptr;
QStandardItemModel* m_workspaceModel = nullptr;
QLabel* m_dockTitleLabel = nullptr;
QToolButton* m_dockCloseBtn = nullptr;
void createWorkspaceDock();
void rebuildWorkspaceModel();
void updateBorderColor(const QColor& color);

View File

@@ -287,7 +287,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
{"name", "hex.read"},
{"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type "
"interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). "
"Offset is provider-relative (0-based) unless baseRelative=true."},
"Offset is tree-relative (0-based, baseAddress added automatically) "
"unless baseRelative=true (offset is absolute)."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
@@ -793,7 +794,7 @@ QJsonObject McpBridge::toolSourceSwitch(const QJsonObject& args) {
}
if (args.contains("pid")) {
uint32_t pid = (uint32_t)args.value("pid").toInteger();
uint32_t pid = (uint32_t)args.value("pid").toInt();
QString name = args.value("processName").toString();
if (name.isEmpty()) name = QString("PID %1").arg(pid);
QString target = QString("%1:%2").arg(pid).arg(name);
@@ -825,8 +826,8 @@ QJsonObject McpBridge::toolHexRead(const QJsonObject& args) {
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
int length = qMin(args.value("length").toInt(64), 4096);
if (args.value("baseRelative").toBool())
offset -= (int64_t)tab->doc->tree.baseAddress;
if (!args.value("baseRelative").toBool())
offset += (int64_t)tab->doc->tree.baseAddress;
if (offset < 0 || !prov->isReadable((uint64_t)offset, length))
return makeTextResult("Cannot read at offset " + QString::number(offset), true);
@@ -907,8 +908,8 @@ QJsonObject McpBridge::toolHexWrite(const QJsonObject& args) {
int64_t offset = static_cast<int64_t>(args.value("offset").toDouble());
QString hexStr = args.value("hexBytes").toString().remove(' ');
if (args.value("baseRelative").toBool())
offset -= (int64_t)doc->tree.baseAddress;
if (!args.value("baseRelative").toBool())
offset += (int64_t)doc->tree.baseAddress;
if (hexStr.size() % 2 != 0)
return makeTextResult("Hex string must have even length", true);

View File

@@ -8,8 +8,6 @@
#include <QGroupBox>
#include <QLabel>
#include <QTreeWidgetItem>
#include <QGraphicsDropShadowEffect>
#include <QEvent>
#include <functional>
namespace rcx {
@@ -20,8 +18,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
setWindowTitle("Options");
setFixedSize(700, 450);
const auto& t = ThemeManager::instance().current();
auto* mainLayout = new QVBoxLayout(this);
mainLayout->setSpacing(8);
mainLayout->setContentsMargins(10, 10, 10, 10);
@@ -62,6 +58,29 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
generalLayout->setContentsMargins(0, 0, 0, 0);
generalLayout->setSpacing(8);
// Refresh Rate group box
auto* refreshGroup = new QGroupBox("Refresh Rate");
auto* refreshLayout = new QFormLayout(refreshGroup);
refreshLayout->setSpacing(8);
refreshLayout->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
m_refreshSpin = new QSpinBox;
m_refreshSpin->setRange(1, 60000);
m_refreshSpin->setSingleStep(50);
m_refreshSpin->setValue(current.refreshMs);
m_refreshSpin->setSuffix(" ms");
m_refreshSpin->setObjectName("refreshSpin");
refreshLayout->addRow("Interval:", m_refreshSpin);
auto* refreshDesc = new QLabel(
"How often live memory is re-read and the view is updated, in milliseconds. "
"Lower values give faster updates but use more CPU. Default: 660 ms.");
refreshDesc->setWordWrap(true);
refreshDesc->setContentsMargins(0, 0, 0, 0);
refreshLayout->addRow(refreshDesc);
generalLayout->addWidget(refreshGroup);
// Visual Experience group box
auto* visualGroup = new QGroupBox("Visual Experience");
auto* visualLayout = new QFormLayout(visualGroup);
@@ -87,6 +106,10 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
m_titleCaseCheck->setChecked(current.menuBarTitleCase);
visualLayout->addRow(m_titleCaseCheck);
m_showIconCheck = new QCheckBox("Show icon in title bar");
m_showIconCheck->setChecked(current.showIcon);
visualLayout->addRow(m_showIconCheck);
generalLayout->addWidget(visualGroup);
// Safe Mode group box
@@ -96,8 +119,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
m_safeModeCheck = new QCheckBox("Safe Mode");
m_safeModeCheck->setChecked(current.safeMode);
m_safeModeCheck->setStyleSheet(QStringLiteral(
"QCheckBox { font-weight: bold; }"));
safeModeLayout->addWidget(m_safeModeCheck);
auto* safeModeDesc = new QLabel(
@@ -127,8 +148,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
m_autoMcpCheck = new QCheckBox("Auto-start MCP server");
m_autoMcpCheck->setChecked(current.autoStartMcp);
m_autoMcpCheck->setStyleSheet(QStringLiteral(
"QCheckBox { font-weight: bold; }"));
mcpLayout->addWidget(m_autoMcpCheck);
auto* mcpDesc = new QLabel(
@@ -144,6 +163,18 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
m_pages->addWidget(aiPage); // index 1
m_pageKeywords[aiItem] = collectPageKeywords(aiPage);
// -- Generator page --
auto* generatorItem = new QTreeWidgetItem(envItem, {"Generator"});
auto* generatorPage = new QWidget;
auto* generatorLayout = new QVBoxLayout(generatorPage);
generatorLayout->setContentsMargins(0, 0, 0, 0);
generatorLayout->setSpacing(8);
generatorLayout->addStretch();
m_pages->addWidget(generatorPage); // index 2
m_pageKeywords[generatorItem] = collectPageKeywords(generatorPage);
middleLayout->addWidget(m_pages, 1);
mainLayout->addLayout(middleLayout, 1);
@@ -151,6 +182,7 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
// Tree <-> page connection
m_itemPageIndex[generalItem] = 0;
m_itemPageIndex[aiItem] = 1;
m_itemPageIndex[generatorItem] = 2;
connect(m_tree, &QTreeWidget::currentItemChanged, this,
[this](QTreeWidgetItem* item, QTreeWidgetItem*) {
if (!item) return;
@@ -165,106 +197,6 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttons);
// -- Styling --
// Combo boxes: set directly so the popup (top-level widget) inherits it
QString comboStyle = QStringLiteral(
"QComboBox {"
" background: %1; color: %2; border: 1px solid %3;"
" padding: 3px 8px; font-size: 12px;"
"}"
"QComboBox::drop-down {"
" border: none; border-left: 1px solid %3;"
" width: 20px;"
"}"
"QComboBox::down-arrow {"
" image: url(:/vsicons/chevron-down.svg);"
" width: 12px; height: 12px;"
"}"
"QComboBox QAbstractItemView {"
" background: %1; color: %2; border: 1px solid %3;"
" selection-background-color: %4;"
"}")
.arg(t.backgroundAlt.name(), t.text.name(),
t.border.name(), t.hover.name());
m_themeCombo->setStyleSheet(comboStyle);
m_fontCombo->setStyleSheet(comboStyle);
// Dialog-wide stylesheet for everything else
setStyleSheet(QStringLiteral(
"QDialog { background: %1; }"
"QLineEdit {"
" background: %2; color: %3; border: 1px solid %4;"
" padding: 4px 8px; font-size: 12px;"
"}"
"QTreeWidget {"
" background: %2; color: %3; border: 1px solid %4;"
" font-size: 12px; outline: none;"
"}"
"QTreeWidget::item { padding: 3px 0; outline: none; }"
"QTreeWidget::item:selected { background: %5; color: %3; }"
"QTreeWidget::item:hover { background: %6; }"
"QGroupBox {"
" color: %3; border: 1px solid %4;"
" margin-top: 8px; padding: 12px 8px 8px 8px;"
" font-size: 12px; font-weight: bold;"
"}"
"QGroupBox::title {"
" subcontrol-origin: margin;"
" left: 8px; padding: 0 4px;"
"}"
"QLabel { color: %3; font-size: 12px; }"
"QCheckBox { color: %3; font-size: 12px; spacing: 6px; }"
"QPushButton {"
" background: %2; color: %3; border: 1px solid %4;"
" padding: 5px 16px; min-width: 70px; font-size: 12px;"
" outline: none;"
"}"
"QPushButton:hover { background: %6; }"
"QPushButton:pressed { background: %1; }"
"QPushButton:focus { outline: none; }")
.arg(t.background.name(), // %1
t.backgroundAlt.name(), // %2
t.text.name(), // %3
t.border.name(), // %4
t.selection.name(), // %5
t.hover.name())); // %6
// Install hover shadow on interactive widgets (not buttons — they use stylesheet hover)
for (auto* w : {static_cast<QWidget*>(m_search),
static_cast<QWidget*>(m_themeCombo),
static_cast<QWidget*>(m_fontCombo),
static_cast<QWidget*>(m_titleCaseCheck),
static_cast<QWidget*>(m_safeModeCheck),
static_cast<QWidget*>(m_autoMcpCheck)})
w->installEventFilter(this);
m_shadowColor = t.text;
m_shadowColor.setAlpha(80);
}
bool OptionsDialog::eventFilter(QObject* obj, QEvent* event) {
if (event->type() == QEvent::Enter) {
auto* w = qobject_cast<QWidget*>(obj);
if (w && !w->graphicsEffect()) {
auto* shadow = new QGraphicsDropShadowEffect(w);
shadow->setBlurRadius(12);
shadow->setOffset(0, 0);
shadow->setColor(m_shadowColor);
w->setGraphicsEffect(shadow);
}
} else if (event->type() == QEvent::Leave) {
auto* w = qobject_cast<QWidget*>(obj);
if (w)
w->setGraphicsEffect(nullptr);
}
return QDialog::eventFilter(obj, event);
}
OptionsResult OptionsDialog::result() const {
@@ -272,8 +204,10 @@ OptionsResult OptionsDialog::result() const {
r.themeIndex = m_themeCombo->currentIndex();
r.fontName = m_fontCombo->currentText();
r.menuBarTitleCase = m_titleCaseCheck->isChecked();
r.showIcon = m_showIconCheck->isChecked();
r.safeMode = m_safeModeCheck->isChecked();
r.autoStartMcp = m_autoMcpCheck->isChecked();
r.refreshMs = m_refreshSpin->value();
return r;
}

View File

@@ -1,5 +1,4 @@
#pragma once
#include "themes/theme.h"
#include <QDialog>
#include <QLineEdit>
#include <QTreeWidget>
@@ -7,7 +6,7 @@
#include <QComboBox>
#include <QCheckBox>
#include <QHash>
#include <QColor>
#include <QSpinBox>
namespace rcx {
@@ -15,8 +14,10 @@ struct OptionsResult {
int themeIndex = 0;
QString fontName;
bool menuBarTitleCase = true;
bool showIcon = false;
bool safeMode = false;
bool autoStartMcp = false;
int refreshMs = 660;
};
class OptionsDialog : public QDialog {
@@ -26,9 +27,6 @@ public:
OptionsResult result() const;
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
private:
void filterTree(const QString& text);
static QStringList collectPageKeywords(QWidget* page);
@@ -39,10 +37,10 @@ private:
QComboBox* m_themeCombo = nullptr;
QComboBox* m_fontCombo = nullptr;
QCheckBox* m_titleCaseCheck = nullptr;
QCheckBox* m_showIconCheck = nullptr;
QCheckBox* m_safeModeCheck = nullptr;
QCheckBox* m_autoMcpCheck = nullptr;
QColor m_shadowColor;
QSpinBox* m_refreshSpin = nullptr;
// searchable keywords per leaf tree item
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;

View File

@@ -92,7 +92,8 @@ bool PluginManager::LoadPlugin(const QString& path)
IProviderPlugin* provider = static_cast<IProviderPlugin*>(plugin);
QString name = QString::fromStdString(plugin->Name());
QString identifier = name.toLower().replace(" ", "");
ProviderRegistry::instance().registerProvider(name, identifier, provider);
QString dllFileName = QFileInfo(path).fileName();
ProviderRegistry::instance().registerProvider(name, identifier, provider, dllFileName);
}
return true;

View File

@@ -6,7 +6,8 @@ ProviderRegistry& ProviderRegistry::instance() {
return s_instance;
}
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin) {
void ProviderRegistry::registerProvider(const QString& name, const QString& identifier,
IProviderPlugin* plugin, const QString& dllFileName) {
// Check if already registered
for (const auto& info : m_providers) {
if (info.identifier == identifier) {
@@ -14,8 +15,8 @@ void ProviderRegistry::registerProvider(const QString& name, const QString& iden
return;
}
}
m_providers.append(ProviderInfo(name, identifier, plugin));
m_providers.append(ProviderInfo(name, identifier, plugin, dllFileName));
qDebug() << "ProviderRegistry: Registered plugin provider:" << name << "(" << identifier << ")";
}

View File

@@ -25,10 +25,13 @@ public:
IProviderPlugin* plugin; // Plugin (if plugin-based)
BuiltinFactory factory; // Factory (if built-in)
bool isBuiltin;
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p)
: name(n), identifier(id), plugin(p), factory(nullptr), isBuiltin(false) {}
QString dllFileName; // Original DLL/SO filename (plugin-based only)
ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p,
const QString& dll = {})
: name(n), identifier(id), plugin(p), factory(nullptr),
isBuiltin(false), dllFileName(dll) {}
ProviderInfo(const QString& n, const QString& id, BuiltinFactory f)
: name(n), identifier(id), plugin(nullptr), factory(f), isBuiltin(true) {}
};
@@ -36,7 +39,8 @@ public:
static ProviderRegistry& instance();
// Register a plugin-based provider
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin);
void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin,
const QString& dllFileName = {});
// Register a built-in provider with a factory function
void registerBuiltinProvider(const QString& name, const QString& identifier, BuiltinFactory factory);

View File

@@ -33,10 +33,10 @@ public:
// Examples: "File", "Process", "Socket"
virtual QString kind() const { return QStringLiteral("File"); }
// Base address for providers that offset reads (e.g. process memory).
// Initial base address discovered by the provider (e.g. main module base).
// Used by the controller to set tree.baseAddress on first attach.
// For file/buffer providers this is always 0.
virtual uint64_t base() const { return 0; }
virtual void setBase(uint64_t newBase) { Q_UNUSED(newBase); }
// Resolve an absolute address to a symbol name.
// Returns empty string if no symbol is known.
@@ -47,6 +47,13 @@ public:
return {};
}
// Resolve a module/symbol name to its address (reverse of getSymbol).
// Returns 0 if the name is not found.
virtual uint64_t symbolToAddress(const QString& name) const {
Q_UNUSED(name);
return 0;
}
// --- Derived convenience (non-virtual, never override) ---
bool isValid() const { return size() > 0; }

View File

@@ -67,6 +67,9 @@ public:
QString getSymbol(uint64_t addr) const override {
return m_real ? m_real->getSymbol(addr) : QString();
}
uint64_t symbolToAddress(const QString& n) const override {
return m_real ? m_real->symbolToAddress(n) : 0;
}
bool write(uint64_t addr, const void* buf, int len) override {
if (!m_real) return false;

View File

@@ -51,5 +51,9 @@
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
<file alias="folder.svg">vsicons/folder.svg</file>
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
<file alias="server-process.svg">vsicons/server-process.svg</file>
<file alias="remote.svg">vsicons/remote.svg</file>
<file alias="plug.svg">vsicons/plug.svg</file>
<file alias="clear-all.svg">vsicons/clear-all.svg</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,32 @@
{
"name": "Mid",
"background": "#0D1219",
"backgroundAlt": "#121720",
"surface": "#161C28",
"border": "#1E2636",
"borderFocused": "#485068",
"button": "#181E2C",
"text": "#B0B8CC",
"textDim": "#505C74",
"textMuted": "#384258",
"textFaint": "#2C3448",
"hover": "#121720",
"selected": "#121720",
"selection": "#1A2038",
"syntaxKeyword": "#5688C0",
"syntaxNumber": "#90B480",
"syntaxString": "#B88060",
"syntaxComment": "#385030",
"syntaxPreproc": "#9868A8",
"syntaxType": "#8FDBFE",
"indHoverSpan": "#C09038",
"indCmdPill": "#141A26",
"indDataChanged": "#608C54",
"indHeatCold": "#B09030",
"indHeatWarm": "#C09038",
"indHeatHot": "#C83838",
"indHintGreen": "#385830",
"markerPtr": "#C83838",
"markerCycle": "#B89028",
"markerError": "#481818"
}

View File

@@ -22,6 +22,9 @@
"indHoverSpan": "#E6B450",
"indCmdPill": "#2a2a2a",
"indDataChanged": "#8fbc7a",
"indHeatCold": "#D4A945",
"indHeatWarm": "#E6B450",
"indHeatHot": "#f44747",
"indHintGreen": "#5a8248",
"markerPtr": "#f44747",
"markerCycle": "#e5a00d",

View File

@@ -22,6 +22,9 @@
"indHoverSpan": "#b180d7",
"indCmdPill": "#2d2d30",
"indDataChanged": "#8fbc7a",
"indHeatCold": "#D4A945",
"indHeatWarm": "#d69d85",
"indHeatHot": "#f44747",
"indHintGreen": "#5a8248",
"markerPtr": "#f44747",
"markerCycle": "#e5a00d",

View File

@@ -22,6 +22,9 @@
"indHoverSpan": "#AA9565",
"indCmdPill": "#2a2a2a",
"indDataChanged": "#6B959F",
"indHeatCold": "#C4A44A",
"indHeatWarm": "#AA9565",
"indHeatHot": "#A05040",
"indHintGreen": "#464646",
"markerPtr": "#6B3B21",
"markerCycle": "#AA9565",

View File

@@ -28,6 +28,9 @@ const ThemeFieldMeta kThemeFields[] = {
{"indHoverSpan", "Hover Span", "Indicators", &Theme::indHoverSpan},
{"indCmdPill", "Cmd Pill", "Indicators", &Theme::indCmdPill},
{"indDataChanged","Data Changed", "Indicators", &Theme::indDataChanged},
{"indHeatCold", "Heat Cold", "Indicators", &Theme::indHeatCold},
{"indHeatWarm", "Heat Warm", "Indicators", &Theme::indHeatWarm},
{"indHeatHot", "Heat Hot", "Indicators", &Theme::indHeatHot},
{"indHintGreen", "Hint Green", "Indicators", &Theme::indHintGreen},
{"markerPtr", "Pointer", "Markers", &Theme::markerPtr},
{"markerCycle", "Cycle", "Markers", &Theme::markerCycle},
@@ -50,6 +53,14 @@ Theme Theme::fromJson(const QJsonObject& o) {
if (o.contains(kThemeFields[i].key))
t.*kThemeFields[i].ptr = QColor(o[kThemeFields[i].key].toString());
}
// Derive heat colors from the theme's own palette when keys are absent
// cold = muted yellow, warm = hover/string amber, hot = marker red
if (!t.indHeatCold.isValid())
t.indHeatCold = QColor("#D4A945");
if (!t.indHeatWarm.isValid())
t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString;
if (!t.indHeatHot.isValid())
t.indHeatHot = t.markerPtr;
return t;
}

View File

@@ -38,7 +38,10 @@ struct Theme {
// ── Indicators ──
QColor indHoverSpan; // hover link text
QColor indCmdPill; // command row pill bg
QColor indDataChanged; // changed data values
QColor indDataChanged; // changed data values (legacy, fallback for old themes)
QColor indHeatCold; // heatmap level 1 (changed once)
QColor indHeatWarm; // heatmap level 2 (moderate changes)
QColor indHeatHot; // heatmap level 3 (frequent changes)
QColor indHintGreen; // comment/hint text
// ── Markers ──

View File

@@ -18,7 +18,11 @@ ThemeManager::ThemeManager() {
loadUserThemes();
QSettings settings("Reclass", "Reclass");
QString fallback = m_builtIn.isEmpty() ? QString() : m_builtIn[0].name;
QString fallback;
for (const auto& t : m_builtIn) {
if (t.name.contains("VS2022", Qt::CaseInsensitive)) { fallback = t.name; break; }
}
if (fallback.isEmpty() && !m_builtIn.isEmpty()) fallback = m_builtIn[0].name;
QString saved = settings.value("theme", fallback).toString();
auto all = themes();
for (int i = 0; i < all.size(); i++) {

View File

@@ -122,6 +122,8 @@ void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
clean.remove('&');
if (titleCase) {
action->setText("&" + clean.toUpper());
} else {
QString result;
bool capitalizeNext = true;
for (int i = 0; i < clean.length(); ++i) {
@@ -135,8 +137,6 @@ void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
}
}
action->setText("&" + result);
} else {
action->setText("&" + clean.toUpper());
}
}
}

View File

@@ -32,7 +32,8 @@ TypeSpec parseTypeSpec(const QString& text) {
if (s.endsWith('*')) {
spec.isPointer = true;
s.chop(1);
if (s.endsWith('*')) s.chop(1); // double pointer
spec.ptrDepth = 1;
if (s.endsWith('*')) { s.chop(1); spec.ptrDepth = 2; }
spec.baseName = s.trimmed();
return spec;
}
@@ -97,6 +98,12 @@ public:
int h = option.rect.height();
int w = option.rect.width();
// Scale metrics from font height
QFontMetrics fmMain(m_font);
int iconSz = fmMain.height(); // icon matches text height
int gutterW = fmMain.horizontalAdvance(QChar(0x25B8)) + 4;
int iconColW = iconSz + 4;
// Section: centered dim text with horizontal rules
if (isSection) {
painter->setPen(t.textDim);
@@ -133,18 +140,18 @@ public:
if (isCurrent) {
painter->setPen(t.text);
painter->setFont(m_font);
painter->drawText(QRect(x, y, 10, h), Qt::AlignCenter,
painter->drawText(QRect(x, y, gutterW, h), Qt::AlignCenter,
QString(QChar(0x25B8)));
}
}
x += 10;
x += gutterW;
// Icon 16x16 — only for composite entries
// Icon (scaled to font height) — only for composite entries
bool hasIcon = (m_filtered && row >= 0 && row < m_filtered->size()
&& (*m_filtered)[row].entryKind == TypeEntry::Composite);
if (hasIcon) {
static QIcon structIcon(QStringLiteral(":/vsicons/symbol-structure.svg"));
QPixmap pm = structIcon.pixmap(16, 16);
QPixmap pm = structIcon.pixmap(iconSz, iconSz);
if (isDisabled) {
// Paint dimmed
QPixmap dimmed(pm.size());
@@ -153,12 +160,12 @@ public:
p.setOpacity(0.35);
p.drawPixmap(0, 0, pm);
p.end();
painter->drawPixmap(x, y + (h - 16) / 2, dimmed);
painter->drawPixmap(x, y + (h - iconSz) / 2, dimmed);
} else {
structIcon.paint(painter, x, y + (h - 16) / 2, 16, 16);
structIcon.paint(painter, x, y + (h - iconSz) / 2, iconSz, iconSz);
}
}
x += 20;
x += iconColW;
// Text
QColor textColor;
@@ -273,14 +280,14 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
// Separator
{
auto* sep = new QFrame;
sep->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain);
m_separator = new QFrame;
m_separator->setFrameShape(QFrame::HLine);
m_separator->setFrameShadow(QFrame::Plain);
QPalette sepPal = pal;
sepPal.setColor(QPalette::WindowText, theme.border);
sep->setPalette(sepPal);
sep->setFixedHeight(1);
layout->addWidget(sep);
m_separator->setPalette(sepPal);
m_separator->setFixedHeight(1);
layout->addWidget(m_separator);
}
// Row 3: Modifier toggles [ plain ] [ * ] [ ** ] [ [n] ]
@@ -334,7 +341,12 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
this, [this](int id, bool checked) {
if (!checked) return;
m_arrayCountEdit->setVisible(id == 3);
if (id == 3) m_arrayCountEdit->setFocus();
if (id == 3) {
if (m_arrayCountEdit->text().trimmed().isEmpty())
m_arrayCountEdit->setText(QStringLiteral("1"));
m_arrayCountEdit->setFocus();
m_arrayCountEdit->selectAll();
}
updateModifierPreview();
});
connect(m_arrayCountEdit, &QLineEdit::textChanged,
@@ -444,28 +456,92 @@ void TypeSelectorPopup::setFont(const QFont& font) {
delegate->setFont(font);
}
void TypeSelectorPopup::applyTheme(const Theme& theme) {
QPalette pal;
pal.setColor(QPalette::Window, theme.backgroundAlt);
pal.setColor(QPalette::WindowText, theme.text);
pal.setColor(QPalette::Base, theme.background);
pal.setColor(QPalette::AlternateBase, theme.surface);
pal.setColor(QPalette::Text, theme.text);
pal.setColor(QPalette::Button, theme.button);
pal.setColor(QPalette::ButtonText, theme.text);
pal.setColor(QPalette::Highlight, theme.hover);
pal.setColor(QPalette::HighlightedText, theme.text);
setPalette(pal);
m_titleLabel->setPalette(pal);
m_filterEdit->setPalette(pal);
m_listView->setPalette(pal);
m_previewLabel->setPalette(pal);
m_arrayCountEdit->setPalette(pal);
// Separator
QPalette sepPal = pal;
sepPal.setColor(QPalette::WindowText, theme.border);
m_separator->setPalette(sepPal);
// Esc button
m_escLabel->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 2px 6px; }"
"QToolButton:hover { color: %2; }")
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
// Create button
m_createBtn->setStyleSheet(QStringLiteral(
"QToolButton { color: %1; border: none; padding: 3px 6px; }"
"QToolButton:hover { color: %2; background: %3; }")
.arg(theme.textMuted.name(), theme.text.name(), theme.hover.name()));
// Modifier toggle buttons
QString btnStyle = QStringLiteral(
"QToolButton { color: %1; background: %2; border: 1px solid %3;"
" padding: 2px 8px; border-radius: 3px; }"
"QToolButton:checked { color: %4; background: %5; border-color: %5; }"
"QToolButton:hover:!checked { background: %6; }")
.arg(theme.textDim.name(), theme.background.name(), theme.border.name(),
theme.text.name(), theme.selected.name(), theme.hover.name());
m_btnPlain->setStyleSheet(btnStyle);
m_btnPtr->setStyleSheet(btnStyle);
m_btnDblPtr->setStyleSheet(btnStyle);
m_btnArray->setStyleSheet(btnStyle);
// Preview label
m_previewLabel->setStyleSheet(QStringLiteral(
"QLabel { color: %1; padding: 1px 6px; }").arg(theme.syntaxType.name()));
}
void TypeSelectorPopup::setTitle(const QString& title) {
m_titleLabel->setText(title);
}
void TypeSelectorPopup::setMode(TypePopupMode mode) {
m_mode = mode;
// Show modifier toggles for modes where type modifiers make sense
bool showMods = (mode == TypePopupMode::FieldType
|| mode == TypePopupMode::ArrayElement);
m_modRow->setVisible(showMods);
// Reset to plain when showing
if (showMods) {
m_btnPlain->setChecked(true);
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
}
// Always reset to plain — prevents stale state from leaking across modes
// (PointerTarget hides buttons but applyFilter still reads their state)
m_btnPlain->setChecked(true);
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
}
void TypeSelectorPopup::setCurrentNodeSize(int bytes) {
m_currentNodeSize = bytes;
}
void TypeSelectorPopup::setModifier(int modId, int arrayCount) {
if (modId == 1) m_btnPtr->setChecked(true);
else if (modId == 2) m_btnDblPtr->setChecked(true);
else if (modId == 3) {
m_btnArray->setChecked(true);
m_arrayCountEdit->setText(QString::number(arrayCount));
m_arrayCountEdit->show();
} else {
m_btnPlain->setChecked(true);
}
}
void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntry* current) {
m_allTypes = types;
if (current) {
@@ -475,10 +551,8 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
m_currentEntry = TypeEntry{};
m_hasCurrent = false;
}
// Reset modifier toggles
m_btnPlain->setChecked(true);
m_arrayCountEdit->clear();
m_arrayCountEdit->hide();
// Don't reset modifier buttons here — setMode() already resets to plain,
// and setModifier() may have preselected a button between setMode/setTypes.
m_previewLabel->hide();
m_filterEdit->clear();
@@ -492,7 +566,9 @@ void TypeSelectorPopup::popup(const QPoint& globalPos) {
QString text = t.classKeyword.isEmpty()
? t.displayName
: (t.classKeyword + QStringLiteral(" ") + t.displayName);
int w = 10 + 20 + fm.horizontalAdvance(text) + 16;
int gutterW = fm.horizontalAdvance(QChar(0x25B8)) + 4;
int iconColW = fm.height() + 4;
int w = gutterW + iconColW + fm.horizontalAdvance(text) + 16;
if (w > maxTextW) maxTextW = w;
}
int popupW = qBound(280, maxTextW + 24, 500);
@@ -562,10 +638,10 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
QString filterBase = text.trimmed();
// Separate primitives and composites
// Separate primitives and composites (all types shown regardless of modifier)
QVector<TypeEntry> primitives, composites;
for (const auto& t : m_allTypes) {
if (t.entryKind == TypeEntry::Section) continue; // skip stale sections
if (t.entryKind == TypeEntry::Section) continue;
bool matchesFilter = filterBase.isEmpty()
|| t.displayName.contains(filterBase, Qt::CaseInsensitive)
|| t.classKeyword.contains(filterBase, Qt::CaseInsensitive);
@@ -577,7 +653,11 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
composites.append(t);
}
// For non-Root modes, sort primitives: same-size first, then rest
auto alphabetical = [](const TypeEntry& a, const TypeEntry& b) {
return a.displayName.compare(b.displayName, Qt::CaseInsensitive) < 0;
};
// For non-Root modes, sort primitives: same-size first, then rest — alphabetical within each group
if (m_mode != TypePopupMode::Root && m_currentNodeSize > 0 && !primitives.isEmpty()) {
QVector<TypeEntry> sameSize, other;
for (const auto& p : primitives) {
@@ -586,7 +666,11 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
else
other.append(p);
}
std::sort(sameSize.begin(), sameSize.end(), alphabetical);
std::sort(other.begin(), other.end(), alphabetical);
primitives = sameSize + other;
} else {
std::sort(primitives.begin(), primitives.end(), alphabetical);
}
// Helper lambdas for appending sections

View File

@@ -16,6 +16,8 @@ class QWidget;
namespace rcx {
struct Theme;
// ── Popup mode ──
enum class TypePopupMode { Root, FieldType, ArrayElement, PointerTarget };
@@ -38,6 +40,7 @@ struct TypeEntry {
struct TypeSpec {
QString baseName;
bool isPointer = false;
int ptrDepth = 0; // 1 = *, 2 = ** (only meaningful when isPointer)
int arrayCount = 0; // 0 = not array
};
@@ -53,7 +56,9 @@ public:
void setFont(const QFont& font);
void setTitle(const QString& title);
void setMode(TypePopupMode mode);
void applyTheme(const Theme& theme);
void setCurrentNodeSize(int bytes);
void setModifier(int modId, int arrayCount = 0);
void setTypes(const QVector<TypeEntry>& types, const TypeEntry* current = nullptr);
void popup(const QPoint& globalPos);
@@ -77,6 +82,7 @@ private:
QLabel* m_previewLabel = nullptr;
QListView* m_listView = nullptr;
QStringListModel* m_model = nullptr;
QFrame* m_separator = nullptr;
// Modifier toggles
QWidget* m_modRow = nullptr;

View File

@@ -0,0 +1,82 @@
#include <QtTest/QtTest>
#include "core.h"
#include "imports/import_pdb.h"
using namespace rcx;
class BenchImportPdb : public QObject {
Q_OBJECT
private slots:
void benchEnumerateAll();
void benchImportAll();
};
static const QString kPdbPath = QStringLiteral(
"C:/Symbols/ntkrnlmp.pdb/0762CF42EF7F3E8116EF7329ADAA09A31/ntkrnlmp.pdb");
void BenchImportPdb::benchEnumerateAll() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
QElapsedTimer timer;
timer.start();
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
qint64 elapsed = timer.elapsed();
QVERIFY2(!types.isEmpty(), qPrintable(err));
qDebug() << "enumeratePdbTypes:" << types.size() << "types in" << elapsed << "ms";
}
void BenchImportPdb::benchImportAll() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
// Phase 1: enumerate
QString err;
QElapsedTimer timer;
timer.start();
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
qint64 enumerateMs = timer.elapsed();
QVERIFY2(!types.isEmpty(), qPrintable(err));
// Collect all type indices
QVector<uint32_t> indices;
indices.reserve(types.size());
for (const auto& t : types)
indices.append(t.typeIndex);
// Phase 2: import all
timer.restart();
int lastProgress = 0;
NodeTree tree = importPdbSelected(kPdbPath, indices, &err,
[&](int cur, int total) -> bool {
// Report progress at 25% intervals
int pct = (cur * 100) / total;
if (pct >= lastProgress + 25) {
qDebug() << " progress:" << cur << "/" << total
<< "(" << pct << "%)";
lastProgress = pct;
}
return true;
});
qint64 importMs = timer.elapsed();
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
// Count root structs
int rootCount = 0;
for (const auto& n : tree.nodes)
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
qDebug() << "";
qDebug() << "=== PDB Import Benchmark (ntkrnlmp.pdb) ===";
qDebug() << " Enumerate:" << types.size() << "types in" << enumerateMs << "ms";
qDebug() << " Import all:" << rootCount << "root structs,"
<< tree.nodes.size() << "total nodes in" << importMs << "ms";
qDebug() << " Total:" << (enumerateMs + importMs) << "ms";
qDebug() << "============================================";
}
QTEST_MAIN(BenchImportPdb)
#include "bench_import_pdb.moc"

View File

@@ -0,0 +1,219 @@
#include "addressparser.h"
#include <QTest>
using rcx::AddressParser;
using rcx::AddressParserCallbacks;
using rcx::AddressParseResult;
class TestAddressParser : public QObject {
Q_OBJECT
private slots:
// -- Hex literals --
void bareHex() { auto r = AddressParser::evaluate("AB"); QVERIFY(r.ok); QCOMPARE(r.value, 0xABULL); }
void prefixedHex() { auto r = AddressParser::evaluate("0x1F4"); QVERIFY(r.ok); QCOMPARE(r.value, 0x1F4ULL); }
void zeroLiteral() { auto r = AddressParser::evaluate("0"); QVERIFY(r.ok); QCOMPARE(r.value, 0ULL); }
void large64bit() { auto r = AddressParser::evaluate("7FF66CCE0000");QVERIFY(r.ok); QCOMPARE(r.value, 0x7FF66CCE0000ULL); }
// -- Arithmetic --
void addition() {
auto r = AddressParser::evaluate("0x100 + 0x200");
QVERIFY(r.ok); QCOMPARE(r.value, 0x300ULL);
}
void subtraction() {
auto r = AddressParser::evaluate("0x300 - 0x100");
QVERIFY(r.ok); QCOMPARE(r.value, 0x200ULL);
}
void multiplication() {
auto r = AddressParser::evaluate("0x10 * 4");
QVERIFY(r.ok); QCOMPARE(r.value, 0x40ULL);
}
void division() {
auto r = AddressParser::evaluate("0x100 / 2");
QVERIFY(r.ok); QCOMPARE(r.value, 0x80ULL);
}
void precedence() {
// 0x10 + 2*3 = 0x10 + 6 = 0x16
auto r = AddressParser::evaluate("0x10 + 2 * 3");
QVERIFY(r.ok); QCOMPARE(r.value, 0x16ULL);
}
void parentheses() {
// (0x10 + 2) * 3 = 0x12 * 3 = 0x36
auto r = AddressParser::evaluate("(0x10 + 2) * 3");
QVERIFY(r.ok); QCOMPARE(r.value, 0x36ULL);
}
// -- Unary minus --
void unaryMinus() {
auto r = AddressParser::evaluate("-0x10 + 0x20");
QVERIFY(r.ok); QCOMPARE(r.value, 0x10ULL);
}
// -- Module resolution --
void moduleResolve() {
AddressParserCallbacks cbs;
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
*ok = (name == "Program.exe");
return *ok ? 0x140000000ULL : 0;
};
auto r = AddressParser::evaluate("<Program.exe> + 0x123", 8, &cbs);
QVERIFY(r.ok);
QCOMPARE(r.value, 0x140000123ULL);
}
void moduleNotFound() {
AddressParserCallbacks cbs;
cbs.resolveModule = [](const QString&, bool* ok) -> uint64_t {
*ok = false;
return 0;
};
auto r = AddressParser::evaluate("<NoSuch.dll>", 8, &cbs);
QVERIFY(!r.ok);
QVERIFY(r.error.contains("not found"));
}
// -- Dereference --
void derefSimple() {
AddressParserCallbacks cbs;
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
*ok = (addr == 0x1000);
return *ok ? 0xDEADBEEFULL : 0;
};
auto r = AddressParser::evaluate("[0x1000]", 8, &cbs);
QVERIFY(r.ok);
QCOMPARE(r.value, 0xDEADBEEFULL);
}
void derefNested() {
AddressParserCallbacks cbs;
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
*ok = (name == "mod");
return *ok ? 0x400000ULL : 0;
};
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
*ok = true;
if (addr == 0x400100) return 0x500000;
if (addr == 0x900000) return 0xABCDEF;
return 0;
};
// [<mod> + [<mod> + 0x100]] = [0x400000 + [0x400000+0x100]]
// inner deref: [0x400100] = 0x500000
// outer: [0x400000 + 0x500000] = [0x900000] = 0xABCDEF
auto r = AddressParser::evaluate("[<mod> + [<mod> + 0x100]]", 8, &cbs);
QVERIFY(r.ok);
QCOMPARE(r.value, 0xABCDEFULL);
}
void derefReadFailure() {
AddressParserCallbacks cbs;
cbs.readPointer = [](uint64_t, bool* ok) -> uint64_t {
*ok = false;
return 0;
};
auto r = AddressParser::evaluate("[0x1000]", 8, &cbs);
QVERIFY(!r.ok);
QVERIFY(r.error.contains("failed to read"));
}
// -- Complex expression from plan --
void complexExpr() {
AddressParserCallbacks cbs;
cbs.resolveModule = [](const QString& name, bool* ok) -> uint64_t {
*ok = (name == "Program.exe");
return *ok ? 0x140000000ULL : 0;
};
cbs.readPointer = [](uint64_t addr, bool* ok) -> uint64_t {
*ok = true;
if (addr == 0x1400000DEULL) return 0x500000;
return 0;
};
// [<Program.exe> + 0xDE] - AB = [0x1400000DE] - 0xAB = 0x500000 - 0xAB = 0x4FFF55
auto r = AddressParser::evaluate("[<Program.exe> + 0xDE] - AB", 8, &cbs);
QVERIFY(r.ok);
QCOMPARE(r.value, 0x4FFF55ULL);
}
// -- Errors --
void emptyInput() {
auto r = AddressParser::evaluate("");
QVERIFY(!r.ok);
}
void unmatchedBracket() {
auto r = AddressParser::evaluate("[0x1000");
QVERIFY(!r.ok);
QVERIFY(r.error.contains("']'"));
}
void unmatchedAngle() {
auto r = AddressParser::evaluate("<Program.exe");
QVERIFY(!r.ok);
QVERIFY(r.error.contains("'>'"));
}
void divisionByZero() {
auto r = AddressParser::evaluate("0x100 / 0");
QVERIFY(!r.ok);
QVERIFY(r.error.contains("division by zero"));
}
void trailingGarbage() {
auto r = AddressParser::evaluate("0x100 xyz");
QVERIFY(!r.ok);
QVERIFY(r.error.contains("unexpected"));
}
void trailingOperator() {
auto r = AddressParser::evaluate("0x100 +");
QVERIFY(!r.ok);
}
// -- Validation --
void validateValid() {
QCOMPARE(AddressParser::validate("0x100 + 0x200"), QString());
QCOMPARE(AddressParser::validate("<Prog.exe> + [0x100]"), QString());
}
void validateInvalid() {
QVERIFY(!AddressParser::validate("").isEmpty());
QVERIFY(!AddressParser::validate("[0x100").isEmpty());
QVERIFY(!AddressParser::validate("0x100 xyz").isEmpty());
}
// -- Backtick stripping --
void backtickStripping() {
auto r = AddressParser::evaluate("7ff6`6cce0000");
QVERIFY(r.ok);
QCOMPARE(r.value, 0x7FF66CCE0000ULL);
}
// -- Whitespace tolerance --
void whitespace() {
auto r = AddressParser::evaluate(" 0x100 + 0x200 ");
QVERIFY(r.ok);
QCOMPARE(r.value, 0x300ULL);
}
// -- Legacy compat: simple hex --
void simpleHexAddress() {
auto r = AddressParser::evaluate("140000000");
QVERIFY(r.ok);
QCOMPARE(r.value, 0x140000000ULL);
}
// -- Multiple additions --
void multipleAdditions() {
auto r = AddressParser::evaluate("0x100 + 0x200 + 0x300");
QVERIFY(r.ok);
QCOMPARE(r.value, 0x600ULL);
}
};
QTEST_GUILESS_MAIN(TestAddressParser)
#include "test_addressparser.moc"

View File

@@ -1,185 +0,0 @@
/**
* test_com_security.cpp — DebugConnect transport diagnostic
*
* Tests EVERY transport to find what works from MinGW:
* 1. TCP to WinDbg .server (port 5055)
* 2. Named pipe to WinDbg .server
* 3. TCP with various COM security configs
* 4. DebugCreate local (baseline)
*
* SETUP: In WinDbg, run BOTH of these:
* .server tcp:port=5055
* .server npipe:pipe=reclass
*
* Then run this test.
*/
#include <cstdio>
#include <cstdlib>
#include <cstring>
#ifdef _WIN32
#include <windows.h>
#include <objbase.h>
#include <initguid.h>
#include <dbgeng.h>
#endif
#ifdef _WIN32
static void try_connect(const char* label, const char* connStr)
{
printf(" %-40s → ", label);
fflush(stdout);
IDebugClient* client = nullptr;
HRESULT hr = DebugConnect(connStr, IID_IDebugClient, (void**)&client);
if (SUCCEEDED(hr) && client) {
printf("SUCCESS (hr=0x%08lX)\n", (unsigned long)hr);
// Try to get data spaces and read something
IDebugDataSpaces* ds = nullptr;
IDebugSymbols* sym = nullptr;
IDebugControl* ctrl = nullptr;
client->QueryInterface(IID_IDebugDataSpaces, (void**)&ds);
client->QueryInterface(IID_IDebugSymbols, (void**)&sym);
client->QueryInterface(IID_IDebugControl, (void**)&ctrl);
if (ctrl) {
HRESULT hrWait = ctrl->WaitForEvent(0, 5000);
printf(" WaitForEvent: hr=0x%08lX\n", (unsigned long)hrWait);
}
if (sym) {
ULONG numMods = 0, numUnloaded = 0;
sym->GetNumberModules(&numMods, &numUnloaded);
printf(" Modules: %lu loaded\n", numMods);
if (numMods > 0 && ds) {
ULONG64 base = 0;
sym->GetModuleByIndex(0, &base);
unsigned char buf[2] = {};
ULONG got = 0;
ds->ReadVirtual(base, buf, 2, &got);
printf(" Read at 0x%llX: got=%lu bytes=[%02X %02X]\n",
(unsigned long long)base, got, buf[0], buf[1]);
}
}
if (sym) sym->Release();
if (ds) ds->Release();
if (ctrl) ctrl->Release();
client->Release();
} else {
char buf[256] = {};
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, (DWORD)hr, 0, buf, sizeof(buf), nullptr);
for (char* p = buf + strlen(buf) - 1; p >= buf && (*p == '\r' || *p == '\n'); --p)
*p = '\0';
printf("FAIL hr=0x%08lX (%s)\n", (unsigned long)hr, buf);
}
}
#endif
int main()
{
#ifdef _WIN32
char hostname[256] = {};
DWORD hsize = sizeof(hostname);
GetComputerNameA(hostname, &hsize);
printf("=== DebugConnect Transport Diagnostic ===\n");
printf("Machine: %s\n\n", hostname);
// ── Baseline: DebugCreate (local) ──
printf("[1] DebugCreate (local, no network)\n");
{
IDebugClient* client = nullptr;
HRESULT hr = DebugCreate(IID_IDebugClient, (void**)&client);
printf(" DebugCreate: %s (hr=0x%08lX)\n\n",
SUCCEEDED(hr) ? "OK" : "FAIL", (unsigned long)hr);
if (client) client->Release();
}
// ── TCP variants ──
printf("[2] TCP connections (need: .server tcp:port=5055)\n");
try_connect("tcp:Port=5055,Server=localhost",
"tcp:Port=5055,Server=localhost");
try_connect("tcp:Port=5055,Server=127.0.0.1",
"tcp:Port=5055,Server=127.0.0.1");
{
char conn[512];
snprintf(conn, sizeof(conn), "tcp:Port=5055,Server=%s", hostname);
try_connect(conn, conn);
}
printf("\n");
// ── Named pipe variants ──
printf("[3] Named pipe connections (need: .server npipe:pipe=reclass)\n");
try_connect("npipe:Pipe=reclass,Server=localhost",
"npipe:Pipe=reclass,Server=localhost");
{
char conn[512];
snprintf(conn, sizeof(conn), "npipe:Pipe=reclass,Server=%s", hostname);
try_connect(conn, conn);
}
try_connect("npipe:Pipe=reclass",
"npipe:Pipe=reclass");
printf("\n");
// ── TCP with COM security ──
printf("[4] TCP with explicit COM init (MTA + IMPERSONATE)\n");
{
// This runs in-process so CoInitialize affects subsequent calls
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
CoInitializeSecurity(
nullptr, -1, nullptr, nullptr,
RPC_C_AUTHN_LEVEL_DEFAULT,
RPC_C_IMP_LEVEL_IMPERSONATE,
nullptr, EOAC_NONE, nullptr);
try_connect("tcp:Port=5055,Server=localhost (MTA+SEC)",
"tcp:Port=5055,Server=localhost");
try_connect("npipe:Pipe=reclass (MTA+SEC)",
"npipe:Pipe=reclass,Server=localhost");
CoUninitialize();
}
printf("\n");
// ── Check if dbgeng.dll is the system one ──
printf("[5] DbgEng DLL info\n");
{
HMODULE hmod = GetModuleHandleA("dbgeng.dll");
if (hmod) {
char path[MAX_PATH] = {};
GetModuleFileNameA(hmod, path, MAX_PATH);
printf(" dbgeng.dll loaded from: %s\n", path);
// Get version
DWORD verSize = GetFileVersionInfoSizeA(path, nullptr);
if (verSize > 0) {
auto* verData = (char*)malloc(verSize);
if (GetFileVersionInfoA(path, 0, verSize, verData)) {
VS_FIXEDFILEINFO* fileInfo = nullptr;
UINT len = 0;
if (VerQueryValueA(verData, "\\", (void**)&fileInfo, &len)) {
printf(" Version: %d.%d.%d.%d\n",
HIWORD(fileInfo->dwFileVersionMS),
LOWORD(fileInfo->dwFileVersionMS),
HIWORD(fileInfo->dwFileVersionLS),
LOWORD(fileInfo->dwFileVersionLS));
}
}
free(verData);
}
} else {
printf(" dbgeng.dll not loaded yet\n");
}
}
printf("\n=== Done ===\n");
return 0;
#else
printf("Windows only.\n");
return 0;
#endif
}

View File

@@ -1017,7 +1017,7 @@ private slots:
void testPrimitiveArrayElements() {
// Expanded primitive array should synthesize element lines dynamically
NodeTree tree;
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
@@ -1920,54 +1920,9 @@ private slots:
}
}
void testComputeStructAlignment() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// Int32 has alignment 4
Node f1;
f1.kind = NodeKind::Int32;
f1.name = "a";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
QCOMPARE(tree.computeStructAlignment(rootId), 4);
// Add Hex64 (alignment 8) — max should become 8
Node f2;
f2.kind = NodeKind::Hex64;
f2.name = "b";
f2.parentId = rootId;
f2.offset = 8;
tree.addNode(f2);
QCOMPARE(tree.computeStructAlignment(rootId), 8);
}
void testComputeStructAlignmentEmpty() {
NodeTree tree;
Node root;
root.kind = NodeKind::Struct;
root.name = "Empty";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// Empty struct → alignment 1
QCOMPARE(tree.computeStructAlignment(rootId), 1);
}
void testCommandRowRootNameSpan() {
// Name span should cover the class name in the merged command row
QString text = "source\u25BE \u00B7 0x0 \u00B7 struct\u25BE MyClass {";
QString text = "source\u25BE \u00B7 0x0 \u00B7 struct MyClass {";
ColumnSpan nameSpan = commandRowRootNameSpan(text);
QVERIFY(nameSpan.valid);
@@ -1979,7 +1934,7 @@ private slots:
void testTextIsNonEmpty() {
// Verify composed text is actually generated (not empty)
NodeTree tree;
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;

View File

@@ -8,7 +8,7 @@
using namespace rcx;
static void buildTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
@@ -394,6 +394,65 @@ private slots:
QApplication::processEvents();
QCOMPARE(countNodes(), before);
}
// ── Change to Ptr* creates class and sets refId ──
void testChangeToPtrStarCreatesClassAndSetsRef() {
// Add a Hex64 node to the root struct
uint64_t rootId = m_doc->tree.nodes[0].id;
m_ctrl->insertNode(rootId, 16, NodeKind::Hex64, "ptrField");
QApplication::processEvents();
int ptrIdx = findNode("ptrField");
QVERIFY(ptrIdx >= 0);
uint64_t ptrNodeId = m_doc->tree.nodes[ptrIdx].id;
int before = countNodes();
// Convert to typed pointer
m_ctrl->convertToTypedPointer(ptrNodeId);
QApplication::processEvents();
// Re-find after tree mutation
ptrIdx = -1;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; }
}
QVERIFY(ptrIdx >= 0);
// Verify: node kind changed to Pointer64
QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Pointer64);
// Verify: node.refId != 0
uint64_t refId = m_doc->tree.nodes[ptrIdx].refId;
QVERIFY(refId != 0);
// Verify: a new Struct node exists with the refId as its id
int structIdx = m_doc->tree.indexOfId(refId);
QVERIFY(structIdx >= 0);
QCOMPARE(m_doc->tree.nodes[structIdx].kind, NodeKind::Struct);
// Verify: the new struct has children (Hex64 fields)
auto children = m_doc->tree.childrenOf(refId);
QVERIFY(children.size() == 16);
for (int ci : children)
QCOMPARE(m_doc->tree.nodes[ci].kind, NodeKind::Hex64);
// Verify: total nodes increased by 1 struct + 16 children = 17
QCOMPARE(countNodes(), before + 17);
// Verify: undo restores the original Hex64 kind and refId==0
m_doc->undoStack.undo();
QApplication::processEvents();
ptrIdx = -1;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
if (m_doc->tree.nodes[i].id == ptrNodeId) { ptrIdx = i; break; }
}
QVERIFY(ptrIdx >= 0);
QCOMPARE(m_doc->tree.nodes[ptrIdx].kind, NodeKind::Hex64);
QCOMPARE(m_doc->tree.nodes[ptrIdx].refId, (uint64_t)0);
QCOMPARE(countNodes(), before);
}
};
QTEST_MAIN(TestContextMenu)

View File

@@ -8,10 +8,29 @@
using namespace rcx;
// Provider with a configurable base address (for testing source-switch logic)
class BaseAwareProvider : public Provider {
QByteArray m_data;
uint64_t m_base;
public:
BaseAwareProvider(QByteArray data, uint64_t base)
: m_data(std::move(data)), m_base(base) {}
bool read(uint64_t addr, void* buf, int len) const override {
if (addr + len > (uint64_t)m_data.size()) return false;
std::memcpy(buf, m_data.constData() + addr, len);
return true;
}
int size() const override { return m_data.size(); }
uint64_t base() const override { return m_base; }
bool isLive() const override { return true; }
QString name() const override { return QStringLiteral("test"); }
QString kind() const override { return QStringLiteral("Process"); }
};
// Small tree: one root struct with a few typed fields at known offsets.
// Keeps tests fast and deterministic (no giant PEB tree).
static void buildSmallTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
@@ -383,6 +402,44 @@ private slots:
QCOMPARE((uint8_t)bytes[0], (uint8_t)0xFF);
}
// ── Test: source switch preserves existing base address ──
void testSourceSwitchPreservesBase() {
// Set a non-zero baseAddress to simulate a loaded .rcx file
m_doc->tree.baseAddress = 0x1000;
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
// Simulate attaching a new provider whose base differs (e.g. 0x400000)
auto prov = std::make_shared<BaseAwareProvider>(makeSmallBuffer(), 0x400000);
uint64_t newBase = prov->base();
QCOMPARE(newBase, (uint64_t)0x400000);
m_doc->provider = prov;
// Controller logic: keep existing baseAddress when non-zero
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
// baseAddress must stay at the original value
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x1000);
// provider base is unchanged (no setBase sync) — provider reports its own initial base
QCOMPARE(m_doc->provider->base(), (uint64_t)0x400000);
}
// ── Test: source switch on fresh doc uses provider default ──
void testSourceSwitchFreshDocUsesProviderBase() {
// Simulate a fresh document (no loaded .rcx → baseAddress == 0)
m_doc->tree.baseAddress = 0;
auto prov = std::make_shared<BaseAwareProvider>(makeSmallBuffer(), 0x7FFE0000);
uint64_t newBase = prov->base();
m_doc->provider = prov;
if (m_doc->tree.baseAddress == 0)
m_doc->tree.baseAddress = newBase;
// Fresh doc should adopt the provider's default base
QCOMPARE(m_doc->tree.baseAddress, (uint64_t)0x7FFE0000);
}
// ── Test: toggleCollapse + undo ──
void testToggleCollapse() {
// Root is index 0, a Struct node
@@ -406,6 +463,211 @@ private slots:
QApplication::processEvents();
QCOMPARE(m_doc->tree.nodes[0].collapsed, false);
}
// ── Test: value history popup only appears during inline editing ──
void testValueHistoryPopupOnlyDuringEdit() {
// Record value history for field_u32 so it has heat
auto& tree = m_doc->tree;
int idx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == "field_u32") { idx = i; break; }
}
QVERIFY(idx >= 0);
uint64_t nodeId = tree.nodes[idx].id;
QHash<uint64_t, ValueHistory> history;
history[nodeId].record("100");
history[nodeId].record("200");
history[nodeId].record("300");
QVERIFY(history[nodeId].uniqueCount() > 1);
m_editor->setValueHistoryRef(&history);
// Refresh and compose so editor has meta with heatLevel
m_ctrl->refresh();
QApplication::processEvents();
ComposeResult result = m_doc->compose();
// Manually set heat on the node's line meta
for (auto& lm : result.meta) {
if (lm.nodeId == nodeId) lm.heatLevel = 2;
}
m_editor->applyDocument(result);
QApplication::processEvents();
// Popup should not exist or not be visible (no editing active)
auto* popup = m_editor->findChild<QWidget*>(QString(), Qt::FindDirectChildrenOnly);
// Even if popup widget exists, it should not be visible
bool popupVisible = false;
for (auto* child : m_editor->findChildren<QFrame*>(QString(), Qt::FindDirectChildrenOnly)) {
if (child->isVisible() && child->windowFlags() & Qt::ToolTip)
popupVisible = true;
}
QVERIFY2(!popupVisible, "Popup should not be visible when not editing");
// Start inline edit on value column of field_u32
int fieldLine = -1;
for (int i = 0; i < result.meta.size(); i++) {
if (result.meta[i].nodeId == nodeId && result.meta[i].lineKind == LineKind::Field) {
fieldLine = i; break;
}
}
QVERIFY(fieldLine >= 0);
bool ok = m_editor->beginInlineEdit(EditTarget::Value, fieldLine);
QVERIFY(ok);
QVERIFY(m_editor->isEditing());
// Trigger hover cursor update (simulates mouse move during editing)
QApplication::processEvents();
// Cancel edit to clean up
m_editor->cancelInlineEdit();
QApplication::processEvents();
m_editor->setValueHistoryRef(nullptr);
}
// ── Test: delete node clears value history for shifted siblings ──
void testDeleteClearsHeatForShiftedNodes() {
// Replace with a live provider so refresh() actually records values
m_doc->provider = std::make_unique<BaseAwareProvider>(makeSmallBuffer(), 0x1000);
m_ctrl->refresh();
QApplication::processEvents();
auto& tree = m_doc->tree;
// Locate field_u32 (the node we'll delete) and the siblings after it.
// The small tree has: field_u32(0), field_float(4), field_u8(8),
// pad0/Hex16(9), pad1/Hex8(11), field_hex/Hex32(12)
// field_float and field_u8 are regular (non-hex) types.
int delIdx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == "field_u32") { delIdx = i; break; }
}
QVERIFY(delIdx >= 0);
uint64_t delId = tree.nodes[delIdx].id;
// Collect sibling node IDs that come after field_u32 (will be shifted)
uint64_t parentId = tree.nodes[delIdx].parentId;
int deletedSize = tree.nodes[delIdx].byteSize(); // 4 bytes
int deletedEnd = tree.nodes[delIdx].offset + deletedSize;
QVector<uint64_t> shiftedIds;
QHash<uint64_t, QString> nameMap; // for debug messages
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].parentId == parentId && i != delIdx
&& tree.nodes[i].offset >= deletedEnd) {
shiftedIds.append(tree.nodes[i].id);
nameMap[tree.nodes[i].id] = tree.nodes[i].name;
}
}
QVERIFY2(!shiftedIds.isEmpty(), "Should have siblings after field_u32");
// Seed value history for shifted siblings (simulate accumulated heat)
auto& history = const_cast<QHash<uint64_t, ValueHistory>&>(m_ctrl->valueHistory());
for (uint64_t id : shiftedIds) {
history[id].record("old_val_1");
history[id].record("old_val_2");
history[id].record("old_val_3");
QVERIFY2(history[id].heatLevel() >= 2,
qPrintable(QString("Pre-delete: %1 should have heat>=2")
.arg(nameMap[id])));
}
// Also seed the to-be-deleted node
history[delId].record("del_1");
history[delId].record("del_2");
QVERIFY(history.contains(delId));
// Delete field_u32 — this shifts all subsequent siblings
m_ctrl->removeNode(delIdx);
QApplication::processEvents();
// The deleted node's history should be gone
QVERIFY2(!m_ctrl->valueHistory().contains(delId),
"Deleted node's value history should be cleared");
// All shifted siblings should have heat=0 after the delete.
// With a live provider, refresh() inside removeNode re-records one new
// value at the new offset → count=1 → heatLevel=0.
for (uint64_t id : shiftedIds) {
int heat = m_ctrl->valueHistory().contains(id)
? m_ctrl->valueHistory()[id].heatLevel() : 0;
QVERIFY2(heat == 0,
qPrintable(QString("Shifted node '%1' (id=%2) should have heat=0, got %3")
.arg(nameMap[id]).arg(id).arg(heat)));
}
}
// ── Test: value history records and cycles correctly ──
void testValueHistoryRingBuffer() {
ValueHistory vh;
QCOMPARE(vh.count, 0);
QCOMPARE(vh.heatLevel(), 0);
vh.record("10");
QCOMPARE(vh.count, 1);
QCOMPARE(vh.heatLevel(), 0); // 1 unique = static
// Duplicate should not increase count
vh.record("10");
QCOMPARE(vh.count, 1);
vh.record("20");
QCOMPARE(vh.count, 2);
QCOMPARE(vh.heatLevel(), 1); // cold
vh.record("30");
QCOMPARE(vh.count, 3);
QCOMPARE(vh.heatLevel(), 2); // warm
vh.record("40");
vh.record("50");
QCOMPARE(vh.count, 5);
QCOMPARE(vh.heatLevel(), 3); // hot
QCOMPARE(vh.last(), QString("50"));
// Ring buffer: uniqueCount() caps at kCapacity
for (int i = 0; i < 20; i++)
vh.record(QString::number(100 + i));
QCOMPARE(vh.uniqueCount(), ValueHistory::kCapacity);
QVERIFY(vh.count > ValueHistory::kCapacity);
// forEach iterates oldest→newest within ring
QStringList vals;
vh.forEach([&](const QString& v) { vals.append(v); });
QCOMPARE(vals.size(), ValueHistory::kCapacity);
QCOMPARE(vals.last(), vh.last());
}
// ── Test: inline edit "int32_t[4]" on primitive converts to array ──
void testInlineEditPrimitiveArray() {
// Find a primitive field to convert
int idx = -1;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
if (m_doc->tree.nodes[i].name == "field_u32") { idx = i; break; }
}
QVERIFY(idx >= 0);
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32);
uint64_t nodeId = m_doc->tree.nodes[idx].id;
// Emit inlineEditCommitted with array syntax
emit m_editor->inlineEditCommitted(idx, 0, EditTarget::Type,
QStringLiteral("int32_t[4]"));
QApplication::processEvents();
// Node should now be an Array with elementKind=Int32, arrayLen=4
int newIdx = m_doc->tree.indexOfId(nodeId);
QVERIFY(newIdx >= 0);
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::Array);
QCOMPARE(m_doc->tree.nodes[newIdx].elementKind, NodeKind::Int32);
QCOMPARE(m_doc->tree.nodes[newIdx].arrayLen, 4);
// Undo should restore to UInt32
m_doc->undoStack.undo();
QApplication::processEvents();
newIdx = m_doc->tree.indexOfId(nodeId);
QVERIFY(newIdx >= 0);
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::UInt32);
}
};
QTEST_MAIN(TestController)

View File

@@ -583,6 +583,94 @@ private slots:
QCOMPARE(norm.size(), 1);
QVERIFY(norm.contains(rootId));
}
// ── ValueHistory tests ──
void testValueHistory_empty() {
rcx::ValueHistory h;
QCOMPARE(h.heatLevel(), 0);
QCOMPARE(h.uniqueCount(), 0);
QCOMPARE(h.last(), QString());
}
void testValueHistory_singleValue() {
rcx::ValueHistory h;
h.record("42");
QCOMPARE(h.heatLevel(), 0); // only 1 unique → static
QCOMPARE(h.uniqueCount(), 1);
QCOMPARE(h.last(), QString("42"));
}
void testValueHistory_duplicateIgnored() {
rcx::ValueHistory h;
h.record("42");
h.record("42");
h.record("42");
QCOMPARE(h.count, 1);
QCOMPARE(h.heatLevel(), 0);
}
void testValueHistory_heatLevels() {
rcx::ValueHistory h;
h.record("a");
QCOMPARE(h.heatLevel(), 0); // 1 unique
h.record("b");
QCOMPARE(h.heatLevel(), 1); // 2 unique → cold
h.record("c");
QCOMPARE(h.heatLevel(), 2); // 3 unique → warm
h.record("d");
QCOMPARE(h.heatLevel(), 2); // 4 unique → warm
h.record("e");
QCOMPARE(h.heatLevel(), 3); // 5 unique → hot
}
void testValueHistory_ringWrap() {
rcx::ValueHistory h;
// Fill beyond capacity
for (int i = 0; i < 15; i++)
h.record(QString::number(i));
QCOMPARE(h.count, 15);
QCOMPARE(h.uniqueCount(), 10); // capped at kCapacity
QCOMPARE(h.heatLevel(), 3); // hot
QCOMPARE(h.last(), QString("14"));
// Verify oldest values were pushed out, newest 10 remain
QStringList collected;
h.forEach([&](const QString& v) { collected.append(v); });
QCOMPARE(collected.size(), 10);
QCOMPARE(collected.first(), QString("5")); // oldest surviving
QCOMPARE(collected.last(), QString("14")); // newest
}
void testValueHistory_forEach() {
rcx::ValueHistory h;
h.record("x");
h.record("y");
h.record("z");
QStringList items;
h.forEach([&](const QString& v) { items.append(v); });
QCOMPARE(items.size(), 3);
QCOMPARE(items[0], QString("x"));
QCOMPARE(items[1], QString("y"));
QCOMPARE(items[2], QString("z"));
}
void testValueHistory_oscillation() {
// Values that oscillate (A → B → A → B) should still count each unique transition
rcx::ValueHistory h;
h.record("A");
h.record("B");
h.record("A");
h.record("B");
QCOMPARE(h.count, 4); // 4 transitions
QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range)
}
};
QTEST_MAIN(TestCore)

465
tests/test_disasm.cpp Normal file
View File

@@ -0,0 +1,465 @@
#include <QtTest/QTest>
#include "disasm.h"
#include "core.h"
#include "providers/buffer_provider.h"
using namespace rcx;
// Helper: extract mnemonic portion from disassembly output (after "addr ")
static QString mnemonic(const QString& line) {
int sep = line.indexOf(" ");
return sep >= 0 ? line.mid(sep + 2) : line;
}
class TestDisasm : public QObject {
Q_OBJECT
private slots:
// ──────────────────────────────────────────────────
// disassemble() unit tests exact mnemonic match
// ──────────────────────────────────────────────────
void testDisasm64_pushMov() {
QByteArray code("\x55\x48\x89\xe5", 4);
QString result = disassemble(code, 0x401000, 64);
QStringList lines = result.split('\n');
QCOMPARE(lines.size(), 2);
QVERIFY(lines[0].startsWith("0000000000401000"));
QVERIFY(lines[1].startsWith("0000000000401001"));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
}
void testDisasm64_ret() { QCOMPARE(mnemonic(disassemble(QByteArray("\xc3",1), 0x7FF000, 64)), QStringLiteral("ret")); }
void testDisasm64_nop() { QCOMPARE(mnemonic(disassemble(QByteArray("\x90",1), 0, 64)), QStringLiteral("nop")); }
void testDisasm64_xorEax() { QCOMPARE(mnemonic(disassemble(QByteArray("\x31\xc0",2), 0, 64)), QStringLiteral("xor eax, eax")); }
void testDisasm64_subRsp() { QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x83\xec\x20",4), 0, 64)), QStringLiteral("sub rsp, 0x20")); }
void testDisasm64_int3() { QCOMPARE(mnemonic(disassemble(QByteArray("\xcc",1), 0, 64)), QStringLiteral("int3")); }
void testDisasm64_pushRdi() { QCOMPARE(mnemonic(disassemble(QByteArray("\x57",1), 0, 64)), QStringLiteral("push rdi")); }
void testDisasm64_popRsi() { QCOMPARE(mnemonic(disassemble(QByteArray("\x5e",1), 0, 64)), QStringLiteral("pop rsi")); }
void testDisasm64_testEax() { QCOMPARE(mnemonic(disassemble(QByteArray("\x85\xc0",2), 0, 64)), QStringLiteral("test eax, eax")); }
void testDisasm64_leaRipRel() {
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x8d\x05\x10\x00\x00\x00",7), 0x1000, 64)),
QStringLiteral("lea rax, [rip+0x10]"));
}
void testDisasm64_callRel() {
// call target = 0x1000 + 5 + 0x100 = 0x1105
QCOMPARE(mnemonic(disassemble(QByteArray("\xe8\x00\x01\x00\x00",5), 0x1000, 64)),
QStringLiteral("call 0x1105"));
}
void testDisasm64_jmpRel() {
// jmp target = 0x1000 + 2 + 0x10 = 0x1012
QCOMPARE(mnemonic(disassemble(QByteArray("\xeb\x10",2), 0x1000, 64)),
QStringLiteral("jmp 0x1012"));
}
void testDisasm64_movMemRead() {
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x8b\x43\x10",4), 0, 64)),
QStringLiteral("mov rax, qword ptr [rbx+0x10]"));
}
void testDisasm64_movMemWrite() {
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x89\x4c\x24\x08",5), 0, 64)),
QStringLiteral("mov qword ptr [rsp+0x8], rcx"));
}
void testDisasm64_functionPrologue() {
QByteArray code("\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
QStringList lines = disassemble(code, 0x140001000ULL, 64).split('\n');
QCOMPARE(lines.size(), 4);
QVERIFY(lines[0].startsWith("0000000140001000"));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
QCOMPARE(mnemonic(lines[2]), QStringLiteral("sub rsp, 0x20"));
QCOMPARE(mnemonic(lines[3]), QStringLiteral("ret"));
}
void testDisasm64_multipleNops() {
QStringList lines = disassemble(QByteArray(5,'\x90'), 0x1000, 64).split('\n');
QCOMPARE(lines.size(), 5);
for (int i = 0; i < 5; i++) {
QCOMPARE(mnemonic(lines[i]), QStringLiteral("nop"));
QVERIFY(lines[i].startsWith(QStringLiteral("%1").arg(0x1000+i, 16, 16, QLatin1Char('0'))));
}
}
void testDisasm32_pushMov() {
QByteArray code("\x55\x89\xe5", 3);
QStringList lines = disassemble(code, 0x401000, 32).split('\n');
QCOMPARE(lines.size(), 2);
QVERIFY(lines[0].startsWith("00401000"));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push ebp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov ebp, esp"));
}
void testDisasm_empty() { QVERIFY(disassemble({}, 0, 64).isEmpty()); QVERIFY(disassemble({}, 0, 32).isEmpty()); }
void testDisasm_invalidBitness() { QVERIFY(disassemble(QByteArray("\x90",1), 0, 16).isEmpty()); }
void testDisasm_maxBytes() { QCOMPARE(disassemble(QByteArray(200,'\x90'), 0, 64, 128).count('\n') + 1, 128); }
void testDisasm64_addrWidth() { QCOMPARE(disassemble(QByteArray("\x90",1), 0, 64).indexOf(" "), 16); }
void testDisasm32_addrWidth() { QCOMPARE(disassemble(QByteArray("\x90",1), 0, 32).indexOf(" "), 8); }
// ──────────────────────────────────────────────────
// hexDump() unit tests
// ──────────────────────────────────────────────────
void testHexDump_basic() {
QByteArray data; for (int i=0;i<32;i++) data.append((char)i);
QString r = hexDump(data, 0x1000, 128);
QCOMPARE(r.count('\n')+1, 2);
QVERIFY(r.startsWith("00001000"));
}
void testHexDump_ascii() {
QVERIFY(hexDump(QByteArray("Hello, World!xx",15), 0, 128).contains("Hello"));
}
void testHexDump_nonPrintable() {
QByteArray d(16,'\0'); d[0]='A'; d[15]='Z';
QVERIFY(hexDump(d, 0, 128).contains("A..............Z"));
}
void testHexDump_empty() { QVERIFY(hexDump({}, 0).isEmpty()); }
void testHexDump_maxBytes() { QCOMPARE(hexDump(QByteArray(200,'\xAA'), 0, 64).count('\n')+1, 4); }
void testHexDump_wideAddr() { QVERIFY(hexDump(QByteArray(16,'\0'), 0x100000000ULL, 128).startsWith("0000000100000000")); }
void testHexDump_hexValues() {
QByteArray d; d.append('\xDE'); d.append('\xAD'); d.append('\xBE'); d.append('\xEF');
while (d.size()<16) d.append('\0');
QVERIFY(hexDump(d, 0, 128).contains("de ad be ef", Qt::CaseInsensitive));
}
void testHexDump_secondLineAddr() {
QStringList lines = hexDump(QByteArray(32,'\x42'), 0x2000, 128).split('\n');
QCOMPARE(lines.size(), 2);
QVERIFY(lines[1].startsWith("00002010"));
}
// ──────────────────────────────────────────────────
// End-to-end: pointer-expanded VTable with FuncPtr64
// Verifies we read from the COMPOSED address, not node.offset
// ──────────────────────────────────────────────────
void testVTableDisasm_composedAddress() {
// Memory layout (absolute addresses, baseAddress = 0):
//
// [0x0000] Root "Obj" struct
// +0x00: Pointer64 __vptr => points to 0x100 (vtable)
//
// [0x0100] VTable (expanded via pointer deref)
// +0x00: func ptr 0 => value 0x200 (func0 code)
// +0x08: func ptr 1 => value 0x300 (func1 code)
//
// [0x0200] func0 code: push rbp; ret
// [0x0300] func1 code: xor eax, eax; ret
//
// Build a 4KB buffer
QByteArray mem(4096, '\0');
auto w64 = [&](int off, uint64_t val) {
memcpy(mem.data() + off, &val, 8);
};
// Root object at offset 0: __vptr points to vtable at 0x100
w64(0x00, 0x100);
// VTable at offset 0x100: two function pointers
w64(0x100, 0x200); // slot 0 -> func0
w64(0x108, 0x300); // slot 1 -> func1
// func0 at offset 0x200: push rbp; ret
mem[0x200] = '\x55';
mem[0x201] = '\xc3';
// func1 at offset 0x300: xor eax, eax; ret
mem[0x300] = '\x31';
mem[0x301] = '\xc0';
mem[0x302] = '\xc3';
BufferProvider prov(mem);
// Build node tree
NodeTree tree;
tree.baseAddress = 0;
// Root struct "Obj"
Node root;
root.kind = NodeKind::Struct;
root.name = "Obj";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// VTable struct definition (template)
Node vtDef;
vtDef.kind = NodeKind::Struct;
vtDef.name = "VTable";
vtDef.parentId = 0;
vtDef.offset = 0x1000; // parked far away so it doesn't overlap
int vti = tree.addNode(vtDef);
uint64_t vtId = tree.nodes[vti].id;
// Two FuncPtr64 children inside VTable definition
Node fp0;
fp0.kind = NodeKind::FuncPtr64;
fp0.name = "func0";
fp0.parentId = vtId;
fp0.offset = 0;
tree.addNode(fp0);
Node fp1;
fp1.kind = NodeKind::FuncPtr64;
fp1.name = "func1";
fp1.parentId = vtId;
fp1.offset = 8;
tree.addNode(fp1);
// Pointer64 "__vptr" in root, pointing to VTable via refId
Node vptr;
vptr.kind = NodeKind::Pointer64;
vptr.name = "__vptr";
vptr.parentId = rootId;
vptr.offset = 0;
vptr.refId = vtId;
tree.addNode(vptr);
// Compose the tree
ComposeResult result = compose(tree, prov);
// Find the FuncPtr64 lines in the composed output that are inside the
// pointer-expanded VTable (near vtable address), not the standalone definition.
struct FuncInfo { int line; uint64_t offsetAddr; NodeKind kind; QString name; };
QVector<FuncInfo> funcPtrs;
for (int i = 0; i < result.meta.size(); i++) {
const LineMeta& lm = result.meta[i];
if (lm.nodeKind == NodeKind::FuncPtr64 && lm.lineKind == LineKind::Field) {
// Only include the pointer-expanded ones (near vtable at 0x100)
if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
int nodeIdx = lm.nodeIdx;
funcPtrs.append({i, lm.offsetAddr, lm.nodeKind,
nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()});
}
}
}
QCOMPARE(funcPtrs.size(), 2);
// Verify composed addresses point to the vtable, NOT to the root struct
// func0 should be at 0x100 (vtable + 0)
QCOMPARE(funcPtrs[0].offsetAddr, (uint64_t)0x100);
// func1 should be at 0x108 (vtable + 8)
QCOMPARE(funcPtrs[1].offsetAddr, (uint64_t)0x108);
// Now simulate what the hover code should do:
// Read the function pointer VALUE from the correct provider address
for (const auto& fp : funcPtrs) {
// Provider reads at absolute address directly
uint64_t provAddr = fp.offsetAddr;
// Read the pointer value (the function address)
uint64_t ptrVal = prov.readU64(provAddr);
// Verify we got the right pointer values
if (fp.name == "func0") {
QCOMPARE(ptrVal, (uint64_t)0x200);
} else {
QCOMPARE(ptrVal, (uint64_t)0x300);
}
// Read code bytes at the pointer target (absolute address)
uint64_t codeProvAddr = ptrVal;
QByteArray codeBytes = prov.readBytes(codeProvAddr, 128);
// Disassemble and verify
QString asm_ = disassemble(codeBytes, ptrVal, 64, 128);
QVERIFY2(!asm_.isEmpty(), qPrintable("Empty disasm for " + fp.name));
QStringList lines = asm_.split('\n');
if (fp.name == "func0") {
// Should decode: push rbp; ret
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func0, got %1: %2").arg(lines.size()).arg(asm_)));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
// Verify address in output matches the real function address
QVERIFY2(lines[0].contains("200"),
qPrintable("func0 addr wrong: " + lines[0]));
} else {
// Should decode: xor eax, eax; ret
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func1, got %1: %2").arg(lines.size()).arg(asm_)));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
QVERIFY2(lines[0].contains("300"),
qPrintable("func1 addr wrong: " + lines[0]));
}
}
// CRITICAL: Verify that reading from node.offset (the WRONG way) gives
// different/wrong results. node.offset for func0=0, func1=8, which are
// inside the ROOT struct, not the vtable.
uint64_t wrongVal0 = prov.readU64(0); // node.offset=0: reads __vptr value
uint64_t wrongVal1 = prov.readU64(8); // node.offset=8: reads garbage after __vptr
// wrongVal0 = 0x100 (the vptr itself, NOT a function address)
QCOMPARE(wrongVal0, (uint64_t)0x100);
// This is the vtable address, not a function — disassembling it would be wrong
QVERIFY2(wrongVal0 != (uint64_t)0x200,
"node.offset reads the vptr, not the function pointer");
QVERIFY2(wrongVal1 != (uint64_t)0x300,
"node.offset=8 reads past vptr, not the second function pointer");
}
void testVTableDisasm_wrongAddressGivesWrongCode() {
// Demonstrate that using node.offset instead of composed address
// gives completely wrong disassembly results
QByteArray mem(1024, '\0');
auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); };
// Root at 0: vptr -> 0x80
w64(0x00, (uint64_t)0x80);
// VTable at 0x80: one func ptr -> 0x100
w64(0x80, (uint64_t)0x100);
// Code at 0x100: sub rsp, 0x28; nop; ret
mem[0x100] = '\x48'; mem[0x101] = '\x83'; mem[0x102] = '\xec';
mem[0x103] = '\x28'; mem[0x104] = '\x90'; mem[0x105] = '\xc3';
BufferProvider prov(mem);
// WRONG: read from node.offset=0 (root's vptr value, not the func ptr)
uint64_t wrongPtrVal = prov.readU64(0);
QCOMPARE(wrongPtrVal, (uint64_t)0x80); // This is the vtable addr, not a function!
// RIGHT: read from composed address (vtable + 0)
uint64_t rightPtrVal = prov.readU64(0x80);
QCOMPARE(rightPtrVal, (uint64_t)0x100); // This IS the function address
// Disassemble the RIGHT target
QByteArray rightCode = prov.readBytes(0x100, 128);
QString rightAsm = disassemble(rightCode, 0x100, 64, 128);
QStringList rightLines = rightAsm.split('\n');
QVERIFY(rightLines.size() >= 3);
QCOMPARE(mnemonic(rightLines[0]), QStringLiteral("sub rsp, 0x28"));
QCOMPARE(mnemonic(rightLines[1]), QStringLiteral("nop"));
QCOMPARE(mnemonic(rightLines[2]), QStringLiteral("ret"));
// Disassemble the WRONG target (vtable data, not code!)
QByteArray wrongCode = prov.readBytes(0x80, 128);
QString wrongAsm = disassemble(wrongCode, 0x80, 64, 128);
// The wrong bytes are the vtable entries (pointer values),
// which decode as garbage instructions, not sub/nop/ret
QVERIFY2(!wrongAsm.contains("sub rsp"),
qPrintable("Wrong address should NOT produce sub rsp: " + wrongAsm));
}
void testHoverFlow_fullSimulation() {
// Full simulation of the hover flow as implemented in editor.cpp:
//
// 1. Compose the tree to get LineMeta with correct offsetAddr
// 2. For each FuncPtr64 line, read pointer value from provider
// using lm.offsetAddr (absolute address)
// 3. Read code bytes from the REAL provider using ptrVal directly
// (the real provider can read any process address; snapshot cannot)
// 4. Disassemble the code bytes
//
// The key distinction: step 2 reads from composed tree addresses (in
// the snapshot), step 3 reads from arbitrary code addresses (needs
// the real provider, not snapshot).
QByteArray mem(8192, '\0');
auto w64 = [&](int off, uint64_t val) {
memcpy(mem.data() + off, &val, 8);
};
// Layout:
// [0x000] Root struct: __vptr -> vtable at 0x100
// [0x100] VTable: func0 -> 0x1000, func1 -> 0x1800
// [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret
// [0x1800] func1 code: xor eax, eax; ret
w64(0x000, (uint64_t)0x100); // __vptr
w64(0x100, (uint64_t)0x1000); // vtable[0]
w64(0x108, (uint64_t)0x1800); // vtable[1]
// func0 code
memcpy(mem.data() + 0x1000, "\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
// func1 code
memcpy(mem.data() + 0x1800, "\x31\xc0\xc3", 3);
// This provider represents the real process memory.
BufferProvider realProv(mem);
// Build a snapshot that only contains tree-data pages (like the
// async refresh does). The snapshot does NOT contain function code pages.
// This simulates the real scenario where SnapshotProvider only has
// pages for the root struct and pointer-expanded structs.
QByteArray snapData(0x200, '\0'); // only pages for root + vtable
memcpy(snapData.data(), mem.constData(), 0x200);
BufferProvider snapProv(snapData);
// Build node tree
NodeTree tree;
tree.baseAddress = 0;
Node root; root.kind = NodeKind::Struct; root.name = "Obj";
root.parentId = 0; root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node vtDef; vtDef.kind = NodeKind::Struct; vtDef.name = "VTable";
vtDef.parentId = 0; vtDef.offset = 0x2000;
int vti = tree.addNode(vtDef);
uint64_t vtId = tree.nodes[vti].id;
Node fp0; fp0.kind = NodeKind::FuncPtr64; fp0.name = "func0";
fp0.parentId = vtId; fp0.offset = 0;
tree.addNode(fp0);
Node fp1; fp1.kind = NodeKind::FuncPtr64; fp1.name = "func1";
fp1.parentId = vtId; fp1.offset = 8;
tree.addNode(fp1);
Node vptr; vptr.kind = NodeKind::Pointer64; vptr.name = "__vptr";
vptr.parentId = rootId; vptr.offset = 0; vptr.refId = vtId;
tree.addNode(vptr);
// Compose with the snapshot (like production: compose uses snapshot)
ComposeResult result = compose(tree, snapProv);
// Find expanded FuncPtr64 lines
for (int i = 0; i < result.meta.size(); i++) {
const LineMeta& lm = result.meta[i];
if (lm.nodeKind != NodeKind::FuncPtr64 || lm.lineKind != LineKind::Field)
continue;
if (lm.offsetAddr < 0x100 || lm.offsetAddr >= 0x200)
continue; // skip standalone VTable definition entries
// --- Hover step 1: read pointer value from snapshot ---
uint64_t provAddr = lm.offsetAddr;
// The snapshot has this data (vtable pages are in it)
QVERIFY2(snapProv.isReadable(provAddr, 8),
qPrintable(QString("Snapshot should have vtable page at %1")
.arg(provAddr, 0, 16)));
uint64_t ptrVal = snapProv.readU64(provAddr);
QVERIFY2(ptrVal != 0, "Function pointer should not be zero");
// --- Hover step 2: read code from REAL provider ---
// The snapshot does NOT have the code pages:
uint64_t codeAddr = ptrVal;
QVERIFY2(!snapProv.isReadable(codeAddr, 1),
"Snapshot should NOT have function code pages");
// But the real provider does:
QByteArray codeBytes(128, Qt::Uninitialized);
bool readOk = realProv.read(codeAddr, codeBytes.data(), 128);
QVERIFY2(readOk, "Real provider should be able to read code bytes");
// --- Hover step 3: disassemble ---
QString asm_ = disassemble(codeBytes, ptrVal, 64, 128);
QVERIFY2(!asm_.isEmpty(), qPrintable("Empty disasm for line " + QString::number(i)));
QStringList lines = asm_.split('\n');
const Node& node = tree.nodes[lm.nodeIdx];
if (node.name == "func0") {
QVERIFY(lines.size() >= 4);
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
QCOMPARE(mnemonic(lines[2]), QStringLiteral("sub rsp, 0x20"));
QCOMPARE(mnemonic(lines[3]), QStringLiteral("ret"));
} else if (node.name == "func1") {
QVERIFY(lines.size() >= 2);
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
}
}
}
};
QTEST_MAIN(TestDisasm)
#include "test_disasm.moc"

File diff suppressed because it is too large Load Diff

360
tests/test_export_xml.cpp Normal file
View File

@@ -0,0 +1,360 @@
#include <QtTest/QtTest>
#include <QTemporaryFile>
#include "core.h"
#include "imports/export_reclass_xml.h"
#include "imports/import_reclass_xml.h"
using namespace rcx;
class TestExportXml : public QObject {
Q_OBJECT
private slots:
void exportEmptyTree();
void exportSingleStruct();
void exportPointerRef();
void exportEmbeddedStruct();
void exportArray();
void exportTextNodes();
void exportVectors();
void exportHexCollapse();
void exportMultiClass();
void roundTripImportExport();
};
static int countRoots(const NodeTree& tree) {
int n = 0;
for (const auto& node : tree.nodes)
if (node.parentId == 0 && node.kind == NodeKind::Struct) n++;
return n;
}
static QVector<int> childrenOf(const NodeTree& tree, uint64_t parentId) {
QVector<int> result;
for (int i = 0; i < tree.nodes.size(); i++)
if (tree.nodes[i].parentId == parentId) result.append(i);
return result;
}
static QString exportToString(const NodeTree& tree) {
QTemporaryFile tmp;
tmp.setAutoRemove(true);
if (!tmp.open()) return {};
QString path = tmp.fileName();
tmp.close();
QString err;
if (!exportReclassXml(tree, path, &err)) return {};
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
return QString::fromUtf8(f.readAll());
}
static NodeTree roundTrip(const NodeTree& tree) {
QTemporaryFile tmp;
tmp.setAutoRemove(true);
if (!tmp.open()) return {};
QString path = tmp.fileName();
tmp.close();
QString err;
if (!exportReclassXml(tree, path, &err)) return {};
return importReclassXml(path, &err);
}
// ── Tests ──
void TestExportXml::exportEmptyTree() {
NodeTree tree;
QString err;
QVERIFY(!exportReclassXml(tree, "dummy.xml", &err));
QVERIFY(!err.isEmpty());
}
void TestExportXml::exportSingleStruct() {
NodeTree tree;
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Player");
s.structTypeName = QStringLiteral("Player"); s.parentId = 0;
int si = tree.addNode(s);
uint64_t sid = tree.nodes[si].id;
Node f1; f1.kind = NodeKind::Int32; f1.name = QStringLiteral("health");
f1.parentId = sid; f1.offset = 0; tree.addNode(f1);
Node f2; f2.kind = NodeKind::Float; f2.name = QStringLiteral("speed");
f2.parentId = sid; f2.offset = 4; tree.addNode(f2);
Node f3; f3.kind = NodeKind::UInt64; f3.name = QStringLiteral("id");
f3.parentId = sid; f3.offset = 8; tree.addNode(f3);
QString xml = exportToString(tree);
QVERIFY(!xml.isEmpty());
QVERIFY(xml.contains(QStringLiteral("Player")));
QVERIFY(xml.contains(QStringLiteral("health")));
QVERIFY(xml.contains(QStringLiteral("speed")));
QVERIFY(xml.contains(QStringLiteral("ReClassEx")));
// Round-trip
NodeTree rt = roundTrip(tree);
QCOMPARE(countRoots(rt), 1);
QCOMPARE(rt.nodes[0].name, QStringLiteral("Player"));
auto kids = childrenOf(rt, rt.nodes[0].id);
QCOMPARE(kids.size(), 3);
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::Int32);
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::Float);
QCOMPARE(rt.nodes[kids[2]].kind, NodeKind::UInt64);
}
void TestExportXml::exportPointerRef() {
NodeTree tree;
Node s1; s1.kind = NodeKind::Struct; s1.name = QStringLiteral("Target");
s1.structTypeName = QStringLiteral("Target"); s1.parentId = 0;
int s1i = tree.addNode(s1);
uint64_t s1id = tree.nodes[s1i].id;
Node f; f.kind = NodeKind::Int32; f.name = QStringLiteral("val");
f.parentId = s1id; f.offset = 0; tree.addNode(f);
Node s2; s2.kind = NodeKind::Struct; s2.name = QStringLiteral("HasPtr");
s2.structTypeName = QStringLiteral("HasPtr"); s2.parentId = 0;
int s2i = tree.addNode(s2);
uint64_t s2id = tree.nodes[s2i].id;
Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QStringLiteral("pTarget");
ptr.parentId = s2id; ptr.offset = 0; ptr.refId = s1id;
tree.addNode(ptr);
QString xml = exportToString(tree);
QVERIFY(xml.contains(QStringLiteral("Pointer=\"Target\"")));
// Round-trip: pointer should resolve
NodeTree rt = roundTrip(tree);
QCOMPARE(countRoots(rt), 2);
bool foundPtr = false;
for (const auto& n : rt.nodes) {
if (n.kind == NodeKind::Pointer64 && n.name == QStringLiteral("pTarget")) {
QVERIFY(n.refId != 0);
foundPtr = true;
}
}
QVERIFY(foundPtr);
}
void TestExportXml::exportEmbeddedStruct() {
NodeTree tree;
Node inner; inner.kind = NodeKind::Struct; inner.name = QStringLiteral("Inner");
inner.structTypeName = QStringLiteral("Inner"); inner.parentId = 0;
int ii = tree.addNode(inner);
uint64_t iid = tree.nodes[ii].id;
Node iv; iv.kind = NodeKind::Int32; iv.name = QStringLiteral("x");
iv.parentId = iid; iv.offset = 0; tree.addNode(iv);
Node outer; outer.kind = NodeKind::Struct; outer.name = QStringLiteral("Outer");
outer.structTypeName = QStringLiteral("Outer"); outer.parentId = 0;
int oi = tree.addNode(outer);
uint64_t oid = tree.nodes[oi].id;
Node embed; embed.kind = NodeKind::Struct; embed.name = QStringLiteral("embedded");
embed.structTypeName = QStringLiteral("Inner"); embed.parentId = oid;
embed.offset = 0; embed.refId = iid;
tree.addNode(embed);
QString xml = exportToString(tree);
QVERIFY(xml.contains(QStringLiteral("Instance=\"Inner\"")));
}
void TestExportXml::exportArray() {
NodeTree tree;
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Container");
s.structTypeName = QStringLiteral("Container"); s.parentId = 0;
int si = tree.addNode(s);
uint64_t sid = tree.nodes[si].id;
Node arr; arr.kind = NodeKind::Array; arr.name = QStringLiteral("items");
arr.parentId = sid; arr.offset = 0; arr.arrayLen = 10;
arr.elementKind = NodeKind::Int32;
tree.addNode(arr);
QString xml = exportToString(tree);
QVERIFY(xml.contains(QStringLiteral("Total=\"10\"")));
QVERIFY(xml.contains(QStringLiteral("<Array")));
}
void TestExportXml::exportTextNodes() {
NodeTree tree;
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("TextStruct");
s.structTypeName = QStringLiteral("TextStruct"); s.parentId = 0;
int si = tree.addNode(s);
uint64_t sid = tree.nodes[si].id;
Node u8; u8.kind = NodeKind::UTF8; u8.name = QStringLiteral("name");
u8.parentId = sid; u8.offset = 0; u8.strLen = 32; tree.addNode(u8);
Node u16; u16.kind = NodeKind::UTF16; u16.name = QStringLiteral("wname");
u16.parentId = sid; u16.offset = 32; u16.strLen = 16; tree.addNode(u16);
NodeTree rt = roundTrip(tree);
QCOMPARE(countRoots(rt), 1);
auto kids = childrenOf(rt, rt.nodes[0].id);
QCOMPARE(kids.size(), 2);
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::UTF8);
QCOMPARE(rt.nodes[kids[0]].strLen, 32);
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::UTF16);
QCOMPARE(rt.nodes[kids[1]].strLen, 16);
}
void TestExportXml::exportVectors() {
NodeTree tree;
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Vectors");
s.structTypeName = QStringLiteral("Vectors"); s.parentId = 0;
int si = tree.addNode(s);
uint64_t sid = tree.nodes[si].id;
Node v2; v2.kind = NodeKind::Vec2; v2.name = QStringLiteral("pos2");
v2.parentId = sid; v2.offset = 0; tree.addNode(v2);
Node v3; v3.kind = NodeKind::Vec3; v3.name = QStringLiteral("pos3");
v3.parentId = sid; v3.offset = 8; tree.addNode(v3);
Node v4; v4.kind = NodeKind::Vec4; v4.name = QStringLiteral("rot");
v4.parentId = sid; v4.offset = 20; tree.addNode(v4);
Node m; m.kind = NodeKind::Mat4x4; m.name = QStringLiteral("matrix");
m.parentId = sid; m.offset = 36; tree.addNode(m);
NodeTree rt = roundTrip(tree);
auto kids = childrenOf(rt, rt.nodes[0].id);
QCOMPARE(kids.size(), 4);
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::Vec2);
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::Vec3);
QCOMPARE(rt.nodes[kids[2]].kind, NodeKind::Vec4);
QCOMPARE(rt.nodes[kids[3]].kind, NodeKind::Mat4x4);
}
void TestExportXml::exportHexCollapse() {
NodeTree tree;
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("HexTest");
s.structTypeName = QStringLiteral("HexTest"); s.parentId = 0;
int si = tree.addNode(s);
uint64_t sid = tree.nodes[si].id;
// 4 consecutive Hex8 nodes should collapse to one Custom node
for (int i = 0; i < 4; i++) {
Node h; h.kind = NodeKind::Hex8; h.parentId = sid; h.offset = i;
tree.addNode(h);
}
// Followed by a real field
Node f; f.kind = NodeKind::Int32; f.name = QStringLiteral("val");
f.parentId = sid; f.offset = 4; tree.addNode(f);
QString xml = exportToString(tree);
// Should have Type="21" (Custom) for the collapsed hex
QVERIFY(xml.contains(QStringLiteral("Type=\"21\"")));
// Size should be 4
QVERIFY(xml.contains(QStringLiteral("Size=\"4\"")));
// Round-trip: custom expands back to hex nodes
NodeTree rt = roundTrip(tree);
QCOMPARE(countRoots(rt), 1);
auto kids = childrenOf(rt, rt.nodes[0].id);
// Import expands Custom(4 bytes) to best-fit hex: Hex32 (1 node) + Int32 = 2
QVERIFY(kids.size() >= 2);
// Last child should be Int32
QCOMPARE(rt.nodes[kids.last()].kind, NodeKind::Int32);
}
void TestExportXml::exportMultiClass() {
NodeTree tree;
for (int c = 0; c < 5; c++) {
Node s; s.kind = NodeKind::Struct;
s.name = QStringLiteral("Class%1").arg(c);
s.structTypeName = s.name; s.parentId = 0;
int si = tree.addNode(s);
uint64_t sid = tree.nodes[si].id;
Node f; f.kind = NodeKind::Int32;
f.name = QStringLiteral("field%1").arg(c);
f.parentId = sid; f.offset = 0; tree.addNode(f);
}
NodeTree rt = roundTrip(tree);
QCOMPARE(countRoots(rt), 5);
// All class names preserved
QSet<QString> names;
for (const auto& n : rt.nodes)
if (n.parentId == 0 && n.kind == NodeKind::Struct) names.insert(n.name);
for (int c = 0; c < 5; c++)
QVERIFY(names.contains(QStringLiteral("Class%1").arg(c)));
}
void TestExportXml::roundTripImportExport() {
// Build a comprehensive tree and verify it survives export->import
NodeTree tree;
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("FullTest");
s.structTypeName = QStringLiteral("FullTest"); s.parentId = 0;
int si = tree.addNode(s);
uint64_t sid = tree.nodes[si].id;
int offset = 0;
auto addField = [&](NodeKind kind, const QString& name) {
Node n; n.kind = kind; n.name = name; n.parentId = sid; n.offset = offset;
tree.addNode(n);
offset += sizeForKind(kind);
};
addField(NodeKind::Int8, QStringLiteral("a"));
addField(NodeKind::Int16, QStringLiteral("b"));
addField(NodeKind::Int32, QStringLiteral("c"));
addField(NodeKind::Int64, QStringLiteral("d"));
addField(NodeKind::UInt8, QStringLiteral("e"));
addField(NodeKind::UInt16, QStringLiteral("f"));
addField(NodeKind::UInt32, QStringLiteral("g"));
addField(NodeKind::UInt64, QStringLiteral("h"));
addField(NodeKind::Float, QStringLiteral("i"));
addField(NodeKind::Double, QStringLiteral("j"));
addField(NodeKind::Vec2, QStringLiteral("k"));
addField(NodeKind::Vec3, QStringLiteral("l"));
addField(NodeKind::Vec4, QStringLiteral("m"));
// Self-pointer
Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QStringLiteral("self");
ptr.parentId = sid; ptr.offset = offset; ptr.refId = sid;
tree.addNode(ptr);
offset += 8;
// UTF8
Node u8; u8.kind = NodeKind::UTF8; u8.name = QStringLiteral("str");
u8.parentId = sid; u8.offset = offset; u8.strLen = 64;
tree.addNode(u8);
NodeTree rt = roundTrip(tree);
QCOMPARE(countRoots(rt), 1);
QCOMPARE(rt.nodes[0].name, QStringLiteral("FullTest"));
auto origKids = childrenOf(tree, sid);
auto rtKids = childrenOf(rt, rt.nodes[0].id);
QCOMPARE(rtKids.size(), origKids.size());
// Verify each field kind matches
for (int i = 0; i < origKids.size(); i++) {
QCOMPARE(rt.nodes[rtKids[i]].kind, tree.nodes[origKids[i]].kind);
QCOMPARE(rt.nodes[rtKids[i]].name, tree.nodes[origKids[i]].name);
}
// Verify self-pointer resolved
bool foundSelf = false;
for (const auto& n : rt.nodes) {
if (n.name == QStringLiteral("self") && n.kind == NodeKind::Pointer64) {
QVERIFY(n.refId != 0);
QCOMPARE(n.refId, rt.nodes[0].id);
foundSelf = true;
}
}
QVERIFY(foundSelf);
}
QTEST_MAIN(TestExportXml)
#include "test_export_xml.moc"

237
tests/test_import_pdb.cpp Normal file
View File

@@ -0,0 +1,237 @@
#include <QtTest/QtTest>
#include "core.h"
#include "imports/import_pdb.h"
using namespace rcx;
class TestImportPdb : public QObject {
Q_OBJECT
private slots:
void missingFileReturnsError();
void importKProcess();
void verifyDispatcherHeader();
void verifyListEntry();
void importFilteredStruct();
void enumerateTypes();
void importSelected();
};
static const QString kPdbPath = QStringLiteral(
"C:/Symbols/ntkrnlmp.pdb/0762CF42EF7F3E8116EF7329ADAA09A31/ntkrnlmp.pdb");
// Find a root struct by structTypeName
static int findRootStruct(const NodeTree& tree, const QString& name) {
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].parentId == 0 &&
tree.nodes[i].kind == NodeKind::Struct &&
tree.nodes[i].structTypeName == name)
return i;
}
return -1;
}
// Find a child of parentId by name
static int findChildNode(const NodeTree& tree, uint64_t parentId, const QString& name) {
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].parentId == parentId && tree.nodes[i].name == name)
return i;
}
return -1;
}
void TestImportPdb::missingFileReturnsError() {
QString err;
NodeTree tree = importPdb(QStringLiteral("C:/nonexistent.pdb"), {}, &err);
QVERIFY(tree.nodes.isEmpty());
QVERIFY(!err.isEmpty());
}
void TestImportPdb::importKProcess() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
// Find _KPROCESS root struct
int kpIdx = findRootStruct(tree, QStringLiteral("_KPROCESS"));
QVERIFY2(kpIdx >= 0, "Expected _KPROCESS root struct");
uint64_t kpId = tree.nodes[kpIdx].id;
// Verify Header field at offset 0 → embedded _DISPATCHER_HEADER
int headerIdx = findChildNode(tree, kpId, QStringLiteral("Header"));
QVERIFY2(headerIdx >= 0, "Expected 'Header' child of _KPROCESS");
QCOMPARE(tree.nodes[headerIdx].kind, NodeKind::Struct);
QCOMPARE(tree.nodes[headerIdx].structTypeName, QStringLiteral("_DISPATCHER_HEADER"));
QCOMPARE(tree.nodes[headerIdx].offset, 0);
// Verify ProfileListHead at offset 0x18 → embedded _LIST_ENTRY
int profileIdx = findChildNode(tree, kpId, QStringLiteral("ProfileListHead"));
QVERIFY2(profileIdx >= 0, "Expected 'ProfileListHead' child of _KPROCESS");
QCOMPARE(tree.nodes[profileIdx].kind, NodeKind::Struct);
QCOMPARE(tree.nodes[profileIdx].structTypeName, QStringLiteral("_LIST_ENTRY"));
QCOMPARE(tree.nodes[profileIdx].offset, 0x18);
}
void TestImportPdb::verifyDispatcherHeader() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
// _DISPATCHER_HEADER should be imported as a transitive dependency
int dhIdx = findRootStruct(tree, QStringLiteral("_DISPATCHER_HEADER"));
QVERIFY2(dhIdx >= 0, "_DISPATCHER_HEADER should be imported as a dependency");
uint64_t dhId = tree.nodes[dhIdx].id;
auto kids = tree.childrenOf(dhId);
QVERIFY2(!kids.isEmpty(), "_DISPATCHER_HEADER should have children (fields)");
// Look for WaitListHead — a _LIST_ENTRY at offset 0x10 in most builds
int waitIdx = findChildNode(tree, dhId, QStringLiteral("WaitListHead"));
QVERIFY2(waitIdx >= 0, "Expected 'WaitListHead' in _DISPATCHER_HEADER");
QCOMPARE(tree.nodes[waitIdx].kind, NodeKind::Struct);
QCOMPARE(tree.nodes[waitIdx].structTypeName, QStringLiteral("_LIST_ENTRY"));
}
void TestImportPdb::verifyListEntry() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_KPROCESS"), &err);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
// _LIST_ENTRY should be imported (used by ProfileListHead and others)
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
QVERIFY2(leIdx >= 0, "_LIST_ENTRY should be imported");
uint64_t leId = tree.nodes[leIdx].id;
// Flink at offset 0 — pointer to _LIST_ENTRY
int flinkIdx = findChildNode(tree, leId, QStringLiteral("Flink"));
QVERIFY2(flinkIdx >= 0, "Expected 'Flink' in _LIST_ENTRY");
QCOMPARE(tree.nodes[flinkIdx].kind, NodeKind::Pointer64);
QCOMPARE(tree.nodes[flinkIdx].offset, 0);
// Blink at offset 8 — pointer to _LIST_ENTRY
int blinkIdx = findChildNode(tree, leId, QStringLiteral("Blink"));
QVERIFY2(blinkIdx >= 0, "Expected 'Blink' in _LIST_ENTRY");
QCOMPARE(tree.nodes[blinkIdx].kind, NodeKind::Pointer64);
QCOMPARE(tree.nodes[blinkIdx].offset, 8);
// Both should point back to _LIST_ENTRY (self-referencing)
QCOMPARE(tree.nodes[flinkIdx].refId, leId);
QCOMPARE(tree.nodes[blinkIdx].refId, leId);
}
void TestImportPdb::importFilteredStruct() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
NodeTree tree = importPdb(kPdbPath, QStringLiteral("_LIST_ENTRY"), &err);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
QVERIFY(leIdx >= 0);
// _LIST_ENTRY only references itself, so exactly 1 root struct
int rootCount = 0;
for (const auto& n : tree.nodes)
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
QCOMPARE(rootCount, 1);
}
void TestImportPdb::enumerateTypes() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
QString err;
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
QVERIFY2(!types.isEmpty(), qPrintable(err));
// Should have hundreds of types in ntkrnlmp
QVERIFY2(types.size() > 100,
qPrintable(QStringLiteral("Expected >100 types, got %1").arg(types.size())));
// Verify _KPROCESS is present
bool foundKProcess = false;
bool foundListEntry = false;
for (const auto& t : types) {
if (t.name == QStringLiteral("_KPROCESS")) {
foundKProcess = true;
QVERIFY2(t.childCount > 0, "_KPROCESS should have children");
QVERIFY2(t.size > 0, "_KPROCESS should have non-zero size");
}
if (t.name == QStringLiteral("_LIST_ENTRY")) {
foundListEntry = true;
}
}
QVERIFY2(foundKProcess, "_KPROCESS not found in enumerated types");
QVERIFY2(foundListEntry, "_LIST_ENTRY not found in enumerated types");
}
void TestImportPdb::importSelected() {
if (!QFile::exists(kPdbPath))
QSKIP("ntkrnlmp.pdb not found at expected path");
// First enumerate to find _LIST_ENTRY's type index
QString err;
QVector<PdbTypeInfo> types = enumeratePdbTypes(kPdbPath, &err);
QVERIFY2(!types.isEmpty(), qPrintable(err));
uint32_t listEntryIdx = 0;
bool found = false;
for (const auto& t : types) {
if (t.name == QStringLiteral("_LIST_ENTRY")) {
listEntryIdx = t.typeIndex;
found = true;
break;
}
}
QVERIFY2(found, "_LIST_ENTRY not found in enumeration");
// Import just _LIST_ENTRY
QVector<uint32_t> indices = { listEntryIdx };
int progressCalls = 0;
NodeTree tree = importPdbSelected(kPdbPath, indices, &err,
[&](int cur, int total) -> bool {
progressCalls++;
Q_UNUSED(total);
Q_ASSERT(cur <= total);
return true; // don't cancel
});
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(err));
QVERIFY(progressCalls > 0);
// Verify _LIST_ENTRY root struct
int leIdx = findRootStruct(tree, QStringLiteral("_LIST_ENTRY"));
QVERIFY2(leIdx >= 0, "_LIST_ENTRY should be imported");
// Flink and Blink
uint64_t leId = tree.nodes[leIdx].id;
int flinkIdx = findChildNode(tree, leId, QStringLiteral("Flink"));
QVERIFY2(flinkIdx >= 0, "Expected 'Flink' in _LIST_ENTRY");
QCOMPARE(tree.nodes[flinkIdx].kind, NodeKind::Pointer64);
int blinkIdx = findChildNode(tree, leId, QStringLiteral("Blink"));
QVERIFY2(blinkIdx >= 0, "Expected 'Blink' in _LIST_ENTRY");
QCOMPARE(tree.nodes[blinkIdx].kind, NodeKind::Pointer64);
// Self-referencing pointers
QCOMPARE(tree.nodes[flinkIdx].refId, leId);
QCOMPARE(tree.nodes[blinkIdx].refId, leId);
// Only 1 root struct
int rootCount = 0;
for (const auto& n : tree.nodes)
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
QCOMPARE(rootCount, 1);
}
QTEST_MAIN(TestImportPdb)
#include "test_import_pdb.moc"

View File

@@ -0,0 +1,846 @@
#include <QtTest/QtTest>
#include "core.h"
#include "imports/import_source.h"
using namespace rcx;
class TestImportSource : public QObject {
Q_OBJECT
private slots:
// Basic type tests
void emptyInput();
void noStructs();
void singleEmptyStruct();
void stdintTypes();
void windowsTypes();
void platformPointerTypes();
void standardCTypes();
void multiWordTypes();
void floatDouble();
void boolType();
// Pointer tests
void voidPointer();
void typedPointer();
void selfReferencingPointer();
void doublePointer();
// Array tests
void primitiveArray();
void charArrayToUtf8();
void wcharArrayToUtf16();
void floatArrayToVec2();
void floatArrayToVec3();
void floatArrayToVec4();
void floatArray4x4ToMat4x4();
void genericFloatArray();
void structArray();
// Comment offset tests
void commentOffsets();
void computedOffsets();
void mixedOffsetsAutoDetect();
// Multi-struct tests
void multiStruct();
void pointerCrossRef();
// Forward declarations
void forwardDeclaration();
// Union handling
void unionPickFirst();
// Padding fields
void paddingFieldExpansion();
// static_assert
void staticAssertTailPadding();
// Embedded struct
void embeddedStruct();
// Typedef
void typedefBasic();
// Qualifiers
void constVolatileQualifiers();
void structPrefixOnType();
// Edge cases
void bitfieldSkipped();
void hexArraySizes();
void windowsStylePEB();
void classKeyword();
void inheritanceSkipped();
// Round-trip test (requires generator.h)
void basicRoundTrip();
};
// ── Helper ──
static int countRoots(const NodeTree& tree) {
int n = 0;
for (const auto& node : tree.nodes)
if (node.parentId == 0 && node.kind == NodeKind::Struct) n++;
return n;
}
static QVector<int> childrenOf(const NodeTree& tree, uint64_t parentId) {
QVector<int> result;
for (int i = 0; i < tree.nodes.size(); i++)
if (tree.nodes[i].parentId == parentId) result.append(i);
return result;
}
// ── Tests ──
void TestImportSource::emptyInput() {
QString err;
NodeTree tree = importFromSource(QString(), &err);
QVERIFY(tree.nodes.isEmpty());
QVERIFY(!err.isEmpty());
}
void TestImportSource::noStructs() {
QString err;
NodeTree tree = importFromSource(QStringLiteral("int x = 42;"), &err);
QVERIFY(tree.nodes.isEmpty());
QVERIFY(!err.isEmpty());
}
void TestImportSource::singleEmptyStruct() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Empty {};\n"
));
QCOMPARE(countRoots(tree), 1);
QCOMPARE(tree.nodes[0].name, QStringLiteral("Empty"));
QCOMPARE(tree.nodes[0].kind, NodeKind::Struct);
}
void TestImportSource::stdintTypes() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Test {\n"
" uint8_t a;\n"
" int8_t b;\n"
" uint16_t c;\n"
" int16_t d;\n"
" uint32_t e;\n"
" int32_t f;\n"
" uint64_t g;\n"
" int64_t h;\n"
"};\n"
));
QCOMPARE(countRoots(tree), 1);
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 8);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int8);
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt16);
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::Int16);
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt32);
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Int32);
QCOMPARE(tree.nodes[kids[6]].kind, NodeKind::UInt64);
QCOMPARE(tree.nodes[kids[7]].kind, NodeKind::Int64);
}
void TestImportSource::windowsTypes() {
NodeTree tree = importFromSource(QStringLiteral(
"struct WinTypes {\n"
" BYTE a;\n"
" WORD b;\n"
" DWORD c;\n"
" QWORD d;\n"
" ULONG e;\n"
" LONG f;\n"
" USHORT g;\n"
" UCHAR h;\n"
" BOOLEAN i;\n"
" BOOL j;\n"
" CHAR k;\n"
" WCHAR l;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 12);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8); // BYTE
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::UInt16); // WORD
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt32); // DWORD
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt64); // QWORD
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt32); // ULONG
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Int32); // LONG
QCOMPARE(tree.nodes[kids[6]].kind, NodeKind::UInt16); // USHORT
QCOMPARE(tree.nodes[kids[7]].kind, NodeKind::UInt8); // UCHAR
QCOMPARE(tree.nodes[kids[8]].kind, NodeKind::UInt8); // BOOLEAN
QCOMPARE(tree.nodes[kids[9]].kind, NodeKind::Int32); // BOOL
QCOMPARE(tree.nodes[kids[10]].kind, NodeKind::Int8); // CHAR
QCOMPARE(tree.nodes[kids[11]].kind, NodeKind::UInt16); // WCHAR
}
void TestImportSource::platformPointerTypes() {
NodeTree tree = importFromSource(QStringLiteral(
"struct PtrTypes {\n"
" PVOID a;\n"
" HANDLE b;\n"
" SIZE_T c;\n"
" ULONG_PTR d;\n"
" uintptr_t e;\n"
" size_t f;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 6);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Pointer64);
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt64);
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt64);
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt64);
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::UInt64);
}
void TestImportSource::standardCTypes() {
NodeTree tree = importFromSource(QStringLiteral(
"struct CTypes {\n"
" char a;\n"
" short b;\n"
" int c;\n"
" long d;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 4);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Int8); // char
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int16); // short
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::Int32); // int
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::Int32); // long
}
void TestImportSource::multiWordTypes() {
NodeTree tree = importFromSource(QStringLiteral(
"struct MultiWord {\n"
" unsigned char a;\n"
" unsigned short b;\n"
" unsigned int c;\n"
" unsigned long d;\n"
" long long e;\n"
" unsigned long long f;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 6);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::UInt16);
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt32);
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt32);
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::Int64);
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::UInt64);
}
void TestImportSource::floatDouble() {
NodeTree tree = importFromSource(QStringLiteral(
"struct FD {\n"
" float a;\n"
" double b;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 2);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Double);
}
void TestImportSource::boolType() {
NodeTree tree = importFromSource(QStringLiteral(
"struct B {\n"
" bool a;\n"
" _Bool b;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 2);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Bool);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Bool);
}
void TestImportSource::voidPointer() {
NodeTree tree = importFromSource(QStringLiteral(
"struct VP {\n"
" void* ptr;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("ptr"));
QCOMPARE(tree.nodes[kids[0]].refId, uint64_t(0)); // void* has no target
}
void TestImportSource::typedPointer() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Target {\n"
" int x;\n"
"};\n"
"struct HasPtr {\n"
" Target* pTarget;\n"
"};\n"
));
QCOMPARE(countRoots(tree), 2);
// Find HasPtr
int hasPtrIdx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == QStringLiteral("HasPtr") && tree.nodes[i].parentId == 0) {
hasPtrIdx = i; break;
}
}
QVERIFY(hasPtrIdx >= 0);
auto kids = childrenOf(tree, tree.nodes[hasPtrIdx].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
QVERIFY(tree.nodes[kids[0]].refId != 0);
// refId should point to Target struct
int targetIdx = tree.indexOfId(tree.nodes[kids[0]].refId);
QVERIFY(targetIdx >= 0);
QCOMPARE(tree.nodes[targetIdx].name, QStringLiteral("Target"));
}
void TestImportSource::selfReferencingPointer() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Node {\n"
" int value;\n"
" Node* next;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 2);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Pointer64);
QCOMPARE(tree.nodes[kids[1]].refId, tree.nodes[0].id);
}
void TestImportSource::doublePointer() {
NodeTree tree = importFromSource(QStringLiteral(
"struct DP {\n"
" void** ppData;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
}
void TestImportSource::primitiveArray() {
NodeTree tree = importFromSource(QStringLiteral(
"struct PA {\n"
" int32_t values[10];\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
QCOMPARE(tree.nodes[kids[0]].arrayLen, 10);
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Int32);
}
void TestImportSource::charArrayToUtf8() {
NodeTree tree = importFromSource(QStringLiteral(
"struct CA {\n"
" char name[64];\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UTF8);
QCOMPARE(tree.nodes[kids[0]].strLen, 64);
}
void TestImportSource::wcharArrayToUtf16() {
NodeTree tree = importFromSource(QStringLiteral(
"struct WC {\n"
" wchar_t name[32];\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UTF16);
QCOMPARE(tree.nodes[kids[0]].strLen, 32);
}
void TestImportSource::floatArrayToVec2() {
NodeTree tree = importFromSource(QStringLiteral(
"struct V {\n"
" float pos[2];\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec2);
}
void TestImportSource::floatArrayToVec3() {
NodeTree tree = importFromSource(QStringLiteral(
"struct V {\n"
" float pos[3];\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec3);
}
void TestImportSource::floatArrayToVec4() {
NodeTree tree = importFromSource(QStringLiteral(
"struct V {\n"
" float rot[4];\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec4);
}
void TestImportSource::floatArray4x4ToMat4x4() {
NodeTree tree = importFromSource(QStringLiteral(
"struct M {\n"
" float matrix[4][4];\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Mat4x4);
}
void TestImportSource::genericFloatArray() {
NodeTree tree = importFromSource(QStringLiteral(
"struct GF {\n"
" float values[8];\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
QCOMPARE(tree.nodes[kids[0]].arrayLen, 8);
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Float);
}
void TestImportSource::structArray() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Item {\n"
" int id;\n"
"};\n"
"struct Container {\n"
" Item items[5];\n"
"};\n"
));
QCOMPARE(countRoots(tree), 2);
// Find Container
int contIdx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == QStringLiteral("Container") && tree.nodes[i].parentId == 0) {
contIdx = i; break;
}
}
QVERIFY(contIdx >= 0);
auto kids = childrenOf(tree, tree.nodes[contIdx].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
QCOMPARE(tree.nodes[kids[0]].arrayLen, 5);
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Struct);
}
void TestImportSource::commentOffsets() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Offsets {\n"
" uint64_t vtable; // 0x0\n"
" float health; // 0x8\n"
" uint8_t _pad000C[0x4]; // 0xC\n"
" double score; // 0x10\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
// vtable at 0x0
QCOMPARE(tree.nodes[kids[0]].offset, 0);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt64);
// health at 0x8
QCOMPARE(tree.nodes[kids[1]].offset, 8);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Float);
// _pad at 0xC -> hex nodes
// score at 0x10
// Find the double
bool foundDouble = false;
for (int k : kids) {
if (tree.nodes[k].kind == NodeKind::Double) {
QCOMPARE(tree.nodes[k].offset, 0x10);
foundDouble = true;
}
}
QVERIFY(foundDouble);
}
void TestImportSource::computedOffsets() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Computed {\n"
" uint8_t a;\n"
" uint16_t b;\n"
" uint32_t c;\n"
" uint64_t d;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 4);
QCOMPARE(tree.nodes[kids[0]].offset, 0); // uint8_t at 0
QCOMPARE(tree.nodes[kids[1]].offset, 1); // uint16_t at 1
QCOMPARE(tree.nodes[kids[2]].offset, 3); // uint32_t at 3
QCOMPARE(tree.nodes[kids[3]].offset, 7); // uint64_t at 7
}
void TestImportSource::mixedOffsetsAutoDetect() {
// If any field has a comment offset, all should use comment mode
NodeTree tree = importFromSource(QStringLiteral(
"struct Mixed {\n"
" uint32_t a; // 0x0\n"
" uint32_t b;\n"
" uint32_t c; // 0x10\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(tree.nodes[kids[0]].offset, 0);
// b has no comment offset, in comment mode it gets computed offset 4
QCOMPARE(tree.nodes[kids[1]].offset, 4);
// c has comment offset 0x10
QCOMPARE(tree.nodes[kids[2]].offset, 0x10);
}
void TestImportSource::multiStruct() {
NodeTree tree = importFromSource(QStringLiteral(
"struct A {\n"
" int x;\n"
"};\n"
"struct B {\n"
" float y;\n"
"};\n"
"struct C {\n"
" double z;\n"
"};\n"
));
QCOMPARE(countRoots(tree), 3);
}
void TestImportSource::pointerCrossRef() {
NodeTree tree = importFromSource(QStringLiteral(
"struct A {\n"
" int value;\n"
"};\n"
"struct B {\n"
" A* ref;\n"
"};\n"
));
// Find B's pointer field
int bIdx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == QStringLiteral("B") && tree.nodes[i].parentId == 0) {
bIdx = i; break;
}
}
QVERIFY(bIdx >= 0);
auto kids = childrenOf(tree, tree.nodes[bIdx].id);
QCOMPARE(kids.size(), 1);
QVERIFY(tree.nodes[kids[0]].refId != 0);
// Should point to A
int aIdx = tree.indexOfId(tree.nodes[kids[0]].refId);
QVERIFY(aIdx >= 0);
QCOMPARE(tree.nodes[aIdx].name, QStringLiteral("A"));
}
void TestImportSource::forwardDeclaration() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Bar;\n"
"struct Foo {\n"
" Bar* pBar;\n"
"};\n"
"struct Bar {\n"
" int val;\n"
"};\n"
));
QCOMPARE(countRoots(tree), 2);
// Foo's pBar should resolve to Bar
int fooIdx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == QStringLiteral("Foo") && tree.nodes[i].parentId == 0) {
fooIdx = i; break;
}
}
QVERIFY(fooIdx >= 0);
auto kids = childrenOf(tree, tree.nodes[fooIdx].id);
QCOMPARE(kids.size(), 1);
QVERIFY(tree.nodes[kids[0]].refId != 0);
}
void TestImportSource::unionPickFirst() {
NodeTree tree = importFromSource(QStringLiteral(
"struct WithUnion {\n"
" union {\n"
" float asFloat;\n"
" uint32_t asInt;\n"
" };\n"
" int after;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
// Should have 2 fields: asFloat (first union member) + after
QCOMPARE(kids.size(), 2);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("asFloat"));
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
}
void TestImportSource::paddingFieldExpansion() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Padded {\n"
" uint8_t _pad0000[0x10];\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
// 0x10 = 16 bytes, should be 2x Hex64 (best fit)
QCOMPARE(kids.size(), 2);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Hex64);
QCOMPARE(tree.nodes[kids[0]].offset, 0);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex64);
QCOMPARE(tree.nodes[kids[1]].offset, 8);
}
void TestImportSource::staticAssertTailPadding() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Sized {\n"
" uint32_t x;\n"
"};\n"
"static_assert(sizeof(Sized) == 0x10, \"Size check\");\n"
));
// x is 4 bytes, static_assert says 0x10 = 16
// Should have tail padding from offset 4 to 16 (12 bytes)
int span = tree.structSpan(tree.nodes[0].id);
QCOMPARE(span, 0x10);
}
void TestImportSource::embeddedStruct() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Inner {\n"
" int a;\n"
"};\n"
"struct Outer {\n"
" Inner embedded;\n"
" float after;\n"
"};\n"
));
int outerIdx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == QStringLiteral("Outer") && tree.nodes[i].parentId == 0) {
outerIdx = i; break;
}
}
QVERIFY(outerIdx >= 0);
auto kids = childrenOf(tree, tree.nodes[outerIdx].id);
QCOMPARE(kids.size(), 2);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Struct);
QCOMPARE(tree.nodes[kids[0]].structTypeName, QStringLiteral("Inner"));
QVERIFY(tree.nodes[kids[0]].refId != 0);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Float);
}
void TestImportSource::typedefBasic() {
NodeTree tree = importFromSource(QStringLiteral(
"typedef uint32_t MyInt;\n"
"struct TD {\n"
" MyInt value;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
}
void TestImportSource::constVolatileQualifiers() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Quals {\n"
" const uint32_t a;\n"
" volatile int32_t b;\n"
" const volatile uint8_t c;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 3);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt8);
}
void TestImportSource::structPrefixOnType() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Inner {\n"
" int val;\n"
"};\n"
"struct Outer {\n"
" struct Inner member;\n"
"};\n"
));
int outerIdx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == QStringLiteral("Outer") && tree.nodes[i].parentId == 0) {
outerIdx = i; break;
}
}
QVERIFY(outerIdx >= 0);
auto kids = childrenOf(tree, tree.nodes[outerIdx].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Struct);
QCOMPARE(tree.nodes[kids[0]].structTypeName, QStringLiteral("Inner"));
}
void TestImportSource::bitfieldSkipped() {
NodeTree tree = importFromSource(QStringLiteral(
"struct BF {\n"
" uint32_t normal;\n"
" uint32_t bitA : 4;\n"
" uint32_t bitB : 12;\n"
" uint32_t after;\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
// Bitfields should be skipped, only normal + after
QCOMPARE(kids.size(), 2);
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
}
void TestImportSource::hexArraySizes() {
NodeTree tree = importFromSource(QStringLiteral(
"struct HexArr {\n"
" uint8_t data[0x20];\n"
"};\n"
));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
QCOMPARE(tree.nodes[kids[0]].arrayLen, 0x20);
}
void TestImportSource::windowsStylePEB() {
// Test with Windows PEB-style struct (no comment offsets)
NodeTree tree = importFromSource(QStringLiteral(
"struct PEB64 {\n"
" BOOLEAN InheritedAddressSpace;\n"
" BOOLEAN ReadImageFileExecOptions;\n"
" BOOLEAN BeingDebugged;\n"
" BOOLEAN BitField;\n"
" PVOID Mutant;\n"
" PVOID ImageBaseAddress;\n"
"};\n"
));
QCOMPARE(countRoots(tree), 1);
QCOMPARE(tree.nodes[0].name, QStringLiteral("PEB64"));
auto kids = childrenOf(tree, tree.nodes[0].id);
QCOMPARE(kids.size(), 6);
// First 4 are BOOLEAN (UInt8)
for (int i = 0; i < 4; i++)
QCOMPARE(tree.nodes[kids[i]].kind, NodeKind::UInt8);
// Last 2 are PVOID (Pointer64)
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::Pointer64);
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Pointer64);
}
void TestImportSource::classKeyword() {
NodeTree tree = importFromSource(QStringLiteral(
"class MyClass {\n"
" int value;\n"
"};\n"
));
QCOMPARE(countRoots(tree), 1);
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("class"));
}
void TestImportSource::inheritanceSkipped() {
NodeTree tree = importFromSource(QStringLiteral(
"struct Base {\n"
" int a;\n"
"};\n"
"struct Derived : public Base {\n"
" float b;\n"
"};\n"
));
QCOMPARE(countRoots(tree), 2);
int derivedIdx = -1;
for (int i = 0; i < tree.nodes.size(); i++) {
if (tree.nodes[i].name == QStringLiteral("Derived") && tree.nodes[i].parentId == 0) {
derivedIdx = i; break;
}
}
QVERIFY(derivedIdx >= 0);
auto kids = childrenOf(tree, tree.nodes[derivedIdx].id);
QCOMPARE(kids.size(), 1);
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
}
void TestImportSource::basicRoundTrip() {
// Build a simple tree manually, export it, then re-import and compare
NodeTree original;
{
Node s;
s.kind = NodeKind::Struct;
s.name = QStringLiteral("RoundTrip");
s.structTypeName = QStringLiteral("RoundTrip");
s.parentId = 0;
s.offset = 0;
int sIdx = original.addNode(s);
uint64_t sId = original.nodes[sIdx].id;
Node f1;
f1.kind = NodeKind::UInt32;
f1.name = QStringLiteral("field_a");
f1.parentId = sId;
f1.offset = 0;
original.addNode(f1);
Node f2;
f2.kind = NodeKind::Float;
f2.name = QStringLiteral("field_b");
f2.parentId = sId;
f2.offset = 4;
original.addNode(f2);
Node f3;
f3.kind = NodeKind::UInt64;
f3.name = QStringLiteral("field_c");
f3.parentId = sId;
f3.offset = 8;
original.addNode(f3);
}
// Create source text that matches what generator would produce
QString source = QStringLiteral(
"struct RoundTrip {\n"
" uint32_t field_a; // 0x0\n"
" float field_b; // 0x4\n"
" uint64_t field_c; // 0x8\n"
"};\n"
"static_assert(sizeof(RoundTrip) == 0x10, \"Size mismatch\");\n"
);
NodeTree reimported = importFromSource(source);
QCOMPARE(countRoots(reimported), 1);
QCOMPARE(reimported.nodes[0].name, QStringLiteral("RoundTrip"));
auto origKids = childrenOf(original, original.nodes[0].id);
auto reimpKids = childrenOf(reimported, reimported.nodes[0].id);
// Compare field count (reimported may have extra padding nodes from static_assert)
// Check that the first 3 fields match
QVERIFY(reimpKids.size() >= 3);
for (int i = 0; i < 3; i++) {
QCOMPARE(reimported.nodes[reimpKids[i]].kind, original.nodes[origKids[i]].kind);
QCOMPARE(reimported.nodes[reimpKids[i]].name, original.nodes[origKids[i]].name);
QCOMPARE(reimported.nodes[reimpKids[i]].offset, original.nodes[origKids[i]].offset);
}
}
QTEST_MAIN(TestImportSource)
#include "test_import_source.moc"

70
tests/test_import_xml.cpp Normal file
View File

@@ -0,0 +1,70 @@
#include <QtTest/QtTest>
#include "core.h"
#include "imports/import_reclass_xml.h"
using namespace rcx;
class TestImportXml : public QObject {
Q_OBJECT
private slots:
void importSmallXml();
};
void TestImportXml::importSmallXml() {
// Create a minimal XML in a temp file and test parsing
QTemporaryFile tmp;
tmp.setAutoRemove(true);
QVERIFY(tmp.open());
tmp.write(R"(<?xml version="1.0" encoding="UTF-8"?>
<ReClass>
<!--ReClassEx-->
<Class Name="TestClass" Type="28" Comment="" Offset="0" strOffset="0" Code="">
<Node Name="vtable" Type="9" Size="8" bHidden="false" Comment=""/>
<Node Name="health" Type="13" Size="4" bHidden="false" Comment=""/>
<Node Name="name" Type="18" Size="32" bHidden="false" Comment=""/>
<Node Name="position" Type="23" Size="12" bHidden="false" Comment=""/>
<Node Name="pNext" Type="8" Size="8" bHidden="false" Comment="" Pointer="TestClass"/>
</Class>
</ReClass>
)");
tmp.flush();
QString error;
NodeTree tree = importReclassXml(tmp.fileName(), &error);
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
// Should have 1 root struct + 5 children = 6 nodes
QCOMPARE(tree.nodes.size(), 6);
// Root struct
QCOMPARE(tree.nodes[0].kind, NodeKind::Struct);
QCOMPARE(tree.nodes[0].name, QStringLiteral("TestClass"));
// vtable = Int64
QCOMPARE(tree.nodes[1].kind, NodeKind::Int64);
QCOMPARE(tree.nodes[1].name, QStringLiteral("vtable"));
QCOMPARE(tree.nodes[1].offset, 0);
// health = Float
QCOMPARE(tree.nodes[2].kind, NodeKind::Float);
QCOMPARE(tree.nodes[2].name, QStringLiteral("health"));
QCOMPARE(tree.nodes[2].offset, 8);
// name = UTF8 with strLen=32
QCOMPARE(tree.nodes[3].kind, NodeKind::UTF8);
QCOMPARE(tree.nodes[3].strLen, 32);
QCOMPARE(tree.nodes[3].offset, 12);
// position = Vec3
QCOMPARE(tree.nodes[4].kind, NodeKind::Vec3);
QCOMPARE(tree.nodes[4].offset, 44);
// pNext = Pointer64 with resolved refId
QCOMPARE(tree.nodes[5].kind, NodeKind::Pointer64);
QCOMPARE(tree.nodes[5].name, QStringLiteral("pNext"));
QVERIFY(tree.nodes[5].refId != 0);
QCOMPARE(tree.nodes[5].refId, tree.nodes[0].id); // points to TestClass
}
QTEST_MAIN(TestImportXml)
#include "test_import_xml.moc"

View File

@@ -0,0 +1,291 @@
#include <QtTest/QTest>
#include <QApplication>
#include <QComboBox>
#include <QCheckBox>
#include <QTreeWidget>
#include <QStackedWidget>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QGroupBox>
#include <QLineEdit>
#include <QSpinBox>
#include <QLabel>
#include "optionsdialog.h"
#include "themes/thememanager.h"
using namespace rcx;
// Helper: apply the global palette the same way main.cpp does
static void applyGlobalTheme(const Theme& theme) {
QPalette pal;
pal.setColor(QPalette::Window, theme.background);
pal.setColor(QPalette::WindowText, theme.text);
pal.setColor(QPalette::Base, theme.background);
pal.setColor(QPalette::AlternateBase, theme.surface);
pal.setColor(QPalette::Text, theme.text);
pal.setColor(QPalette::Button, theme.button);
pal.setColor(QPalette::ButtonText, theme.text);
pal.setColor(QPalette::Highlight, theme.selection);
pal.setColor(QPalette::HighlightedText, theme.text);
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
pal.setColor(QPalette::ToolTipText, theme.text);
pal.setColor(QPalette::Mid, theme.border);
pal.setColor(QPalette::Dark, theme.background);
pal.setColor(QPalette::Light, theme.textFaint);
pal.setColor(QPalette::Link, theme.indHoverSpan);
pal.setColor(QPalette::Disabled, QPalette::WindowText, theme.textMuted);
pal.setColor(QPalette::Disabled, QPalette::Text, theme.textMuted);
pal.setColor(QPalette::Disabled, QPalette::ButtonText, theme.textMuted);
pal.setColor(QPalette::Disabled, QPalette::HighlightedText, theme.textMuted);
pal.setColor(QPalette::Disabled, QPalette::Light, theme.background);
qApp->setPalette(pal);
qApp->setStyleSheet(QString());
}
class TestOptionsDialog : public QObject {
Q_OBJECT
private slots:
void initTestCase() {
// Apply theme palette so dialog inherits real colors
auto& tm = ThemeManager::instance();
applyGlobalTheme(tm.current());
}
void dialogCreatesAllWidgets() {
OptionsResult defaults;
defaults.themeIndex = 0;
defaults.fontName = "JetBrains Mono";
defaults.menuBarTitleCase = true;
defaults.safeMode = false;
defaults.autoStartMcp = false;
OptionsDialog dlg(defaults);
// Core widgets exist
auto* tree = dlg.findChild<QTreeWidget*>();
QVERIFY(tree);
auto* pages = dlg.findChild<QStackedWidget*>();
QVERIFY(pages);
QCOMPARE(pages->count(), 3);
auto* themeCombo = dlg.findChild<QComboBox*>("themeCombo");
QVERIFY(themeCombo);
QVERIFY(themeCombo->count() >= 3);
auto* fontCombo = dlg.findChild<QComboBox*>("fontCombo");
QVERIFY(fontCombo);
QCOMPARE(fontCombo->count(), 2);
auto* showIconCheck = dlg.findChild<QCheckBox*>();
QVERIFY(showIconCheck);
auto* buttons = dlg.findChild<QDialogButtonBox*>();
QVERIFY(buttons);
QVERIFY(buttons->button(QDialogButtonBox::Ok));
QVERIFY(buttons->button(QDialogButtonBox::Cancel));
}
void resultReflectsInput() {
OptionsResult input;
input.themeIndex = 1;
input.fontName = "Consolas";
input.menuBarTitleCase = false;
input.safeMode = true;
input.autoStartMcp = true;
OptionsDialog dlg(input);
auto r = dlg.result();
QCOMPARE(r.themeIndex, 1);
QCOMPARE(r.fontName, QString("Consolas"));
QCOMPARE(r.menuBarTitleCase, false);
QCOMPARE(r.safeMode, true);
QCOMPARE(r.autoStartMcp, true);
}
void noStyleSheetOnDialog() {
OptionsResult defaults;
OptionsDialog dlg(defaults);
// Dialog itself must have no stylesheet override
QVERIFY(dlg.styleSheet().isEmpty());
// Combo boxes must have no stylesheet override
auto* themeCombo = dlg.findChild<QComboBox*>("themeCombo");
QVERIFY(themeCombo->styleSheet().isEmpty());
auto* fontCombo = dlg.findChild<QComboBox*>("fontCombo");
QVERIFY(fontCombo->styleSheet().isEmpty());
// No child widget should have a stylesheet set
for (auto* child : dlg.findChildren<QWidget*>()) {
QVERIFY2(child->styleSheet().isEmpty(),
qPrintable(QString("Widget %1 (%2) has unexpected stylesheet: %3")
.arg(child->objectName(),
child->metaObject()->className(),
child->styleSheet())));
}
}
void highlightColorDiffersFromBackground() {
// Verify the palette Highlight is distinguishable from Window background
// This is the root cause of broken hover: if they're the same, hover is invisible
auto& tm = ThemeManager::instance();
const auto themes = tm.themes();
for (const auto& theme : themes) {
QVERIFY2(theme.selection != theme.background,
qPrintable(QString("Theme '%1': selection == background (%2)")
.arg(theme.name, theme.background.name())));
}
}
void paletteHighlightIsSelection() {
// After applying theme, QPalette::Highlight must be theme.selection (not theme.hover)
auto& tm = ThemeManager::instance();
const auto& theme = tm.current();
applyGlobalTheme(theme);
QPalette pal = qApp->palette();
QCOMPARE(pal.color(QPalette::Highlight), theme.selection);
}
void treePageSwitching() {
OptionsResult defaults;
OptionsDialog dlg(defaults);
auto* tree = dlg.findChild<QTreeWidget*>();
auto* pages = dlg.findChild<QStackedWidget*>();
QVERIFY(tree && pages);
// General is selected by default -> page 0
QCOMPARE(pages->currentIndex(), 0);
// Find "AI Features" item and select it
auto* envItem = tree->topLevelItem(0);
QVERIFY(envItem);
QTreeWidgetItem* aiItem = nullptr;
for (int i = 0; i < envItem->childCount(); ++i) {
if (envItem->child(i)->text(0) == "AI Features") {
aiItem = envItem->child(i);
break;
}
}
QVERIFY(aiItem);
tree->setCurrentItem(aiItem);
QCOMPARE(pages->currentIndex(), 1);
// Switch back to General
QTreeWidgetItem* generalItem = nullptr;
for (int i = 0; i < envItem->childCount(); ++i) {
if (envItem->child(i)->text(0) == "General") {
generalItem = envItem->child(i);
break;
}
}
QVERIFY(generalItem);
tree->setCurrentItem(generalItem);
QCOMPARE(pages->currentIndex(), 0);
}
void searchFilterHidesItems() {
OptionsResult defaults;
OptionsDialog dlg(defaults);
auto* search = dlg.findChild<QLineEdit*>();
auto* tree = dlg.findChild<QTreeWidget*>();
QVERIFY(search && tree);
auto* envItem = tree->topLevelItem(0);
QVERIFY(envItem);
// All children visible initially
for (int i = 0; i < envItem->childCount(); ++i)
QVERIFY(!envItem->child(i)->isHidden());
// Search for "MCP" - should hide General, show AI Features
search->setText("MCP");
QTreeWidgetItem* generalItem = nullptr;
QTreeWidgetItem* aiItem = nullptr;
for (int i = 0; i < envItem->childCount(); ++i) {
auto* child = envItem->child(i);
if (child->text(0) == "General") generalItem = child;
if (child->text(0) == "AI Features") aiItem = child;
}
QVERIFY(generalItem && aiItem);
QVERIFY(generalItem->isHidden());
QVERIFY(!aiItem->isHidden());
// Clear search - all visible again
search->setText("");
QVERIFY(!generalItem->isHidden());
QVERIFY(!aiItem->isHidden());
}
void refreshRateSpinBoxExists() {
OptionsResult defaults;
defaults.refreshMs = 660;
OptionsDialog dlg(defaults);
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
QVERIFY(spin);
QCOMPARE(spin->value(), 660);
QCOMPARE(spin->minimum(), 1);
QCOMPARE(spin->maximum(), 60000);
}
void refreshRateResultReflectsInput() {
OptionsResult input;
input.refreshMs = 200;
OptionsDialog dlg(input);
auto r = dlg.result();
QCOMPARE(r.refreshMs, 200);
// Change via spin box
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
QVERIFY(spin);
spin->setValue(100);
r = dlg.result();
QCOMPARE(r.refreshMs, 100);
}
void refreshRateClampsMin() {
OptionsResult input;
input.refreshMs = 0; // below minimum
OptionsDialog dlg(input);
auto* spin = dlg.findChild<QSpinBox*>("refreshSpin");
QVERIFY(spin);
// QSpinBox clamps to minimum
QCOMPARE(spin->value(), 1);
}
void dialogInheritsPalette() {
auto& tm = ThemeManager::instance();
const auto& theme = tm.current();
applyGlobalTheme(theme);
OptionsResult defaults;
OptionsDialog dlg(defaults);
dlg.show();
QTest::qWaitForWindowExposed(&dlg);
// Dialog's effective palette should match the app palette
QPalette dlgPal = dlg.palette();
QPalette appPal = qApp->palette();
QCOMPARE(dlgPal.color(QPalette::Window), appPal.color(QPalette::Window));
QCOMPARE(dlgPal.color(QPalette::WindowText), appPal.color(QPalette::WindowText));
QCOMPARE(dlgPal.color(QPalette::Highlight), appPal.color(QPalette::Highlight));
QCOMPARE(dlgPal.color(QPalette::Button), appPal.color(QPalette::Button));
QCOMPARE(dlgPal.color(QPalette::ButtonText), appPal.color(QPalette::ButtonText));
// Highlight must be visible against background
QVERIFY(dlgPal.color(QPalette::Highlight) != dlgPal.color(QPalette::Window));
}
};
QTEST_MAIN(TestOptionsDialog)
#include "test_options_dialog.moc"

View File

@@ -0,0 +1,246 @@
#include <QtTest/QTest>
#include <QApplication>
#include <QSplitter>
#include <QDir>
#include <QFile>
#include <Qsci/qsciscintilla.h>
#include "controller.h"
#include "core.h"
#include "providers/null_provider.h"
#include "providers/buffer_provider.h"
using namespace rcx;
static void buildTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
Node root;
root.kind = NodeKind::Struct;
root.structTypeName = "TestClass";
root.name = "TestClass";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f;
f.kind = NodeKind::Hex64;
f.name = "field_00";
f.parentId = rootId;
f.offset = 0;
tree.addNode(f);
}
class TestSourceManagement : public QObject {
Q_OBJECT
private:
RcxDocument* m_doc = nullptr;
RcxController* m_ctrl = nullptr;
QSplitter* m_splitter = nullptr;
// Helper: write a temp binary file and return its path
QString writeTempFile(const QString& name, const QByteArray& data) {
QString path = QDir::tempPath() + "/" + name;
QFile f(path);
f.open(QIODevice::WriteOnly);
f.write(data);
f.close();
return path;
}
// Helper: directly add a file source entry (bypasses QFileDialog)
void addFileSource(const QString& path, const QString& displayName) {
m_doc->loadData(path);
SavedSourceEntry entry;
entry.kind = QStringLiteral("File");
entry.displayName = displayName;
entry.filePath = path;
entry.baseAddress = m_doc->tree.baseAddress;
// Access saved sources through selectSource's internal mechanism
// We manually add since selectSource("File") opens a dialog
m_ctrl->document()->provider = std::make_shared<BufferProvider>(
QFile(path).readAll().isEmpty() ? QByteArray(64, '\0') : QByteArray(64, '\0'));
// Use the test accessor pattern from controller
}
private slots:
void init() {
m_doc = new RcxDocument();
buildTree(m_doc->tree);
m_splitter = new QSplitter();
m_ctrl = new RcxController(m_doc, nullptr);
m_ctrl->addSplitEditor(m_splitter);
m_splitter->resize(800, 600);
m_splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(m_splitter));
QApplication::processEvents();
}
void cleanup() {
delete m_ctrl; m_ctrl = nullptr;
delete m_splitter; m_splitter = nullptr;
delete m_doc; m_doc = nullptr;
}
// ── Initial state: NullProvider, no saved sources ──
void testInitialProviderIsNull() {
QVERIFY(m_doc->provider != nullptr);
QCOMPARE(m_doc->provider->size(), 0);
QVERIFY(!m_doc->provider->isValid());
QCOMPARE(m_ctrl->savedSources().size(), 0);
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
}
// ── Loading binary data creates a valid provider ──
void testLoadDataCreatesValidProvider() {
QByteArray data(128, '\xAB');
m_doc->loadData(data);
QApplication::processEvents();
QVERIFY(m_doc->provider->isValid());
QCOMPARE(m_doc->provider->size(), 128);
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0xAB);
}
// ── clearSources resets to NullProvider ──
void testClearSourcesResetsToNull() {
// Load some data first so provider is valid
QByteArray data(64, '\xFF');
m_doc->loadData(data);
QApplication::processEvents();
QVERIFY(m_doc->provider->isValid());
m_ctrl->clearSources();
QApplication::processEvents();
// Provider should be NullProvider
QVERIFY(!m_doc->provider->isValid());
QCOMPARE(m_doc->provider->size(), 0);
// Saved sources should be empty
QCOMPARE(m_ctrl->savedSources().size(), 0);
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
}
// ── clearSources clears value history ──
void testClearSourcesClearsValueHistory() {
// The value history is cleared via resetSnapshot inside clearSources
m_ctrl->clearSources();
QApplication::processEvents();
QVERIFY(m_ctrl->valueHistory().isEmpty());
}
// ── clearSources clears dataPath ──
void testClearSourcesClearsDataPath() {
QString path = writeTempFile("rcx_test_src.bin", QByteArray(64, '\xCC'));
m_doc->loadData(path);
QVERIFY(!m_doc->dataPath.isEmpty());
m_ctrl->clearSources();
QApplication::processEvents();
QVERIFY(m_doc->dataPath.isEmpty());
QFile::remove(path);
}
// ── selectSource("#clear") calls clearSources ──
void testSelectSourceClearCommand() {
QByteArray data(64, '\xFF');
m_doc->loadData(data);
QVERIFY(m_doc->provider->isValid());
m_ctrl->selectSource(QStringLiteral("#clear"));
QApplication::processEvents();
QVERIFY(!m_doc->provider->isValid());
QCOMPARE(m_ctrl->savedSources().size(), 0);
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
}
// ── clearSources then refresh still works (compose doesn't crash) ──
void testClearSourcesThenRefreshWorks() {
m_ctrl->clearSources();
QApplication::processEvents();
// refresh() is called internally by clearSources; verify it didn't crash
// and the editor still has content (the tree structure is intact)
auto* editor = m_ctrl->editors().first();
QVERIFY(editor != nullptr);
}
// ── Multiple clearSources calls are safe (idempotent) ──
void testMultipleClearSourcesIdempotent() {
m_ctrl->clearSources();
m_ctrl->clearSources();
m_ctrl->clearSources();
QApplication::processEvents();
QVERIFY(!m_doc->provider->isValid());
QCOMPARE(m_ctrl->savedSources().size(), 0);
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
}
// ── switchToSavedSource with invalid index is no-op ──
void testSwitchInvalidIndexNoOp() {
m_ctrl->switchSource(-1);
m_ctrl->switchSource(999);
QApplication::processEvents();
// Should still be in initial state
QCOMPARE(m_ctrl->activeSourceIndex(), -1);
}
// ── Provider read fails after clear (all zeros) ──
void testProviderReadFailsAfterClear() {
QByteArray data(64, '\xAB');
m_doc->loadData(data);
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0xAB);
m_ctrl->clearSources();
QApplication::processEvents();
// NullProvider: read returns false, readU8 returns 0
uint8_t buf = 0xFF;
QVERIFY(!m_doc->provider->read(0, &buf, 1));
QCOMPARE(m_doc->provider->readU8(0), (uint8_t)0);
}
// ── clearSources resets snapshot state ──
void testClearSourcesResetsSnapshot() {
QByteArray data(64, '\x00');
m_doc->loadData(data);
QApplication::processEvents();
m_ctrl->clearSources();
QApplication::processEvents();
// After clear, the value history should be empty (resetSnapshot was called)
QVERIFY(m_ctrl->valueHistory().isEmpty());
}
// ── NullProvider name is empty (triggers "source" placeholder in command row) ──
void testNullProviderNameEmpty() {
m_ctrl->clearSources();
QApplication::processEvents();
QVERIFY(m_doc->provider->name().isEmpty());
}
};
QTEST_MAIN(TestSourceManagement)
#include "test_source_management.moc"

View File

@@ -5,6 +5,7 @@
#include <QElapsedTimer>
#include <QVBoxLayout>
#include <QToolButton>
#include <QButtonGroup>
#include <QLineEdit>
#include <QListView>
#include <QStringListModel>
@@ -21,7 +22,7 @@ Q_DECLARE_METATYPE(rcx::TypeEntry)
using namespace rcx;
static void buildTwoRootTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node a;
a.kind = NodeKind::Struct;
@@ -62,7 +63,7 @@ private slots:
// ── Chevron span detection ──
void testChevronSpanDetected() {
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {");
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {");
ColumnSpan span = commandRowChevronSpan(text);
QVERIFY(span.valid);
QCOMPARE(span.start, 0);
@@ -79,7 +80,7 @@ private slots:
// ── Existing spans unbroken by chevron prefix ──
void testSpansWithPrefix() {
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {");
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {");
ColumnSpan src = commandRowSrcSpan(text);
QVERIFY(src.valid);
@@ -498,6 +499,7 @@ private slots:
TypeSpec spec = parseTypeSpec("Ball*");
QCOMPARE(spec.baseName, QString("Ball"));
QVERIFY(spec.isPointer);
QCOMPARE(spec.ptrDepth, 1);
QCOMPARE(spec.arrayCount, 0);
}
@@ -505,6 +507,7 @@ private slots:
TypeSpec spec = parseTypeSpec("Ball**");
QCOMPARE(spec.baseName, QString("Ball"));
QVERIFY(spec.isPointer);
QCOMPARE(spec.ptrDepth, 2);
}
void testParseTypeSpecEmpty() {
@@ -736,6 +739,732 @@ private slots:
QVERIFY(listView);
QVERIFY(listView->model()->rowCount() > 2);
}
// ── FieldType popup: primitive with [n] creates an array ──
void testFieldTypePrimitiveArrayCreation() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Find the "x" field (Int32)
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
// Simulate the primitive-array path of applyTypePopupResult:
// beginMacro → changeNodeKind(Array) → ChangeArrayMeta → endMacro
doc->undoStack.beginMacro(QStringLiteral("Change to primitive array"));
ctrl->changeNodeKind(xIdx, NodeKind::Array);
xIdx = doc->tree.indexOfId(xNodeId);
QVERIFY(xIdx >= 0);
doc->undoStack.push(new RcxCommand(ctrl,
cmd::ChangeArrayMeta{xNodeId, doc->tree.nodes[xIdx].elementKind,
NodeKind::Int32,
doc->tree.nodes[xIdx].arrayLen, 4}));
doc->undoStack.endMacro();
QApplication::processEvents();
// Node should now be an Array
xIdx = doc->tree.indexOfId(xNodeId);
QVERIFY(xIdx >= 0);
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Array);
QCOMPARE(doc->tree.nodes[xIdx].elementKind, NodeKind::Int32);
QCOMPARE(doc->tree.nodes[xIdx].arrayLen, 4);
// Single undo reverses the entire macro
doc->undoStack.undo();
QApplication::processEvents();
xIdx = doc->tree.indexOfId(xNodeId);
QVERIFY(xIdx >= 0);
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32);
delete ctrl;
delete splitter;
delete doc;
}
// ── Test: SVG icon and gutter scale with font size ──
void testDelegateIconScalesWithFont() {
// Create a popup and set two different font sizes.
// The delegate sizeHint row height should scale with font.
TypeSelectorPopup popup;
TypeEntry prim;
prim.entryKind = TypeEntry::Primitive;
prim.primitiveKind = NodeKind::Int32;
prim.displayName = QStringLiteral("int32_t");
TypeEntry comp;
comp.entryKind = TypeEntry::Composite;
comp.structId = 100;
comp.displayName = QStringLiteral("TestStruct");
comp.classKeyword = QStringLiteral("struct");
// Small font
QFont small(QStringLiteral("Consolas"), 9);
popup.setFont(small);
popup.setTypes({prim, comp});
popup.popup(QPoint(-9999, -9999)); // offscreen
QApplication::processEvents();
auto* listView = popup.findChild<QListView*>();
QVERIFY(listView);
auto* delegate = listView->itemDelegate();
QVERIFY(delegate);
// Find first non-section row for consistent measurement
int dataRow = -1;
for (int i = 0; i < listView->model()->rowCount(); i++) {
QSize h = delegate->sizeHint(QStyleOptionViewItem(), listView->model()->index(i, 0));
// Non-section rows are taller (font.height + 8 vs + 2)
if (h.height() > QFontMetrics(small).height() + 4) { dataRow = i; break; }
}
QVERIFY2(dataRow >= 0, "Should find a non-section row");
QSize smallHint = delegate->sizeHint(QStyleOptionViewItem(), listView->model()->index(dataRow, 0));
popup.hide();
// Large font (simulates zoomed editor)
QFont large(QStringLiteral("Consolas"), 18);
popup.setFont(large);
popup.setTypes({prim, comp});
popup.popup(QPoint(-9999, -9999));
QApplication::processEvents();
QSize largeHint = delegate->sizeHint(QStyleOptionViewItem(), listView->model()->index(dataRow, 0));
popup.hide();
// Large font should produce taller rows than small font
QVERIFY2(largeHint.height() > smallHint.height(),
qPrintable(QString("Large hint %1 should be > small hint %2")
.arg(largeHint.height()).arg(smallHint.height())));
// The ratio should roughly match the font size ratio (18/9 = 2x)
double ratio = double(largeHint.height()) / double(smallHint.height());
QVERIFY2(ratio > 1.4, qPrintable(QString("Row height ratio %1 should be > 1.4").arg(ratio)));
}
void testPopupWidthScalesWithFont() {
TypeSelectorPopup popup;
TypeEntry comp;
comp.entryKind = TypeEntry::Composite;
comp.structId = 100;
comp.displayName = QStringLiteral("MyLongStructName");
comp.classKeyword = QStringLiteral("struct");
popup.setTypes({comp});
// Small font
QFont small(QStringLiteral("Consolas"), 9);
popup.setFont(small);
popup.popup(QPoint(-9999, -9999));
QApplication::processEvents();
int smallW = popup.width();
popup.hide();
// Large font
QFont large(QStringLiteral("Consolas"), 18);
popup.setFont(large);
popup.setTypes({comp});
popup.popup(QPoint(-9999, -9999));
QApplication::processEvents();
int largeW = popup.width();
popup.hide();
// Popup with larger font should be wider
QVERIFY2(largeW > smallW,
qPrintable(QString("Large popup width %1 should be > small %2")
.arg(largeW).arg(smallW)));
}
// ── Test: popup updates colors when theme changes ──
void testPopupUpdatesOnThemeChange() {
auto& tm = ThemeManager::instance();
int origIdx = tm.currentIndex();
// Ensure at least two themes exist
QVERIFY2(tm.themes().size() >= 2,
"Need at least 2 themes to test theme switching");
// Create popup with current theme
TypeSelectorPopup popup;
TypeEntry prim;
prim.entryKind = TypeEntry::Primitive;
prim.primitiveKind = NodeKind::Int32;
prim.displayName = QStringLiteral("int32_t");
popup.setTypes({prim});
QColor bgBefore = popup.palette().color(QPalette::Window);
// Switch to a different theme
int otherIdx = (origIdx == 0) ? 1 : 0;
tm.setCurrent(otherIdx);
QApplication::processEvents();
// The popup should have applyTheme connected to themeChanged
popup.applyTheme(tm.current());
QColor bgAfter = popup.palette().color(QPalette::Window);
// If the two themes have different background colors, verify the change
// (some themes may coincidentally share colors, so we just verify the
// method doesn't crash and the palette is set to the new theme's color)
QCOMPARE(bgAfter, tm.current().backgroundAlt);
// Also verify child widgets got updated
auto* filterEdit = popup.findChild<QLineEdit*>();
QVERIFY(filterEdit);
QCOMPARE(filterEdit->palette().color(QPalette::Base),
tm.current().background);
auto* listView = popup.findChild<QListView*>();
QVERIFY(listView);
QCOMPARE(listView->palette().color(QPalette::Base),
tm.current().background);
// Restore original theme
tm.setCurrent(origIdx);
}
void testPopupAutoConnectsThemeChange() {
auto& tm = ThemeManager::instance();
int origIdx = tm.currentIndex();
QVERIFY2(tm.themes().size() >= 2, "Need >= 2 themes");
TypeSelectorPopup popup;
// applyTheme is a public slot — verify it can be connected
connect(&tm, &ThemeManager::themeChanged,
&popup, &TypeSelectorPopup::applyTheme);
QColor bgBefore = popup.palette().color(QPalette::Window);
int otherIdx = (origIdx == 0) ? 1 : 0;
tm.setCurrent(otherIdx);
QApplication::processEvents();
// After theme change + signal, popup palette should match new theme
QCOMPARE(popup.palette().color(QPalette::Window),
tm.current().backgroundAlt);
// Restore
tm.setCurrent(origIdx);
}
// ── parseTypeSpec: primitive pointer ptrDepth ──
void testParseTypeSpecPrimitiveStar() {
TypeSpec spec = parseTypeSpec("int32_t*");
QCOMPARE(spec.baseName, QString("int32_t"));
QVERIFY(spec.isPointer);
QCOMPARE(spec.ptrDepth, 1);
QCOMPARE(spec.arrayCount, 0);
}
void testParseTypeSpecPrimitiveDoubleStar() {
TypeSpec spec = parseTypeSpec("f64**");
QCOMPARE(spec.baseName, QString("f64"));
QVERIFY(spec.isPointer);
QCOMPARE(spec.ptrDepth, 2);
QCOMPARE(spec.arrayCount, 0);
}
// ── Primitive pointer creation via applyTypePopupResult path ──
void testPrimitivePointerCreation() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Find the "x" field (Int32) inside Alpha
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
// Simulate the primitive-pointer path: Int32 → Pointer64 + elementKind=Int32 + ptrDepth=1
doc->undoStack.beginMacro(QStringLiteral("Change to primitive pointer"));
ctrl->changeNodeKind(xIdx, NodeKind::Pointer64);
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
doc->tree.nodes[idx].elementKind = NodeKind::Int32;
doc->tree.nodes[idx].ptrDepth = 1;
doc->undoStack.endMacro();
QApplication::processEvents();
// Verify: Pointer64 with elementKind=Int32, ptrDepth=1, refId=0
idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Int32);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 1);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
// Undo reverses the macro
doc->undoStack.undo();
QApplication::processEvents();
idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Int32);
delete ctrl;
delete splitter;
delete doc;
}
void testDoublePointerCreation() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Find the "x" field (Int32) inside Alpha
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
// Simulate: Int32 → Pointer64 + elementKind=Double + ptrDepth=2
doc->undoStack.beginMacro(QStringLiteral("Change to double pointer"));
ctrl->changeNodeKind(xIdx, NodeKind::Pointer64);
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
doc->tree.nodes[idx].elementKind = NodeKind::Double;
doc->tree.nodes[idx].ptrDepth = 2;
doc->undoStack.endMacro();
QApplication::processEvents();
// Verify: Pointer64 with elementKind=Double, ptrDepth=2
idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Double);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 2);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
// ── ptrDepth JSON round-trip ──
void testPtrDepthJsonRoundTrip() {
Node n;
n.kind = NodeKind::Pointer64;
n.name = "pData";
n.elementKind = NodeKind::Float;
n.ptrDepth = 2;
n.id = 42;
QJsonObject obj = n.toJson();
QCOMPARE(obj["ptrDepth"].toInt(), 2);
Node restored = Node::fromJson(obj);
QCOMPARE(restored.ptrDepth, 2);
QCOMPARE(restored.elementKind, NodeKind::Float);
QCOMPARE(restored.kind, NodeKind::Pointer64);
}
void testPtrDepthJsonDefault() {
// Nodes without ptrDepth in JSON should default to 0
Node n;
n.kind = NodeKind::Pointer64;
n.name = "pVoid";
n.id = 99;
QJsonObject obj = n.toJson();
// ptrDepth==0 is not serialized
QVERIFY(!obj.contains("ptrDepth"));
Node restored = Node::fromJson(obj);
QCOMPARE(restored.ptrDepth, 0);
}
// ── setMode always resets modifier buttons ──
void testSetModeResetsModifierInPointerTargetMode() {
TypeSelectorPopup popup;
// Set FieldType mode and select * modifier
popup.setMode(TypePopupMode::FieldType);
popup.setModifier(1); // select *
// Now switch to PointerTarget mode — should reset to plain
popup.setMode(TypePopupMode::PointerTarget);
// Verify: modifier buttons are hidden but internally reset to plain (modId=0)
// This means primitives will be visible in applyFilter
TypeEntry prim;
prim.entryKind = TypeEntry::Primitive;
prim.primitiveKind = NodeKind::Int32;
prim.displayName = "int32_t";
TypeEntry voidEntry;
voidEntry.entryKind = TypeEntry::Primitive;
voidEntry.primitiveKind = NodeKind::Pointer64;
voidEntry.displayName = "void";
popup.setTypes({prim, voidEntry});
// Both primitives should be visible (not filtered out)
auto* listView = popup.findChild<QListView*>();
QVERIFY(listView);
int rowCount = listView->model()->rowCount();
// Should have section header + 2 primitives = at least 3 rows
QVERIFY2(rowCount >= 3,
qPrintable(QString("Expected >=3 rows (header+2 prims), got %1").arg(rowCount)));
}
// ── setModifier preselection ──
void testSetModifierPreselects() {
TypeSelectorPopup popup;
// Test * preselection
popup.setMode(TypePopupMode::FieldType);
popup.setModifier(1);
auto* btnGroup = popup.findChild<QButtonGroup*>();
QVERIFY(btnGroup);
QCOMPARE(btnGroup->checkedId(), 1);
// Test ** preselection
popup.setMode(TypePopupMode::FieldType);
popup.setModifier(2);
QCOMPARE(btnGroup->checkedId(), 2);
// Test [n] preselection with count
popup.setMode(TypePopupMode::FieldType);
popup.setModifier(3, 8);
QCOMPARE(btnGroup->checkedId(), 3);
auto* countEdit = popup.findChild<QLineEdit*>(QStringLiteral("arrayCountEdit"));
// Array count edit may not have objectName set; find via parent
// Just verify button group is correct
}
// ── isValidPrimitivePtrTarget ──
void testIsValidPrimitivePtrTarget() {
// Hex types → NOT valid (deref shows same hex as void*)
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex8));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex16));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex32));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Hex64));
// Pointer types → NOT valid (use composite * for chains)
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Pointer32));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Pointer64));
// Function pointers → NOT valid
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::FuncPtr32));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::FuncPtr64));
// Containers → NOT valid
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Struct));
QVERIFY(!isValidPrimitivePtrTarget(NodeKind::Array));
// Value types → valid
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Int32));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::UInt64));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Float));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Double));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Bool));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::Vec3));
QVERIFY(isValidPrimitivePtrTarget(NodeKind::UTF8));
}
// ── hex64* falls back to void* ──
void testHex64StarFallsBackToVoidPointer() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Find the "x" field (Int32)
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
// Build a TypeEntry for hex64
TypeEntry hexEntry;
hexEntry.entryKind = TypeEntry::Primitive;
hexEntry.primitiveKind = NodeKind::Hex64;
hexEntry.displayName = "hex64";
// Apply it with pointer modifier (fullText = "hex64*")
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
hexEntry, QStringLiteral("hex64*"));
QApplication::processEvents();
// Should be a void pointer: Pointer64, ptrDepth=0, refId=0
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
void testHex8StarFallsBackToVoidPointer() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
TypeEntry hexEntry;
hexEntry.entryKind = TypeEntry::Primitive;
hexEntry.primitiveKind = NodeKind::Hex8;
hexEntry.displayName = "hex8";
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
hexEntry, QStringLiteral("hex8*"));
QApplication::processEvents();
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
void testPtr64StarFallsBackToVoidPointer() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
TypeEntry ptrEntry;
ptrEntry.entryKind = TypeEntry::Primitive;
ptrEntry.primitiveKind = NodeKind::Pointer64;
ptrEntry.displayName = "ptr64";
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
ptrEntry, QStringLiteral("ptr64*"));
QApplication::processEvents();
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 0);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
// ── Valid primitive pointers still work ──
void testInt32StarStillCreatesPrimitivePointer() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
TypeEntry intEntry;
intEntry.entryKind = TypeEntry::Primitive;
intEntry.primitiveKind = NodeKind::Int32;
intEntry.displayName = "int32_t";
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
intEntry, QStringLiteral("int32_t*"));
QApplication::processEvents();
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 1);
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Int32);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
void testDoubleDoubleStarStillCreatesPrimitivePointer() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
TypeEntry dblEntry;
dblEntry.entryKind = TypeEntry::Primitive;
dblEntry.primitiveKind = NodeKind::Double;
dblEntry.displayName = "double";
ctrl->applyTypePopupResult(TypePopupMode::FieldType, xIdx,
dblEntry, QStringLiteral("double**"));
QApplication::processEvents();
int idx = doc->tree.indexOfId(xNodeId);
QVERIFY(idx >= 0);
QCOMPARE(doc->tree.nodes[idx].kind, NodeKind::Pointer64);
QCOMPARE(doc->tree.nodes[idx].ptrDepth, 2);
QCOMPARE(doc->tree.nodes[idx].elementKind, NodeKind::Double);
QCOMPARE(doc->tree.nodes[idx].refId, uint64_t(0));
delete ctrl;
delete splitter;
delete doc;
}
// ── Defense: compose/format treat invalid ptrDepth as void* ──
void testComposeShowsVoidPtrForHexPtrDepth() {
// If a node somehow has ptrDepth>0 with hex elementKind
// (e.g. from old JSON), compose should show "void*" not "hex64*"
NodeTree tree;
tree.baseAddress = 0x1000;
Node root;
root.kind = NodeKind::Struct;
root.name = "Test";
root.structTypeName = "Test";
root.parentId = 0;
tree.addNode(root);
uint64_t rootId = tree.nodes[0].id;
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "badPtr";
ptr.parentId = rootId;
ptr.offset = 0;
ptr.ptrDepth = 1;
ptr.elementKind = NodeKind::Hex64; // invalid target
tree.addNode(ptr);
QByteArray buf(0x100, '\0');
BufferProvider prov(buf);
ComposeResult result = compose(tree, prov);
// The composed text should NOT contain "hex64*" — the invalid target
// should fall through to normal void pointer display
QVERIFY2(!result.text.contains("hex64*"),
qPrintable("Should not show 'hex64*', got: " + result.text));
}
};
QTEST_MAIN(TestTypeSelector)

View File

@@ -0,0 +1,332 @@
#include <QtTest/QTest>
#include <QApplication>
#include <QSplitter>
#include <Qsci/qsciscintilla.h>
#include "controller.h"
#include "typeselectorpopup.h"
#include "core.h"
#include "providers/buffer_provider.h"
using namespace rcx;
static QByteArray makeBuffer() { return QByteArray(0x200, '\0'); }
// Build a tree with one root struct + a Pointer64 field
static void buildPointerTree(NodeTree& tree, const QString& rootName) {
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "instance";
root.structTypeName = rootName;
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node ptr;
ptr.kind = NodeKind::Pointer64;
ptr.name = "ptr";
ptr.parentId = rootId;
ptr.offset = 0;
tree.addNode(ptr);
}
class TestTypeVisibility : public QObject {
Q_OBJECT
private slots:
// ── 1. New types created via createNewTypeRequested get a default name ──
void testCreateNewTypeGetsDefaultName() {
auto* doc = new RcxDocument();
buildPointerTree(doc->tree, "Main");
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int nodesBefore = doc->tree.nodes.size();
// Simulate what createNewTypeRequested does: create struct with default name
// (The actual handler is a lambda; we test the result via tree inspection)
{
bool wasSuppressed = ctrl->document() != nullptr; Q_UNUSED(wasSuppressed);
// Generate unique default name — same logic as the handler
QString baseName = QStringLiteral("NewClass");
QString typeName = baseName;
int counter = 1;
QSet<QString> existing;
for (const auto& nd : doc->tree.nodes) {
if (nd.kind == NodeKind::Struct && !nd.structTypeName.isEmpty())
existing.insert(nd.structTypeName);
}
while (existing.contains(typeName))
typeName = baseName + QString::number(counter++);
Node n;
n.kind = NodeKind::Struct;
n.structTypeName = typeName;
n.name = QStringLiteral("instance");
n.parentId = 0;
n.offset = 0;
n.id = doc->tree.reserveId();
doc->undoStack.push(new RcxCommand(ctrl, cmd::Insert{n}));
}
ctrl->refresh();
QApplication::processEvents();
// Verify new struct was created with a name
QCOMPARE(doc->tree.nodes.size(), nodesBefore + 1);
bool found = false;
for (const auto& n : doc->tree.nodes) {
if (n.structTypeName == "NewClass") { found = true; break; }
}
QVERIFY2(found, "New struct should have structTypeName 'NewClass'");
delete ctrl;
delete splitter;
delete doc;
}
// ── 2. Second new type gets incremented name ──
void testCreateNewTypeIncrementsName() {
auto* doc = new RcxDocument();
buildPointerTree(doc->tree, "Main");
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
// Add a struct already named "NewClass"
{
Node n;
n.kind = NodeKind::Struct;
n.structTypeName = "NewClass";
n.name = "instance";
n.parentId = 0;
n.offset = 0;
doc->tree.addNode(n);
}
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Generate name using same logic
QString baseName = QStringLiteral("NewClass");
QString typeName = baseName;
int counter = 1;
QSet<QString> existing;
for (const auto& nd : doc->tree.nodes) {
if (nd.kind == NodeKind::Struct && !nd.structTypeName.isEmpty())
existing.insert(nd.structTypeName);
}
while (existing.contains(typeName))
typeName = baseName + QString::number(counter++);
QCOMPARE(typeName, QStringLiteral("NewClass1"));
delete ctrl;
delete splitter;
delete doc;
}
// ── 3. Cross-tab: types from other documents visible via project docs ──
void testCrossTabTypesVisible() {
// Doc A: has "Alpha" struct with a Pointer64 field
auto* docA = new RcxDocument();
buildPointerTree(docA->tree, "Alpha");
docA->provider = std::make_unique<BufferProvider>(makeBuffer());
// Doc B: has "Beta" struct
auto* docB = new RcxDocument();
buildPointerTree(docB->tree, "Beta");
docB->provider = std::make_unique<BufferProvider>(makeBuffer());
// Shared doc list (simulates MainWindow::m_allDocs)
QVector<RcxDocument*> allDocs;
allDocs << docA << docB;
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(docA, nullptr);
ctrl->addSplitEditor(splitter);
ctrl->setProjectDocuments(&allDocs);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Find the Pointer64 node in docA
int ptrIdx = -1;
for (int i = 0; i < docA->tree.nodes.size(); i++) {
if (docA->tree.nodes[i].kind == NodeKind::Pointer64) {
ptrIdx = i;
break;
}
}
QVERIFY(ptrIdx >= 0);
// Apply an external type (structId=0, displayName="Beta") as pointer target
TypeEntry extEntry;
extEntry.entryKind = TypeEntry::Composite;
extEntry.structId = 0; // external sentinel
extEntry.displayName = QStringLiteral("Beta");
ctrl->applyTypePopupResult(TypePopupMode::PointerTarget, ptrIdx,
extEntry, QString());
QApplication::processEvents();
// "Beta" should now exist in docA as a local struct (imported)
bool found = false;
uint64_t betaLocalId = 0;
for (const auto& n : docA->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct
&& n.structTypeName == "Beta") {
found = true;
betaLocalId = n.id;
break;
}
}
QVERIFY2(found, "Beta struct should be imported into docA");
// The pointer's refId should point at the local Beta
int ptrIdx2 = -1;
for (int i = 0; i < docA->tree.nodes.size(); i++) {
if (docA->tree.nodes[i].kind == NodeKind::Pointer64
&& docA->tree.nodes[i].name == "ptr") {
ptrIdx2 = i;
break;
}
}
QVERIFY(ptrIdx2 >= 0);
QCOMPARE(docA->tree.nodes[ptrIdx2].refId, betaLocalId);
delete ctrl;
delete splitter;
delete docA;
delete docB;
}
// ── 4. findOrCreateStructByName reuses existing local struct ──
void testFindOrCreateReusesExisting() {
auto* doc = new RcxDocument();
buildPointerTree(doc->tree, "Main");
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
// Add "Target" struct manually
Node target;
target.kind = NodeKind::Struct;
target.structTypeName = "Target";
target.name = "instance";
target.parentId = 0;
target.offset = 0;
int ti = doc->tree.addNode(target);
uint64_t targetId = doc->tree.nodes[ti].id;
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
int nodesBefore = doc->tree.nodes.size();
// Apply external entry with name "Target" — should reuse existing
int ptrIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].kind == NodeKind::Pointer64) {
ptrIdx = i;
break;
}
}
QVERIFY(ptrIdx >= 0);
TypeEntry extEntry;
extEntry.entryKind = TypeEntry::Composite;
extEntry.structId = 0;
extEntry.displayName = QStringLiteral("Target");
ctrl->applyTypePopupResult(TypePopupMode::PointerTarget, ptrIdx,
extEntry, QString());
QApplication::processEvents();
// Should NOT have created a new struct — reused existing one
QCOMPARE(doc->tree.nodes.size(), nodesBefore);
// Pointer should reference the existing Target
int ptrIdx2 = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].kind == NodeKind::Pointer64
&& doc->tree.nodes[i].name == "ptr") {
ptrIdx2 = i;
break;
}
}
QVERIFY(ptrIdx2 >= 0);
QCOMPARE(doc->tree.nodes[ptrIdx2].refId, targetId);
delete ctrl;
delete splitter;
delete doc;
}
// ── 5. External types skip duplicates already in local doc ──
void testExternalTypesSkipLocalDuplicates() {
// Both docs have "Shared" type — should not appear twice
auto* docA = new RcxDocument();
buildPointerTree(docA->tree, "Shared");
docA->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* docB = new RcxDocument();
buildPointerTree(docB->tree, "Shared");
docB->provider = std::make_unique<BufferProvider>(makeBuffer());
QVector<RcxDocument*> allDocs;
allDocs << docA << docB;
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(docA, nullptr);
ctrl->addSplitEditor(splitter);
ctrl->setProjectDocuments(&allDocs);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Count how many "Shared" entries exist in local doc's root structs
int sharedCount = 0;
for (const auto& n : docA->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct
&& n.structTypeName == "Shared")
sharedCount++;
}
QCOMPARE(sharedCount, 1); // only the local one
delete ctrl;
delete splitter;
delete docA;
delete docB;
}
};
QTEST_MAIN(TestTypeVisibility)
#include "test_type_visibility.moc"

View File

@@ -16,7 +16,7 @@ using namespace rcx;
// ── Fixture: small tree with diverse field types ──
static void buildValidationTree(NodeTree& tree) {
tree.baseAddress = 0x1000;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
@@ -483,7 +483,7 @@ private slots:
QVERIFY(!fmt::validateBaseAddress("").isEmpty()); // empty
QVERIFY(!fmt::validateBaseAddress(" ").isEmpty()); // whitespace only - no hex digits
QVERIFY(!fmt::validateBaseAddress("0xGGGG").isEmpty());
QVERIFY(!fmt::validateBaseAddress("0x1000 * 2").isEmpty()); // multiplication not supported
QVERIFY(fmt::validateBaseAddress("0x1000 * 2").isEmpty()); // multiplication supported
QVERIFY(!fmt::validateBaseAddress("0x1000 ++ 0x100").isEmpty()); // double operator
QVERIFY(!fmt::validateBaseAddress("hello").isEmpty());
}
@@ -1028,7 +1028,7 @@ private slots:
// Test the validation function directly
QVERIFY(!fmt::validateBaseAddress("0x1000 ** 2").isEmpty());
QVERIFY(!fmt::validateBaseAddress("0x1000 / 2").isEmpty());
QVERIFY(fmt::validateBaseAddress("0x1000 / 2").isEmpty()); // division supported
QVERIFY(!fmt::validateBaseAddress("abc xyz").isEmpty());
// Original base should be unchanged

View File

@@ -260,17 +260,6 @@ private slots:
qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16);
}
void provider_setBase()
{
WinDbgMemoryProvider prov(m_connString);
QVERIFY(prov.isValid());
uint64_t orig = prov.base();
prov.setBase(0x1000);
QCOMPARE(prov.base(), (uint64_t)0x1000);
prov.setBase(orig);
QCOMPARE(prov.base(), orig);
}
// ── Read: MZ header on main thread ──
void provider_read_mz_mainThread()
@@ -325,7 +314,7 @@ private slots:
// Verify it's not all zeros (the old failure mode)
bool allZero = true;
for (int i = 0; i < data.size(); ++i) {
if (data[i] != 0) { allZero = false; break; }
if (data[i] != '\0') { allZero = false; break; }
}
QVERIFY2(!allZero, "Data is all zeros — background thread read failed");
}

16
third_party/fadec/.build.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
image: alpine/edge
sources:
- https://git.sr.ht/~aengelke/fadec
packages:
- meson
tasks:
- build: |
mkdir fadec-build1
meson fadec-build1 fadec
ninja -C fadec-build1
ninja -C fadec-build1 test
# Complete test with encode2 API.
mkdir fadec-build2
meson fadec-build2 fadec -Dwith_encode2=true
ninja -C fadec-build2
ninja -C fadec-build2 test

View File

@@ -0,0 +1,51 @@
name: CI
on: [push]
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt install -y ninja-build meson
- name: Configure
run: mkdir build; CC=clang CXX=clang++ meson -Dbuildtype=debugoptimized -Dwith_encode2=true build
- name: Build
run: ninja -v -C build
- name: Test
run: meson test -v -C build
build-linux-cmake:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt install -y ninja-build cmake
- name: Configure
run: CC=clang CXX=clang++ cmake -B build -G Ninja -DFADEC_ENCODE2=ON
- name: Build
run: cmake --build build -v
- name: Test
run: ctest --test-dir build -V
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: pip install ninja meson
- name: Configure
run: mkdir build; meson setup --vsenv -Dbuildtype=debugoptimized -Dwith_encode2=true build
- name: Build
run: meson compile -v -C build
- name: Test
run: meson test -v -C build
build-windows-cmake:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -B build -DFADEC_ENCODE2=ON
- name: Build
run: cmake --build build -v
- name: Test
run: ctest --test-dir build -V -C Debug

1
third_party/fadec/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build/

109
third_party/fadec/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,109 @@
cmake_minimum_required(VERSION 3.23)
project(fadec LANGUAGES C)
enable_testing()
# TODO: make this actually optional
enable_language(CXX OPTIONAL)
# Options
set(FADEC_ARCHMODE "both" CACHE STRING "Support only 32-bit x86, 64-bit x86 or both")
set_property(CACHE FADEC_ARCHMODE PROPERTY STRINGS both only32 only64)
option(FADEC_UNDOC "Include undocumented instructions" FALSE)
option(FADEC_DECODE "Include support for decoding" TRUE)
option(FADEC_ENCODE "Include support for encoding" TRUE)
option(FADEC_ENCODE2 "Include support for new encoding API" FALSE)
set(CMAKE_C_STANDARD 11)
if (MSVC)
add_compile_options(/W4 -D_CRT_SECURE_NO_WARNINGS /wd4018 /wd4146 /wd4244 /wd4245 /wd4267 /wd4310)
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-Zc:preprocessor>)
else()
add_compile_options(-Wall -Wextra -Wpedantic -Wno-overlength-strings)
endif()
find_package(Python3 3.9 REQUIRED)
add_library(fadec)
add_library(fadec::fadec ALIAS fadec)
set_target_properties(fadec PROPERTIES
LINKER_LANGUAGE C
)
set(GEN_ARGS "")
if (NOT FADEC_ARCHMODE STREQUAL "only64")
list(APPEND GEN_ARGS "--32")
endif ()
if (NOT FADEC_ARCHMODE STREQUAL "only32")
list(APPEND GEN_ARGS "--64")
endif ()
if (FADEC_UNDOC)
list(APPEND GEN_ARGS "--with-undoc")
endif ()
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include")
function(fadec_component)
cmake_parse_arguments(ARG "" "NAME" "HEADERS;SOURCES" ${ARGN})
set(PRIV_INC ${CMAKE_CURRENT_BINARY_DIR}/include/fadec-${ARG_NAME}-private.inc)
set(PUB_INC ${CMAKE_CURRENT_BINARY_DIR}/include/fadec-${ARG_NAME}-public.inc)
add_custom_command(
OUTPUT ${PRIV_INC} ${PUB_INC}
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/parseinstrs.py ${ARG_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/instrs.txt
${PUB_INC} ${PRIV_INC} ${GEN_ARGS}
DEPENDS instrs.txt parseinstrs.py
COMMENT "Building table for ${ARG_NAME}"
)
list(APPEND FADEC_HEADERS ${PUB_INC})
target_sources(fadec PRIVATE
${ARG_SOURCES}
PUBLIC
FILE_SET HEADERS
BASE_DIRS .
FILES
${ARG_HEADERS}
PUBLIC
FILE_SET generated_public TYPE HEADERS
BASE_DIRS ${CMAKE_CURRENT_BINARY_DIR}/include
FILES
${PUB_INC}
PRIVATE
FILE_SET generated_private TYPE HEADERS
BASE_DIRS ${CMAKE_CURRENT_BINARY_DIR}/include
FILES
${PRIV_INC}
)
add_executable(fadec-${ARG_NAME}-test ${ARG_NAME}-test.c)
target_link_libraries(fadec-${ARG_NAME}-test PRIVATE fadec)
add_test(NAME ${ARG_NAME} COMMAND fadec-${ARG_NAME}-test)
if (CMAKE_CXX_COMPILER AND ${ARG_NAME} STREQUAL "encode2")
add_executable(fadec-${ARG_NAME}-test-cpp ${ARG_NAME}-test.cc)
target_link_libraries(fadec-${ARG_NAME}-test-cpp PRIVATE fadec)
add_test(NAME ${ARG_NAME}-cpp COMMAND fadec-${ARG_NAME}-test-cpp)
endif()
endfunction()
if (FADEC_DECODE)
fadec_component(NAME decode SOURCES decode.c format.c HEADERS fadec.h)
endif ()
if (FADEC_ENCODE)
fadec_component(NAME encode SOURCES encode.c HEADERS fadec-enc.h)
endif ()
if (FADEC_ENCODE2)
fadec_component(NAME encode2 SOURCES encode2.c HEADERS fadec-enc2.h)
endif ()
install(TARGETS fadec EXPORT fadec
LIBRARY
ARCHIVE
FILE_SET HEADERS FILE_SET generated_public)

28
third_party/fadec/LICENSE vendored Normal file
View File

@@ -0,0 +1,28 @@
Copyright (c) 2018, Alexis Engelke
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

184
third_party/fadec/README.md vendored Normal file
View File

@@ -0,0 +1,184 @@
# Fadec — Fast Decoder for x86-32 and x86-64 and Encoder for x86-64
Fadec is a fast and lightweight decoder for x86-32 and x86-64. To meet the goal of speed, lookup tables are used to map the opcode the (internal) description of the instruction encoding. This table currently has a size of roughly 37 kiB (for 32/64-bit combined).
Fadec-Enc (or Faenc) is a small, lightweight and easy-to-use encoder, currently for x86-64 only.
## Key features
> **Q: Why not just use any other decoding/encoding library available out there?**
>
> A: I needed to embed a small and fast decoder in a project for a freestanding environment (i.e., no libc). Further, only very few plain encoding libraries are available for x86-64; and most of them are large or make heavy use of external dependencies.
- **Small size:** the entire library with the x86-64/32 decoder and the x86-64 encoder are only 95 kiB; for specific use cases, the size can be reduced even further (e.g., by dropping AVX-512). The main decode/encode routines are only a few hundreds lines of code.
- **Performance:** Fadec is significantly faster than libopcodes, Capstone, or Zydis due to the absence of high-level abstractions and the small lookup table.
- **Zero dependencies:** the entire library has no dependencies, even on the standard library, making it suitable for freestanding environments without a full libc or `malloc`-style memory allocation.
- **Correctness:** even corner cases should be handled correctly (if not, that's a bug), e.g., the order of prefixes, immediate sizes of jump instructions, the presence of the `lock` prefix, or properly handling VEX.W in 32-bit mode.
All components of this library target the Intel 64 implementations of x86. While AMD64 is _mostly similar_, there are some minor differences (e.g. operand sizes for jump instructions, more instructions, `cr8` can be accessed with `lock` prefix, `f34190` is `xchg`, not `pause`) which are currently not handled.
## Decoder Usage
### Example
```c
uint8_t buffer[] = {0x49, 0x90};
FdInstr instr;
// Decode from buffer into instr in 64-bit mode.
int ret = fd_decode(buffer, sizeof(buffer), 64, 0, &instr);
// ret<0 indicates an error, ret>0 the number of decoded bytes
// Relevant properties of instructions can now be queried using the FD_* macros.
// Or, we can format the instruction to a string buffer:
char fmtbuf[64];
fd_format(&instr, fmtbuf, sizeof(fmtbuf));
// fmtbuf now reads: "xchg r8, rax"
```
### API
The API consists of two functions to decode and format instructions, as well as several accessor macros. A full documentation can be found in [fadec.h](fadec.h). Direct access of any structure fields is not recommended.
- `int fd_decode(const uint8_t* buf, size_t len, int mode, uintptr_t address, FdInstr* out_instr)`
- Decode a single instruction. For internal performance reasons, note that:
- The decoded operand sizes are not always exact. However, the exact size can be reconstructed in all cases.
- An implicit `fwait` in FPU instructions is decoded as a separate instruction (matching the opcode layout in machine code). For example, `finit` is decoded as `FD_FWAIT` + `FD_FINIT`
- Return value: number of bytes used, or a negative value in case of an error.
- `buf`/`len`: buffer containing instruction bytes. At most 15 bytes will be read. If the instruction is longer than `len`, an error value is returned.
- `mode`: architecture mode, either `32` or `64`.
- `address`: set to `0`. (Obsolete use: virtual address of the decoded instruction.)
- `out_instr`: Pointer to the instruction buffer, might get written partially in case of an error.
- `void fd_format(const FdInstr* instr, char* buf, size_t len)`
- Format a single instruction to a human-readable format.
- `instr`: decoded instruction.
- `buf`/`len`: buffer for formatted instruction string
- Various accessor macros: see [fadec.h](fadec.h).
## Encoder Usage
The encoder has two API variants: "v1" has a single entry point (`fe_enc64`) and the instruction is specified as integer parameter. "v2" has one entry point per instruction. v2 is currently about 3x faster than v1, but also has much larger code size (v1: <10 kiB; v2: ~3 MiB) and takes much longer to compile. It is therefore off by default and can be enabled by passing `-Dwith_encode2=true` to Meson. Both variants are supported.
### Example (API v1)
```c
int failed = 0;
uint8_t buf[64];
uint8_t* cur = buf;
// xor eax, eax
failed |= fe_enc64(&cur, FE_XOR32rr, FE_AX, FE_AX);
// movzx ecx, byte ptr [rdi + 1*rax + 0]
failed |= fe_enc64(&cur, FE_MOVZXr32m8, FE_CX, FE_MEM(FE_DI, 1, FE_AX, 0));
// test ecx, ecx
failed |= fe_enc64(&cur, FE_TEST32rr, FE_CX, FE_CX);
// jnz $
// This will be replaced later; FE_JMPL enforces use of longest offset
uint8_t* fwd_jmp = cur;
failed |= fe_enc64(&cur, FE_JNZ|FE_JMPL, (intptr_t) cur);
uint8_t* loop_tgt = cur;
// add rax, rcx
failed |= fe_enc64(&cur, FE_ADD64rr, FE_AX, FE_CX);
// sub ecx, 1
failed |= fe_enc64(&cur, FE_SUB32ri, FE_CX, 1);
// jnz loop_tgt
failed |= fe_enc64(&cur, FE_JNZ, (intptr_t) loop_tgt);
// (alternatively: fe_enc64(&cur, FE_Jcc|FE_CC_NZ, (intptr_t) loop_tgt).)
// Update previous jump to jump here. Note that we _must_ specify FE_JMPL too.
failed |= fe_enc64(&fwd_jmp, FE_JNZ|FE_JMPL, (intptr_t) cur);
// ret
failed |= fe_enc64(&cur, FE_RET);
// cur now points to the end of the buffer, failed indicates any failures.
```
### Example (API v2)
```c
uint8_t buf[64];
uint8_t* cur = buf;
// xor eax, eax
cur += fe64_XOR32rr(cur, 0, FE_AX, FE_AX);
// movzx ecx, byte ptr [rdi + 1*rax + 0]
cur += fe64_MOVZXr32m8(cur, 0, FE_CX, FE_MEM(FE_DI, 1, FE_AX, 0));
// test ecx, ecx
cur += fe64_TEST32rr(cur, 0, FE_CX, FE_CX);
// jnz $
// This will be replaced later; FE_JMPL enforces use of longest offset
uint8_t* fwd_jmp = cur;
cur += fe64_JNZ(cur, FE_JMPL, cur);
uint8_t* loop_tgt = cur;
// add rax, rcx
cur += fe64_ADD64rr(cur, 0, FE_AX, FE_CX);
// sub ecx, 1
cur += fe64_SUB32ri(cur, 0, FE_CX, 1);
// jnz loop_tgt
cur += fe64_JNZ(cur, 0, loop_tgt);
// (alternatively: fe64_Jcc(cur, FE_CC_NZ, loop_tgt).)
// Update previous jump to jump here. Note that we _must_ specify FE_JMPL too.
fe64_JNZ(fwd_jmp, FE_JMPL, cur);
// ret
cur += fe64_RET(cur, 0);
// cur now points to the end of the buffer
// errors are ignored, this example should not cause any :-)
```
### API v1
The API consists of one function to handle encode requests, as well as some macros. More information can be found in [fadec-enc.h](fadec-enc.h). Usage of internals like enum values is not recommended.
- `int fe_enc64(uint8_t** buf, uint64_t mnem, int64_t operands...)`
- Encodes an instruction for x86-64 into `*buf`. EVEX-encoded instructions will transparently encode with the shorter VEX prefix where permitted.
- Return value: `0` on success, a negative value in error cases.
- `buf`: Pointer to the pointer to the instruction buffer. The pointer (`*buf`) will be advanced by the number of bytes written. The instruction buffer must have at least 15 bytes left.
- `mnem`: Instruction mnemonic to encode combined with extra flags:
- `FE_SEG(segreg)`: override segment to specified segment register.
- `FE_ADDR32`: override address size to 32-bit.
- `FE_JMPL`: use longest possible offset encoding, useful when jump target is not known.
- `FE_MASK(maskreg)`: specify non-zero mask register (1--7) for instructions that support masking (suffixed with `_mask` or `_maskz`) or require a mask (AVX-512 gather/scatter).
- `FE_RC_RN/RD/RU/RZ`: set rounding mode for instructions with static rounding control (suffixed `_er`).
- `FE_CC_O/NO/E/NE/...`: set condition code for instructions with unspecified condition code (`Jcc`, `SETcc`, `CMOVcc`, `CMPccXADD`).
- `operands...`: Up to 4 instruction operands. The operand kinds must match the requirements of the mnemonic.
- For register operands (`r`=non-mask register, `k`=mask register), use the register: `FE_AX`, `FE_AH`, `FE_XMM12`.
- For immediate operands (`i`=regular, `a`=absolute address), use the constant: `12`, `-0xbeef`.
- For memory operands (`m`=regular or `b`=broadcast), use: `FE_MEM(basereg,scale,indexreg,offset)`. Use `0` to specify _no register_. For RIP-relative addressing, the size of the instruction is added automatically.
- For offset operands (`o`), specify the target address.
### API v2
The API consists of one function per instruction, as well as some macros. The API provides type safety for different register types as well as for memory operands (regular vs. VSIB). Besides a few details listed here, the usage is very similar to API v1. More information can be found in [fadec-enc2.h](fadec-enc2.h). Usage of internals like enum values is not recommended.
- `int fe64_<mnemonic>(uint8_t* buf, int flags, <operands...>)`
- Encodes the specified instruction for x86-64 into `buf`. EVEX-encoded instructions will transparently encode with the shorter VEX prefix where permitted.
- Return value: `0` on failure, otherwise the instruction length.
- `buf`: Pointer to the instruction buffer. The instruction buffer must have at least 15 bytes left. Bytes beyond the returned instruction length can be overwritten.
- `flags`: combination of extra flags, default to `0`:
- `FE_SEG(segreg)`: override segment to specified segment register.
- `FE_ADDR32`: override address size to 32-bit.
- `FE_JMPL`: use longest possible offset encoding, useful when jump target is not known.
- `FE_RC_RN/RD/RU/RZ`: set rounding mode for instructions with static rounding control (suffixed `_er`).
- `FE_CC_O/NO/E/NE/...`: set condition code for instructions with unspecified condition code (`Jcc`, `SETcc`, `CMOVcc`, `CMPccXADD`).
- `FeRegMASK opmask` (instructions with opmask only): specify non-zero mask register (1--7) for instructions suffixed with `_mask`/`_maskz` and AVX-512 gather/scatter.
- `operands...`: up to four instruction operands.
- Registers have types `FeRegGP`/`FeRegXMM`/`FeRegMASK`/etc.; byte registers accepting high-byte operands also accept `FeRegGPH`.
- Immediate operands have an appropriately sized integer type.
- Memory operands use a `FeMem` (VSIB: `FeMemV`) structure, use the macro `FE_MEM(basereg,scale,indexreg,offset)` (VSIB: `FE_MEMV(...)`). Use `FE_NOREG` to specify _no register_. For RIP-relative addressing, the size of the instruction is added automatically.
- For offset operands (`o`), specify the target address relative to `buf`.
- `int fe64_NOP(uint8_t* buf, unsigned size)`
- Encode a series of `nop`s of `size` bytes, but at least emit one byte. This will use larger the `nop` encodings to reduce the number of instructions and is intended for filling padding.
## Known issues
- Decoder/Encoder: register uniqueness constraints are not enforced. This affects:
- VSIB-encoded instructions: no vector register may be used more than once
- AMX instructions: no tile register may be used more than once
- AVX-512 complex FP16 multiplication: destination must be not be equal to a source register
- Prefixes for indirect jumps and calls are not properly decoded, e.g. `notrack`, `bnd`.
- Low test coverage. (Help needed.)
- No Python API.
Some ISA extensions are not supported, often because they are deprecated or unsupported by recent hardware. These are unlikely to be implemented in the near future:
- (Intel) MPX: Intel lists MPX as deprecated.
- (Intel) HLE prefixes `xacquire`/`xrelease`: Intel lists HLE as deprecated. The formatter for decoded instructions is able to reconstruct these in most cases, though.
- (Intel) Xeon Phi (KNC/KNL/KNM) extensions, including the MVEX prefix: the hardware is discontinued/no longer available.
- (AMD) XOP: unsupported by newer hardware.
- (AMD) FMA4: unsupported by newer hardware.
If you find any other issues, please report a bug. Or, even better, send a patch fixing the issue.

Some files were not shown because too many files have changed in this diff Show More