Compare commits

...

284 Commits

Author SHA1 Message Date
√(noham)²
b4727df3e9 Add macOS support for ProcessMemory plugin
Implement macOS-specific support for the ProcessMemory plugin and update plugin discovery/build.

- Add macOS build/install support: include plugins/ProcessMemory in top-level CMake and copy the built plugin into Reclass.app/Contents/PlugIns on macOS.
- Implement Apple-specific ProcessMemoryProvider: task_for_pid usage, mach_vm_read_overwrite/mach_vm_write, proc_pidpath/proc_regionfilename based module caching, region enumeration, symbol formatting, module enumeration, and proper cleanup (mach_port_deallocate).
- Extend plugin header to track m_task and adjust readability checks for macOS.
- Add macOS handling in getInitialBaseAddress and process enumeration to find base addresses and processes using proc APIs.
- Improve PluginManager to probe multiple plugin locations (including Contents/PlugIns inside macOS bundles), aggregate/log candidate counts, and continue scanning multiple dirs.
- Add macOS screenshot to README (docs/README_PIC6.png) and reference it in README.

These changes enable the ProcessMemory plugin to operate on macOS and make plugin discovery more robust on macOS app bundles.
2026-03-15 14:47:48 +01:00
IChooseYou
dc6963e0d5 feat: extract typeIndex from PDB symbols and add symbols.importType MCP tool
extractPdbSymbols() was reading S_GDATA32/S_GTHREAD32 records which
contain a typeIndex field linking the symbol to its type definition in
the TPI stream, but this field was discarded — only name + RVA were
kept. This meant loading symbols gave you address resolution but no
way to automatically import the type associated with a global variable.

Changes:
- PdbSymbol now carries typeIndex (0 = no type info / public symbol)
- extractPdbSymbols() captures typeIndex from all global data symbols
- PdbSymbolSet stores nameToTypeIndex mapping alongside nameToRva
- New importTypeForSymbol() follows LF_POINTER/LF_MODIFIER chains to
  find the underlying UDT/enum and imports it with full recursive children
- New symbols.importType MCP tool: given "ntdll!g_pShimEngineModule",
  resolves its typeIndex, imports the type definition from the PDB, and
  merges it into the active project
- loadPdbIntoStore() helper consolidates the extract+store pattern with
  type index support
2026-03-14 18:11:57 -06:00
IChooseYou
cb10bc8a82 docs: remove dedicated kernel driver section from README 2026-03-14 17:47:07 -06:00
IChooseYou
b5521bd638 docs: add kernel driver plugin to README
Document the KernelMemory plugin — capabilities, driver build
instructions, and architecture diagram.
2026-03-14 17:45:21 -06:00
IChooseYou
89d6e1944b fix: guard computeOffset against negative results before address arithmetic
computeOffset() returns int64_t but most callers added the result directly
to baseAddress (uint64_t) without checking for negative values. A malformed
tree with negative cumulative offsets would produce wrapped addresses,
potentially reading/writing arbitrary memory in the bitfield toggle and
edit paths. Added sign checks at all 9 unguarded call sites.
2026-03-14 17:31:13 -06:00
IChooseYou
7528d1bbbb Merge pull request #12 from 70RMUND/fix/linux-menubar-toolbuttons
fix: Linux menu bar horizontal layout via QToolButton fallback
2026-03-14 16:07:39 -06:00
Your Name
4f2288048e fix: Linux menu bar renders as horizontal tool buttons instead of collapsed extension popup
On Linux, QMenuBar inside a custom title bar widget (setMenuWidget) collapses
all items into the extension overflow popup. Replace with QToolButton widgets
on Linux that share the same QMenu objects. Includes hover-to-switch behavior
via event filter on open menus.

Windows and macOS paths are unchanged — guarded by #ifdef __linux__ and
runtime m_useToolButtons flag.
2026-03-14 15:51:31 -04:00
IChooseYou
97b6f55e1f fix: Linux menu popups and SVG icon rendering
- Guard FramelessWindowHint + WA_TranslucentBackground on QMenu to
  Windows only — breaks popup submenus on Linux/Wayland compositors
- Render SVG icons via QSvgRenderer to QPixmap explicitly, avoiding
  dependency on the qsvgicon image format plugin which may not be
  deployed on Linux
2026-03-14 12:41:11 -06:00
IChooseYou
6a30e0a402 fix: replace remaining QList::append({}) in plugins and tests
Missed plugin and test directories in the previous Qt 6.8 compat fix.
2026-03-14 12:11:08 -06:00
IChooseYou
1501a1542c feat: symbol double-click navigation, tree icons, and module.dll address parsing
- Double-click symbol in Symbols tab navigates to moduleBase + RVA
- Add symbol-method.svg icon for function symbols, symbol-structure.svg for modules
- Force-populate all modules on search so filter works without expanding first
- Parse module.dll/exe/sys as identifiers in address bar (e.g. client.dll + 0xFF)
2026-03-14 11:57:32 -06:00
IChooseYou
4f82b39785 fix: uint64_t to QVariant ambiguity on Qt 6.8 Linux 2026-03-14 11:52:45 -06:00
IChooseYou
009ddc951c fix: commit remaining uncommitted source changes for CI
Add extractPdbSymbols declaration to import_pdb.h,
enumerateModules to provider.h, and other pending changes
that were only local.
2026-03-14 09:36:49 -06:00
IChooseYou
5921af2b4f fix: replace QList::append({}) with push_back/emplaceBack for Qt 6.8
Qt 6.8's stricter QList rejects brace-enclosed initializer lists in
append(). Fixed 43 call sites across 13 files.
2026-03-14 09:21:14 -06:00
IChooseYou
5ded192990 fix: sync tab title on keyword convert, add new screenshots
- Update dock tab title when converting enum/class via workspace menu
- Add tooltip and source picker screenshots to README
2026-03-14 09:14:28 -06:00
IChooseYou
54bee5022b fix: add missing symbols dock declarations to mainwindow.h 2026-03-14 09:03:41 -06:00
IChooseYou
5d2d324946 fix: add missing symbol store and PDB debug info sources
These files were referenced in CMakeLists.txt and main.cpp but
never committed, breaking the CI build.
2026-03-14 08:13:58 -06:00
IChooseYou
5b2cf1ae1f feat: arrow tooltip improvements and base address cheat sheet
- Scale tooltip font to 90% of editor font
- Replace inline edit hint for base address with hover tooltip
- Two-column cheat sheet: syntax examples + explanations
- Dismiss all popups on alt-tab (ActivationChange)
2026-03-14 08:03:23 -06:00
IChooseYou
f1a36f2ad3 feat: custom arrow tooltip with transparent background
Rewrite RcxTooltip to use WA_TranslucentBackground with a single
contiguous QPainterPath (rounded rect + arrow notch). Pre-set the
DarkTitleBar property to prevent DarkApp from calling
DwmSetWindowAttribute which breaks layered window compositing.

Dismiss all popups (including arrow tooltip) on alt-tab via
MainWindow::changeEvent(ActivationChange).
2026-03-14 06:45:45 -06:00
IChooseYou
665138e688 fix: force all nodes collapsed on file load
Prevents 512-element arrays from expanding on load and triggering
thousands of memory reads. Root nodes still show children via
isRootHeader override.
2026-03-14 06:05:53 -06:00
IChooseYou
7688bb5b92 ci: add SDK include paths for WDK NuGet driver build
NuGet splits WDK and SDK into separate packages. specstrings.h
lives in the SDK shared headers. Add SDK_INC_ROOT for shared/ucrt.
2026-03-14 05:40:24 -06:00
IChooseYou
701e088be8 ci: install WDK via NuGet for driver build in CI
Runner doesn't have WDK headers installed. Use NuGet to install
Microsoft.Windows.WDK.x64 and pass paths via env vars.
build_driver.bat now accepts WDK_INC_ROOT/WDK_LIB_ROOT overrides.
2026-03-14 05:20:23 -06:00
IChooseYou
3c0c248d54 fix: use delayed expansion in build_driver.bat for CI
Parentheses in "Program Files (x86)" broke cmd parser inside
for loop bodies. Switch to !var! delayed expansion.
2026-03-14 04:56:46 -06:00
IChooseYou
7af969f6bd ci: build kernel driver and include rcxdrv.sys in release
Add build_driver.bat step to Windows CI using runner's MSVC + WDK.
Copy rcxdrv.sys into Plugins/ in the release zip.
2026-03-13 15:27:51 -06:00
IChooseYou
8ba1fd2492 fix: auto-detect MSVC and WDK paths in build_driver.bat
Remove hardcoded MSVC 14.39.33519 and WDK 10.0.22621.0 paths.
Now scans for the newest installed version automatically.
2026-03-13 15:05:59 -06:00
IChooseYou
b08736245b feat: kernel memory plugin + unified source menu + driver improvements
- KernelMemory plugin: kernel-mode process/physical memory R/W via IOCTL driver
- rcxdrv.sys: MmCopyMemory for reads, MDL mapping with correct cache types
  (MmCached for RAM, MmNonCached for MMIO only — fixes cache corruption BSOD)
- Driver reconnect: ensureDriverLoaded tries device handle first, no auto
  stop+delete cycle. Manual unload closes handle only, service stays running.
- Unified source menu: ProviderRegistry::populateSourceMenu() shared by both
  main window Data Source menu and RcxEditor inline picker (icons + dll names)
- IProviderPlugin::populatePluginMenu() for conditional plugin actions
  (e.g. "Unload Kernel Driver" only when loaded)
- Physical memory mode removed from selectTarget (access via context menu only)
- requestOpenProviderTab sets base address from provider after template load
- Address parser: vtop(), cr3(), physRead() callbacks for kernel paging expressions
2026-03-13 14:46:22 -06:00
IChooseYou
7f7bbdcc45 fix: remove isRelative references from generator.cpp
Node::isRelative is not yet in the pushed core.h, breaking CI builds.
2026-03-12 18:07:34 -06:00
IChooseYou
79b5125229 fix: remove unreleased isRelative/ToggleRelative references from controller
These were local-only changes that referenced cmd::ToggleRelative and
Node::isRelative which don't exist in the pushed core.h, breaking CI.
2026-03-12 16:30:59 -06:00
IChooseYou
3aeb1a80d5 feat: inline hex byte and ASCII preview editors for hex nodes
Right-click context menu adds "Edit Hex Bytes" and "Edit ASCII" for
hex nodes (Hex8/16/32/64). Both are fixed-length overwrite-mode editors
with space-skipping, input validation, and IND_HEX_DIM indicator
preservation.
2026-03-11 16:01:37 -06:00
IChooseYou
3b7ed682ac Merge commit 'refs/pull/11/head' of github.com:IChooseYou/Reclass
# Conflicts:
#	src/mcp/mcp_bridge.cpp
2026-03-10 16:02:12 -06:00
IChooseYou
0582cb286b fix: commit missing selectPage() for OptionsDialog 2026-03-10 15:43:27 -06:00
IChooseYou
ea85b7a621 feat: add C# and Python ctypes code generators
- C# backend: [StructLayout(LayoutKind.Explicit)] with [FieldOffset], IntPtr pointers, fixed arrays, enums
- Python backend: ctypes.Structure with _fields_, POINTER() for typed pointers, c_void_p, padding
- Both support enums, vectors, bitfields, arrays, unions, static fields
- Export menu: C# Structs... and Python ctypes... entries
- Format combo auto-populates new options
- 14 new tests for both backends (all passing)
2026-03-10 15:20:56 -06:00
IChooseYou
6c8b7d3d97 feat: Rust/#define generators, code tab format/scope combos, enum #define support
- Add Rust #[repr(C)] and #define offset code generators with dispatch
- Add format combo + scope combo + gear button as corner widget on Code tab
- Corner controls hidden on Reclass tab, shown only on Code tab
- Chevron-down SVG arrows on combo dropdowns for consistent styling
- Fix enum #define output: emit named members instead of empty 0x0 struct
2026-03-10 15:05:23 -06:00
IChooseYou
d1321b5165 fix: per-group sentinel docks, editor inline-edit comment alignment
Sentinel dock refactored to per-tab-group model — each split group gets
its own hidden sentinel so tab bars stay visible without the Hide event
filter hack.  Editor inline-edit comment column now anchors correctly
for base-address edits and shows expression hint instead of generic text.
2026-03-09 11:44:55 -06:00
IChooseYou
483f87cfbd feat: type hints green [bracketed] notation, workspace cleanup, unique naming
- Type inference hints now show value-first with bracketed type in comment
  green: "0x7ff718570000 [ptr64]", "6, 16 [int32_t×2]"
- Raise hint threshold to strong-only (score >= 75%)
- Remove Bool inference, widen Int16 range to ±16384
- Workspace: remove dead WorkspaceProxy, fix null deref, debounce search,
  cache icons, add pinning support
- Unique naming: UnnamedClass0/UnnamedEnum1 with global counter
- Footer buttons: +10h +100h +1000h replacing +1024
- MCP: project lifecycle API, snapshot provider fix
2026-03-09 10:39:22 -06:00
noita-player
4d0782db68 MCP bridge: support multiple concurrent clients
Replace single-client model (m_client/m_readBuffer/m_initialized)
with a ClientState vector. Each client gets its own read buffer and
initialized flag. Responses route to m_currentSender (set during
request processing); notifications broadcast to all initialized
clients.

Re-entrancy guard in onReadyRead: re-resolve ClientState after each
processLine() call since sendJson flush can re-enter the event loop
and trigger onDisconnected, removing the client mid-iteration.

Tests: 378-line test_mcp exercising connect, initialize, tools/list,
disconnect one client, notification broadcast, and serial requests
against a MockMcpServer with the same multi-client architecture.
2026-03-08 22:44:47 -07:00
noita-player
51de48a6ed Add MCP scanner tools, source.modules, reconnect, and constraint regions
Scanner engine:
- Add constrainRegions to ScanRequest — callers pass address ranges
  that are intersected with provider regions before scanning
- Merge overlapping/adjacent constraints to prevent duplicate results
- Fix final-chunk overlap: skip overlap advance on last chunk to avoid
  re-scanning the tail of a region

MCP tools:
- scanner.scan: value scans (int/float types) with optional region
  constraints, returns first 15 addresses
- scanner.scan_pattern: pattern/signature scans with wildcards
- source.modules: list loaded modules with base address and size
- mcp.reconnect: graceful client disconnect for IDE reconnection
- parseInteger() helper for hex string args (avoids JSON double
  precision loss on 64-bit addresses)
- Fix baseRelative semantics in hex.read/hex.write (was inverted)
- Auto-set tree.baseAddress from provider after process attach

Scanner panel:
- runValueScanAndWait() and runPatternScanAndWait() for blocking
  scan execution from MCP/automation code

Tests: 41 new test cases for constrainRegions covering gaps, partial
overlap, adjacent regions, writable filter, degenerate ranges,
overlapping constraints, boundary patterns, alignment, and value
types at region start/end positions.
2026-03-08 22:44:46 -07:00
noita-player
7b9b140823 Fix MCP use-after-free, scanner chunk overlap, build scripts
- MCP bridge: guard against use-after-free when client disconnects
  during sendJson flush by re-checking m_client after write
- Scanner engine: fix chunk overlap advancing past region end on
  final chunk; fix fallback region flags for providers without
  enumerateRegions
- Build scripts: prefer GCC MinGW over LLVM-MinGW in PATH detection
2026-03-08 22:44:36 -07:00
IChooseYou
a21e5a07a8 feat: replace +1024 footer button with +10h +100h +1000h granular grow
- Three hex-sized grow buttons: +10h (16B), +100h (256B), +1000h (4096B)
- Single-space gaps between buttons for tighter layout
- All click, hover, cursor, and pill styling updated
- Enum +10 button unchanged and correctly disambiguated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 16:38:03 -06:00
IChooseYou
25afbe373b feat: status bar format, tab titles with source, taller tabs, pill hover, source switch base fix
- Status bar: show StructName.field +0xOFFSET with dimmed offset suffix
- Status bar: sync font to global editor font (JetBrains Mono 10pt)
- Dock tab title: include active source name (StructName — source.exe)
- Dock tabs +10% height (28→31), pane tabs (24→26), workspace title (26→29)
- Footer pills (+1024, Trim, +10): add visual hover highlight via IND_HOVER_SPAN
- Fix source switch keeping old base address for plugin providers
2026-03-08 16:29:12 -06:00
IChooseYou
6a4cb47ed4 fix: kill Fusion outline on QScintilla, type inference hints, workspace styling
- Suppress PE_Frame on QsciScintilla in MenuBarStyle to eliminate the
  1px dark (#171717) Fusion outline around the editor area
- Add --screenshot flag for automated pixel regression testing
- Add type inference engine (typeinfer.h) with hex pattern analysis
- Show inferred type hints on hex nodes in compose output
- Style workspace tree corner/header widgets to match theme
- Fix integer overflow in compose.cpp array element addressing
- Fix integer overflow in core.h structSpan calculation
- Add bounds check on activePaneIdx in controller
- Use QPointer for deferred dock lambda safety
- Workspace delegate uses icon Normal/Disabled for viewed state
2026-03-08 10:26:12 -06:00
IChooseYou
431e2b90c9 perf: TypeSelector — zero-alloc fuzzy scorer, warm popup 75% faster
Stack arrays + pre-lowered QChars in fuzzyScore eliminate all heap
allocations in the hot path. applyFilter uses indices instead of
deep-copying TypeEntry. popup() width estimated from cached max name
length. QListView: uniform sizes, batched layout, cached sizeHint.

Benchmark (5000 structs): warm popup 27ms→7ms, filter 5ms→1.7ms.
2026-03-08 08:33:21 -06:00
IChooseYou
43365c1aff fix: close project actually destroys dock, editor perf single-pass line attributes
- Set WA_DeleteOnClose on doc docks so all close paths trigger cleanup
- Create fresh empty class when last project closes
- Add splitDockWidget/resizeDocks to project_new() so workspace doesn't eat editor space
- Merge applyMarginText, applyMarkers, applyFoldLevels into single-pass applyLineAttributes
- Cache line texts for heatmap/symbol coloring passes (avoid redundant Scintilla IPC)
- Zero-alloc scroll width scan replaces QString::split
2026-03-08 08:13:36 -06:00
IChooseYou
596f410b96 perf: compose 30% faster — move semantics, BFS offsets, zero-alloc hex formatting
- compose.cpp: emitLine takes LineMeta&& (move, not copy) at all 22 call sites
- compose.cpp: reserve meta/text buffers, BFS offset computation O(N) vs O(N*D)
- compose.cpp: pre-compute typeNameLens[], merge global width loops
- format.cpp: bytesToHex uses stack buffer + lookup table (zero heap allocs)
- format.cpp: hexVal single QString::asprintf instead of 2-string concat
- editor.cpp: guard hover updates during applyDocument (stale index safety)
- core.h: assertion on makeArrayElemSelId negative index
- format.cpp: assertion on extractBits overflow
- main.cpp: tree lines enabled by default
- bench_large_class: add 2000-field benchComposeLarge test

Benchmark: 500 fields 0.70→0.51ms (27%), 2000 fields 2.28→1.57ms (31%)
2026-03-08 07:28:26 -06:00
IChooseYou
f0fc85f60f fix: CI test failures from collapsed=true default
- compose.cpp: show static fields for root structs even when collapsed
- test_compose: set collapsed=false on nodes needing expanded rendering
- test_disasm: set collapsed=false on vtable pointer nodes
- test_static_fields: rewrite collapsed test to use non-root child struct
2026-03-07 11:58:08 -07:00
IChooseYou
70c7404556 fix: MSVC build support, modern theme, vergilius fnptr import
- CMake: detect MSVC↔MinGW Qt ABI mismatch at configure time (#10)
- CMake: add /utf-8 /MP for MSVC builds
- CMake: fix theme/example deployment for multi-config generators (MSVC)
- Auto-run windeployqt post-build so correct Qt DLLs are always deployed
- Add Modern theme (dark blue with cyan/purple/amber accents)
- Vergilius import: handle function pointer typedefs
2026-03-07 11:31:04 -07:00
IChooseYou
f27459c21b fix: default collapsed=true for child structs, dock border wraps panel, search bar borderless, title bar +2px 2026-03-07 11:17:35 -07:00
IChooseYou
a5abcbeea6 Merge pull request #9 from noita-player/feature/peb-teb-mcp
Add process.info MCP tool for PEB/TEB enumeration and peb/tebs API for providers to implement
2026-03-07 09:42:51 -07:00
IChooseYou
7071402319 fix: workspace panel — preserve expansion on clear, dock title counts, drop kind text, close.svg clear button 2026-03-07 08:37:15 -07:00
IChooseYou
0dc390ed86 fix: WinDbg plugin dynamic dbgeng loading, editor two-tone bg, UI polish
WinDbg plugin: load dbgeng.dll dynamically from Debugging Tools directory
instead of static linking (system dbgeng.dll lacks remote DebugConnect).
Copy tools dbghelp.dll next to exe so it loads before System32 version.
Add COM init on DbgEng thread, browse for tools dir, styled dialog.

Editor: derive darker background via theme.background.darker(115) for
visual depth between chrome and editor surfaces.

UI: global scrollbar styling, workspace accent bar 1px, pane tab font
from editor settings, workspace dock default width 128px.
2026-03-07 08:31:51 -07:00
IChooseYou
188c27c6e2 feat: workspace panel visual overhaul, perf optimizations, remove kernel base addresses
Workspace panel:
- Custom WorkspaceDelegate: struct names bright, metadata dimmed, child types in teal
- Search box: monospace font, search icon, bordered with focus highlight
- Selection: accent bar, all fonts synced to 10pt monospace
- Remove rebuildWorkspaceModel from visibilityChanged (fixes double-click refresh)
- Incremental sync (syncProjectExplorer) preserves tree expansion state

Performance:
- childrenOf() O(1) via cached parent→children hash map
- Debounced workspace rebuilds (50ms coalesce)
- Pre-reserve node vector in NodeTree::fromJson
- Benchmark suite (bench_project)

Data:
- Remove kernel baseAddress from Vergilius/WinSDK examples (default to 0x400000)
2026-03-07 06:47:16 -07:00
noita-player
81f1e4319f Add process.info MCP tool for PEB/TEB enumeration
Expose PEB address via provider interface and query it in the
ProcessMemory plugin using NtQueryInformationProcess. The new
process.info MCP tool returns the PEB VA and enumerates TEBs by
querying thread information via NtQuerySystemInformation and
NtQueryInformationThread for each thread in the target process.
2026-03-06 23:21:10 -08:00
IChooseYou
3ab6affa5e fix: vergilius fnptr import, remove tab pin, flatten workspace tree, middle-click close
- Fix vergilius_to_rcx.py to detect function pointer syntax (*Name)(params) and emit FuncPtr64
- Re-fetch 85 structs to recover proper field names (697/716 fixed)
- Remove pin button from dock tabs and all pin-related context menu items
- Fix newClass() creating duplicate tabs
- Set workspace tree font to match tab bar (size 10)
- Flatten workspace tree: remove redundant Project group node (VS Code Explorer style)
- Add middle-click to close dock widget tabs
- Allow type chooser to show cross-doc types for root nodes
2026-03-06 17:39:50 -07:00
IChooseYou
35b3cd9ac1 feat: enum editing UI, protect enums from struct ops, New Class opens two tabs
- New Class creates two Unnamed tabs, selects the first
- New Enum creates 5 placeholder members (Member0-4)
- Right-click enum member: Add Member Above/Below, Remove Member
- Right-click enum header: Add Member, Rename, Delete only
- Enum nodes fully protected from struct operations (no Add Child, Insert, Convert)
2026-03-06 11:00:06 -07:00
IChooseYou
e5938f7e82 fix: enable hover on dock tab bars via WA_Hover attribute 2026-03-06 09:45:23 -07:00
IChooseYou
03c49d19dd fix: type chooser always shows modifiers, tabs show class name, dock buttons restored on re-dock 2026-03-06 09:23:36 -07:00
IChooseYou
b7eebedf50 fix: remove grab_tabs test target (missing source file) 2026-03-06 08:23:09 -07:00
IChooseYou
9ff456a8d6 revert: remove theme xcopy to avoid clobbering custom themes 2026-03-06 08:22:40 -07:00
IChooseYou
580f285edd fix: also copy theme JSON files to output dir for MSVC builds 2026-03-06 08:22:02 -07:00
IChooseYou
d23a6c7656 fix: copy example .rcx files to output dir for MSVC builds 2026-03-06 08:20:33 -07:00
IChooseYou
25d8de95b7 fix: crash in dismissStartPage due to re-entrant close/rejected signal 2026-03-06 08:16:13 -07:00
Sen66
955db3813a fix: msvc build due to startpage.h 2026-03-06 16:10:54 +01:00
IChooseYou
f4f203e0f0 Merge remote-tracking branch 'origin/fix-msvc-build' 2026-03-06 08:07:57 -07:00
IChooseYou
1d3f1a672a fix: start page card order, icon consistency, and Continue placement 2026-03-06 08:07:27 -07:00
Sen66
da29206bdb fix: msvc build with latest dock header file 2026-03-06 16:03:54 +01:00
IChooseYou
4986893fca feat: VS2022-style start page popup with recent files and get started cards 2026-03-06 07:58:13 -07:00
IChooseYou
17a1fb032e chore: remove Demo.rcx, add WinSDK + windows-x86_64.h examples 2026-03-06 07:56:33 -07:00
IChooseYou
8d92957837 fix: move DockTabButtons to header for MSVC automoc compatibility
automoc doesn't generate main.moc on MSVC, breaking the build.
Move DockTabButtons (which needs Q_OBJECT) to its own header so
automoc handles it as moc_dock_tab_buttons.cpp instead.
2026-03-06 06:14:59 -07:00
IChooseYou
f981fe456d feat: see-through popup dismiss for disasm/value-history/struct-preview
Override mouseMoveEvent in all three popup classes to forward mouse
position back to viewport hover logic. When the row underneath the
popup represents a different node, the popup dismisses automatically,
allowing rapid swiping through FuncPtr rows.
2026-03-05 18:25:40 -07:00
IChooseYou
877ceea4c1 feat: VS-style dock tabs with middle-elision and full context menu
- Remove stylesheet from dock tab bars; handle all painting in
  MenuBarStyle (CE_TabBarTabShape + CE_TabBarTabLabel) so middle-
  elision actually works (QStyleSheetStyle was intercepting labels)
- Accent line on selected tab, dark background, bottom border
- Tab font synced with editor font for correct sizing
- Full right-click context menu: Close, Close All Tabs, Close All
  But This, Close All But Pinned, Copy Full Path, Open Containing
  Folder, Float/Dock, Pin/Unpin Tab, New Horizontal/Vertical
  Document Group
- Add View → Reset Windows to re-tabify all docks
- Remove old View → Split/Remove Split
- Guard deferred timer lambdas with QPointer<QDockWidget>
- Extract setupDockTabBars() for idempotent tab bar configuration
- Register close-all.svg and split-vertical.svg icons
2026-03-05 15:16:01 -07:00
IChooseYou
4160a229c6 feat: workspace double-click opens struct in new tab + flat tab corners
- Double-clicking a root struct in the workspace tree opens it in a new
  tab (dock) sharing the same document, focused on that struct
- If a tab already views that struct, raises it instead of duplicating
- Child member double-click still navigates within the existing tab
- Doc lifecycle ref-counted: only deleted when last tab referencing it closes
- rebuildAllDocs/rebuildWorkspaceModel deduplicate shared docs
- Removed border-radius from all tab bar stylesheets (flat corners)
2026-03-05 13:49:42 -07:00
Sen66
1e1afc1640 fix: docking of 'project' window 2026-03-05 19:47:18 +01:00
IChooseYou
f0cf6c549a revert: restore .NET CLR hosting description for ReClass.NET plugin 2026-03-05 06:37:56 -07:00
Sen66
683eab16ee fix: better fix to switch to newly created class 2026-03-05 14:25:49 +01:00
Sen66
b53dea8f9f fix crash on application close 2026-03-05 14:25:06 +01:00
Sen66
f06abbab79 fix: on new class, switch to it 2026-03-05 14:23:07 +01:00
Sen66
2477591ed2 fix: assertion due to undo history disabled nullptr 2026-03-05 14:21:07 +01:00
IChooseYou
6c13356d6d docs: trim README plugin descriptions 2026-03-05 06:07:37 -07:00
IChooseYou
3b273a7ab2 fix: don't skip Array in scope width calc — only skip Struct
Array headers like int32_t[10] render in the type column and need
their width accounted for. Only Struct (pointer headers) should be
excluded from inflating sibling column widths.
2026-03-05 06:02:43 -07:00
IChooseYou
3509a0d9dd Merge remote-tracking branch 'origin/floating' 2026-03-05 05:58:18 -07:00
Sen66
43c3f5a842 fix: highlight issue between command row & opening brace 2026-03-05 13:52:40 +01:00
Sen66
0697ce4853 feat: option to have class opening brace on new line 2026-03-05 13:48:26 +01:00
IChooseYou
ed1bfd04cd fix: tighten editor column spacing — skip struct/array in scope width calc
Reduce kMinTypeW from 8 to 7, and exclude Struct/Array children from
per-scope column width measurement so pointer headers don't inflate
sibling hex row padding.
2026-03-05 13:48:26 +01:00
IChooseYou
c275eb33c9 fix: tighten editor column spacing — skip struct/array in scope width calc
Reduce kMinTypeW from 8 to 7, and exclude Struct/Array children from
per-scope column width measurement so pointer headers don't inflate
sibling hex row padding.
2026-03-05 05:46:14 -07:00
Sen66
636176ee8c feat: floating windows like old windbg 2026-03-05 13:23:00 +01:00
IChooseYou
9a716444f4 fix: menu border clipping, context menu cleanup, workspace sort
- Use WA_TranslucentBackground on QMenu popups so DWM doesn't clip
  border edges; draw 1px border at true widget edge via drawLine
- Move Insert 4/8 into Insert submenu, reorder context menu sections
- Sort workspace tree by visible (non-hex-pad) children count
2026-03-05 04:59:25 -07:00
Sen66
a46da4ee16 fix: horizontal scrollbar calculations for C/C++ view
- added msvc define NOMINMAX so we can use std::max
2026-03-05 12:46:55 +01:00
Sen66
cd52451210 fix: Release build configuration on MSVC & add windeployqt post-build 2026-03-05 12:16:11 +01:00
IChooseYou
82bf9118c9 feat: options dialog cleanup, menu/tree styling, light theme contrast
- Remove dead "Safe Mode" option, rename title case to "Uppercase menu items"
- Options tree: icons, themed hover/selection, mouse tracking (matches workspace tree)
- Tree item row padding (+4px) via MenuBarStyle CT_ItemViewItem for all trees
- Titlebar grows 2px when icon shown
- Menu popups: custom separator drawing, opaque background fill, flat hover highlight
- Menu bar/popup hover uses accent color (QPalette::Highlight) instead of grey
- Light theme: bump textMuted/textFaint contrast
- Dock grip widget for workspace and scanner docks
2026-03-04 13:44:42 -07:00
IChooseYou
f4c7e9327d fix: audit cleanup — themed close button, stale popup dismiss, bitfield clamp, scanner guard, process sort 2026-03-04 11:15:04 -07:00
IChooseYou
5944dbdc81 fix: cast char16_t to uint for QString::arg on macOS 2026-03-04 10:37:18 -07:00
IChooseYou
b3425aec9e clean up README: move screenshots above features, trim sections 2026-03-04 10:34:39 -07:00
IChooseYou
2a8cfee719 docs: update README screenshots (Windows, macOS, scanner) 2026-03-04 10:22:58 -07:00
IChooseYou
e999c664b8 feat: tree lines, scanner improvements, themes, tooltips, README overhaul
- Tree line connectors (Unicode box-drawing ├─ └─ │) at arbitrary depth
- Fix editor overwriting tree chars at depth 2+ (applyMarginText Pass 2)
- Scanner: unknown value scan, comparison rescan modes (Changed/Unchanged/Increased/Decreased)
- New Tailwind theme (tw.json), WCAG contrast fixes for warm/mid themes
- Tooltip system (rcxtooltip.h)
- Comprehensive README rewrite with full feature inventory
- New tests for compose tree lines, scanner, tooltips

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 09:21:09 -07:00
Lab
0dc4af6b1d Merge pull request #7 from IChooseYou/bundle-mcp-bridge
Bundle ReclassMcpBridge into macOS .app
2026-03-03 15:45:07 -08:00
Lab
376aad2169 Bundle ReclassMcpBridge into macOS .app
Copy the MCP stdio bridge executable into Reclass.app/Contents/MacOS/
via a POST_BUILD step so Claude Desktop can find it when the app is
distributed as a bundle.
2026-03-03 15:43:37 -08:00
Matty
4937c58062 fix: grey out value input instead of hiding, raise unknown scan cap to 10M 2026-03-03 12:16:14 -07:00
Matty
9c72265901 feat: scanner unknown value + comparison rescan modes, find bar height fix
Add Cheat Engine-style scan conditions: Unknown Value captures all
aligned addresses as baseline, then Changed/Unchanged/Increased/Decreased
narrow results by comparing current vs previous values. Exact Value
mode unchanged. Also fix find bar search box height to match buttons
and improve MCP bridge instructions.
2026-03-03 11:32:13 -07:00
IChooseYou
86499e58ee fix: remove value history cooldown hack, dismiss popup on clear
The cooldown suppressed tracking for ~1s but the popup persisted showing
stale "1h ago" values because applyDocument skips popup dismissal.
Replaced with explicit dismissHistoryPopup() after clear+refresh so the
popup is gone immediately. Value tracking resumes on the next async cycle
with a clean baseline (m_refreshGen++ discards in-flight reads,
m_prevPages.clear() prevents phantom diffs).
2026-03-03 08:38:08 -07:00
IChooseYou
b2ae8d5a5d fix: insert above node, clear value history cooldown, search context menu
- Insert 4/8 now inserts above the right-clicked node and shifts siblings
  down instead of appending at end. Insert key shortcut (Shift+Ins = 4,
  Ins = 8). Falls back to append when clicking empty space.
- Clear Value History uses a 5-cycle cooldown counter so heat stays gone
  for ~1s instead of returning on the next async refresh.
- Right-click Search defers showFindBar via QTimer::singleShot so focus
  isn't stolen by the closing context menu.
2026-03-03 08:31:49 -07:00
IChooseYou
6768f04e9a Merge pull request #6 from LabGuy94/add-macos-support
Fix file opening on macOS
2026-03-03 08:31:25 -07:00
Lab
c6e5f6508f Fix file opening on macOS 2026-03-02 15:25:57 -08:00
IChooseYou
e6529052b3 fix: clear value history clears subtree, add Copy Line and Search to context menu
- Clear Value History now removes history for all descendant nodes too
- Add "Copy Line" right-click menu item
- Add "Search..." right-click menu item (opens Ctrl+F find bar)
- Move showFindBar() to public in editor.h
2026-03-02 15:34:37 -07:00
IChooseYou
d43e989992 Merge pull request #5 from LabGuy94/add-macos-support
Add macOS support and CI
2026-03-02 14:57:55 -07:00
IChooseYou
879e9f4047 fix: global blue highlight, Ctrl+F find bar with prev/next/close buttons
- Change QPalette::Highlight from theme.selection to theme.hover globally
- RcxEditor find: use SCI_SEARCHINTARGET + INDIC_COMPOSITIONTHICK indicator
  (selection rendering is disabled, so findFirst was invisible)
- Re-apply find indicators after applyDocument() refresh cycle
- Add prev/next/close buttons to find bars in both Reclass and C/C++ modes
- Buttons styled with hover/pressed states matching tab styling
2026-03-02 14:53:14 -07:00
Lab
e0d5a799b4 Add macOS support and CI 2026-03-02 11:34:22 -08:00
IChooseYou
efae193520 feat: value history timestamps, Ctrl+F search, base address fixes
- Add timestamps to ValueHistory ring buffer, expose via new MCP tool
  node.history, show relative time in popup ("26s ago", "2m ago")
- Add "Clear Value History" right-click menu for single and multi-select
- Add Ctrl+F find bar to RcxEditor with live search, Enter-to-next, wrap
- Fix Ctrl+F in workspace dock to auto-focus search field
- Add "Change to float" quick-convert for Hex32 right-click menu
- Sort workspace explorer by children count descending (most fields first)
- Fix provider->base() overwriting saved base address from .rcx files
- Add formula support to MCP change_base operation
- Re-evaluate baseAddressFormula on provider attach in selectSource()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:00:17 -07:00
IChooseYou
ba1c2f8e5a refactor: process picker themed styling, context menu, auto-select
Extract shared init into initUi(). Apply dark theme styling from global
palette to table, header, filter, and buttons. Add right-click context
menu with Copy PID/Name/Path. Auto-select last attached process on open.
Remove duplicate attach->accept() connection from .ui (handled in code).
2026-03-02 08:24:39 -07:00
IChooseYou
5a0a4d1802 feat: recent files menu, remove split visibility, clean up demo data
Add Recent Files submenu under File menu (persists last 10 opened/saved
files in QSettings). Hide Remove Split action until a split actually
exists. Remove _SAMPLE_OBJECT demo class from both buildEmptyStruct and
buildEditorDemo. Create a second empty class tab on selfTest so the user
starts with a clean workspace.
2026-03-02 07:50:46 -07:00
Sen66
030eb34510 fix: include shim also on linux 2026-03-02 00:11:37 +01:00
Sen66
2939b25895 fix: build instructions for fadec on cmake build 2026-03-02 00:08:11 +01:00
Sen66
d38cb02fa2 fix: mingw build 2026-03-01 23:58:06 +01:00
IChooseYOu
9f285b37b2 Merge remote-tracking branch 'origin/msvc' 2026-03-01 14:03:34 -07:00
IChooseYOu
cae599a0c6 fix: fixed-width float formatting, fix test_32bit_support on Linux CI
Float values now use a fixed 7-char body (digits.decimals + f suffix)
that adapts decimal places to the integer magnitude. Removes the
variable-width 'g' format and sign-space prefix.

Set QT_QPA_PLATFORM=offscreen for test_32bit_support so it no longer
crashes on headless Linux CI without an X display.
2026-03-01 14:02:40 -07:00
Sen66
d0734ba8be update readme with MSVC/VS guide 2026-03-01 22:02:07 +01:00
Sen66
696ff044ac msvc VS22+ support 2026-03-01 21:54:17 +01:00
Sen66
da312ccac6 fix app close crash, fix error on msvc 2026-03-01 21:49:49 +01:00
Sen66
552b45b16c Fix QSci assert on msvc with text being nullptr 2026-03-01 21:40:57 +01:00
Sen66
e89fd4a6c1 make fadec, raw_pdb submodule 2026-03-01 18:55:26 +01:00
Sen66
7524004b32 remove /fadec and /raw_pdb content 2026-03-01 18:46:01 +01:00
IChooseYou
ed8a44917b feat: 32-bit process support, scanner rescan filtering, suppress flash on navigate
- Add pointerSize() to Provider base; WoW64/ELF detection in ProcessMemory,
  WinDbg, and RemoteProcessMemory plugins
- Wire pointer size through NodeTree, source/XML imports, C++ generator,
  controller, compose, address parser, and RPC protocol header
- Add is32Bit to PluginProcessInfo and ProcessInfo; show (32-bit) in picker
- Scanner rescan now filters results against the current input value
- Go-to-address from scanner resets change tracking to prevent false flashing
2026-03-01 07:42:40 -07:00
IChooseYou
ecfac3decf fix: add missing test source files to repository 2026-02-28 12:54:38 -07:00
IChooseYou
851d744263 fix: rescan performance overhaul, background thread, WinDbg regions
Move rescan to background thread via ScanEngine::startRescan() to
prevent UI freeze. Fix populateTable bottleneck caused by
QHeaderView::ResizeToContents iterating all rows (6s -> 0ms for 512
results). Add chunked batch reads (256KB spans), enumerateRegions()
for WinDbg/ProcessMemory providers, cancel support, and diagnostic
logging throughout the scanner pipeline.
2026-02-28 12:53:25 -07:00
IChooseYou
41e2f9f662 feat: scanner panel with signature/value search, rescan, address delegate
- Signature mode (IDA-style patterns with wildcards) and value mode (typed exact match)
- Async scan engine with progress, cancel support
- Re-scan updates all results with unified progress (single-pass read + table build)
- Previous value column appears after first re-scan
- WinDbg backtick address format with dimmed leading zeros (AddressDelegate)
- Inline editing: address expressions navigate, value edits write to provider
- Right-click context menu: Copy Address, Copy Value, Go to Address
- Auto-sized columns, themed buttons with icons, dynamic combo width
- 49 UI tests covering scan, rescan, editing, theming, progress completion
2026-02-28 11:53:51 -07:00
IChooseYou
95faf027a9 refactor: rename helpers to static fields, block-style rendering, sibling insert
Rename isHelper/ToggleHelper to isStatic/ToggleStatic across core, compose,
controller, editor, and generator. Static fields now render with block syntax
(static Type name { return expr } → 0xADDR) and support collapsed/expanded
display. Add "Add Static Field" context menu for sibling nodes. Update
expression span parser, completions, C++ generator comments, and all tests.
2026-02-28 08:21:00 -07:00
IChooseYou
6a51c904de feat: type selector overhaul, fuzzy search, address parser, value tracking
Redesign type selector popup with fuzzy subsequence matching, per-category
icons, field summary tooltips, compact chips, and pointer target primitives.
Add address expression parser with arithmetic and register support.
Enable track value changes by default.
2026-02-28 06:59:22 -07:00
IChooseYou
0d73575ea7 fix: C++ generator bitfields, sizeof placement, Ctrl+F search, view sync
- Generator emits proper bitfield members instead of padding stubs
- Named bitfield structs (MitigationFlagsValues etc) now converted by parser
- sizeof comment moved from top to closing brace (}; // sizeof 0x80)
- C/C++ view syncs with workspace double-click and controller navigation
- Ctrl+F incremental search in C++ code view (Enter=next, Escape=close)
- Workspace dock resizable via 1px drag handle separator
- Regenerated Vergilius_25H2.rcx with all fixes (61 named bitfield containers)
2026-02-26 12:07:55 -07:00
IChooseYou
aa04cfcb5c feat: add Vergilius-to-RCX converter, full Windows 11 25H2 kernel structs
Add tools/vergilius_to_rcx.py: scrapes struct definitions from
vergiliusproject.com and generates .rcx JSON files. Supports bitfields,
arrays, self-referential pointers, deep union/struct nesting, and
cross-struct references. Offsets correctly stored as parent-relative.

Add src/examples/Vergilius_25H2.rcx: 1,690 kernel structs (18,924 nodes)
from Windows 11 25H2 including _EPROCESS, _KTHREAD, _MMPFN, _PEB, etc.

Remove orange M_CYCLE background on self-referential pointer children —
rows now render with normal theme background while retaining click-to-
materialize behavior.
2026-02-26 11:02:12 -07:00
IChooseYou
1465e7fbed feat: Vergilius-style C++ generator, struct type click fix, item view highlight fix
Rewrite C++ generator for Vergilius-style output: inline anonymous
structs/unions, reference opaque types by name with struct keyword
prefix, size comments, aligned offset comments, no anon_ stubs.

Fix struct type name not clickable in editor headers (headerTypeNameSpan
assumed "struct TYPENAME" format but named structs use bare name).

Add static_assert toggle in Options > Generator, default off.

Fix item view highlight bleed: patch PE_PanelItemViewRow to use
theme.hover so row background matches CE_ItemViewItem.
2026-02-26 08:21:15 -07:00
IChooseYou
52f751e751 fix: redesign Type Aliases dialog — visible presets, compact layout
stdint button now fills cells with actual type names instead of clearing
to empty. Removed redundant Reset button, hidden column/row headers,
filtered out irrelevant types (Vec/Mat/Struct/Array). Fixed item view
hover being invisible on dark themes by painting explicit fillRect.
2026-02-25 17:39:17 -07:00
IChooseYou
0a19789a9d feat: enhance workspace dock, reorganize menus, fix Reclass Dark theme
- Workspace dock: show member count per type, expandable child rows
  (Type Name format, Hex padding filtered), search/filter box with
  recursive matching, collapsed by default, double-click navigates
  to member in editor
- Menu reorganization: Import/Export submenus, new Tools menu (Type
  Aliases, MCP Server, Options), Data Source moved to View, renamed
  Unload→Close Project, Unsplit→Remove Split, Current Tab Source→
  Data Source
- View menu: add Relative Offsets toggle (persisted, applies to all
  editors and new splits)
- Fix Reclass Dark theme: hover/selected colors were identical to
  background (#1e1e1e), now #2a2a2a/#2a2d2e for visible contrast
- Dim MDI tab text via QPalette::WindowText (Fusion ignores CSS color)
- Remove dead QProxyStyle tab handlers (never called for QMdiArea)
2026-02-25 14:27:02 -07:00
IChooseYou
62a68bef80 fix: align workspace dock header with MDI tab bar, dim tab text
Use QProxyStyle for tab height (24px) and text color instead of CSS.
Selected/hover tabs now use textDim to match the dock header.
2026-02-24 15:16:33 -07:00
IChooseYou
4941f860b6 docs: fix misleading README claims, add missing features, remove hr noise
- Fix "server does not start by default" (MCP now auto-starts)
- Rephrase tagline to name ReClass.NET/ReClassEx directly
- Add missing features: enums, bitfields, PDB import, themes, disasm preview, heatmap, MDI tabs, import/export
- Note Qt 5 support alongside Qt 6
- Align autoStartMcp default to true in options dialog
- Remove all horizontal rule separators
2026-02-24 12:48:50 -07:00
IChooseYou
c45d51d736 feat: shimmer status bar for MCP activity, auto-start MCP, remove "Ready" spam
- Add ShimmerLabel widget with animated glow band for MCP tool activity
- Separate app/MCP status channels (setAppStatus/setMcpStatus/clearMcpStatus)
- 750ms delayed clear so shimmer stays visible after fast tool calls
- MCP auto-starts on launch by default
- Remove "Ready" text that was overwriting useful status info
- Add statusText field to project.state MCP response
2026-02-24 12:31:25 -07:00
IChooseYou
5b46065403 feat: enum/bitfield editing, MCP guard rails, PDB anonymous type inlining
- Enum inline editing: name/value commit handling, auto-sort by value
- Bitfield support in PDB import with proper container nodes
- Per-member hover/selection highlighting (kMemberBit encoding)
- Context menu fixes for enum/bitfield member lines
- MCP pagination (limit/offset), includeMembers param, tree.search tool
- MCP status bar activity indicator for tool calls
- PDB anonymous type inlining: inline <unnamed-tag> types as children
- Skip anonymous pointer targets to prevent root orphans
- Enum import diagnostics for debugging missing enums
2026-02-24 10:37:42 -07:00
IChooseYou
4706f7b782 Merge branch 'docs' — update README and add banner SVGs 2026-02-23 18:33:41 -07:00
IChooseYou
fe9bfafa3b Merge pull request #3 from H4vC/main
perf: removed redundant cache invalidations and preindexed lookups for pdbs
2026-02-23 16:07:27 -07:00
IChooseYou
ff928df685 feat: enum support, workspace styling, EPROCESS/MMPFN test data
- Import enums from C/C++ source and PDB with name/value members
- Compose/format/generate enum definitions properly
- Workspace dock: rename to Project, theme-based titlebar and selection
- Add comprehensive EPROCESS.rcx (325 nodes) and MMPFN.rcx (65 nodes)
2026-02-23 16:01:35 -07:00
Brit
d6e3c182fc perf(import-compose): removed redundant cache invalidations and preindexed lookups 2026-02-23 17:56:44 +01:00
IChooseYou
078a6028f0 fix: WinDbg provider stops auto-selecting module, new tabs inherit source
- WinDbg provider no longer picks arbitrary module[0] as name/base
  (was showing "WS2_32" for kernel dumps). Name is now generic
  "WinDbg (Live)" / "WinDbg (Dump)", base stays 0 so controller
  doesn't override user's address.
- Added throttled read failure logging to WinDbg provider.
- New tabs (File→New Class, workspace right-click) inherit the
  current tab's source/provider so users don't have to re-attach.
- Updated WinDbg provider tests for new behavior.
2026-02-23 08:08:46 -07:00
Sen66
d7a6e1862e update height of banner 2026-02-22 22:02:27 +01:00
Sen66
1ddf47a754 update svg 2026-02-22 22:01:29 +01:00
Sen66
1a885a8b1d update readme 2026-02-22 21:54:11 +01:00
IChooseYou
67218d3e48 fix: move payload init out of DllMain to avoid loader lock deadlock
RcxPayloadInit() is now an exported function called after LoadLibrary
returns. DllMain only handles cleanup on detach. Timer queue creation
under the loader lock was crashing target processes.
2026-02-22 13:14:01 -07:00
IChooseYou
f651edd740 feat: remove nonce/bootstrap from remote process IPC, use PID-only naming
Shared memory names simplified to Local\RCX_SHM_<pid>, no bootstrap
handshake needed. Payload uses CreateTimerQueueTimer (10ms poll) instead
of a dedicated server thread.
2026-02-22 11:36:24 -07:00
IChooseYou
25aaace382 Merge remote-tracking branch 'origin/fix-issue-2' 2026-02-22 11:09:05 -07:00
Sen66
b5ddb042b8 Try to fix missing DLLs at CI windows builds
Fix https://github.com/IChooseYou/Reclass/issues/2
2026-02-22 19:06:50 +01:00
IChooseYou
e900dea836 fix: menu bar item paint no longer covers title bar bottom border
Take full ownership of CE_MenuBarItem in MenuBarStyle — never
delegate to Fusion which unconditionally fills the full item rect.
Non-hovered items draw text only (transparent bg lets parent border
show through). Hover/pressed states fill adjusted rect leaving 1px
for the border. Pressed state uses darker(130) for visual feedback.
2026-02-22 11:05:54 -07:00
IChooseYou
b647a334bc docs: fix Remote Process description 2026-02-22 09:14:04 -07:00
IChooseYou
fc390bc1f7 docs: add Remote Process data source to README 2026-02-22 09:06:32 -07:00
IChooseYou
7efe740ec1 fix: hover invisible when theme.hover == background, remove CSS on QMenuBar
Move hover color fixup into Theme::fromJson so all consumers get a
visible hover automatically. Remove duplicate lighter(130) fallback
from applyGlobalTheme. Replace QMenuBar CSS with QPalette so
MenuBarStyle QProxyStyle is not bypassed. Add PE_PanelMenuBar and
CE_MenuBarEmptyArea suppression so Fusion never paints over the
title bar background.
2026-02-22 08:58:57 -07:00
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
computron
71bc51cbab fix: guard Windows-only setDarkTitleBar and fix deprecated addAction arg order 2026-02-15 11:28:39 -07:00
computron
60a97ab81b fix: add missing Unix headers for Linux build 2026-02-15 11:20:28 -07:00
IChooseYou
bb00e75019 CI: win64 + linux64 builds, guard Windows-only targets for cross-platform 2026-02-15 11:16:09 -07:00
IChooseYou
c038c59e34 CI: add write permission for releases 2026-02-15 11:01:31 -07:00
IChooseYou
862f76b984 CI: auto-upload build zip to latest release 2026-02-15 10:24:46 -07:00
sysadmin
818285a76e CI: skip editor/windbg/com tests that need display or debug tools 2026-02-15 09:49:34 -07:00
sysadmin
ef5e2ebdb9 CI: fix Qt6 install - remove invalid module, pin aqtversion, use 6.8.1 LTS 2026-02-15 09:39:52 -07:00
sysadmin
75fedd2222 CI: switch to Qt6 2026-02-15 09:34:48 -07:00
sysadmin
389745e501 Add Windows CI build 2026-02-15 09:30:49 -07:00
sysadmin
1473a58742 IChooseYou 2026-02-15 09:23:17 -07:00
untitled
4192a4dad3 Hide project tree by default, remove 1px menu border, darken hover/selected theme colors 2026-02-15 08:29:59 -07:00
IChooseYou
4c6bb9564f Fix 7 verified bugs: ref invalidation, bounds check, double refresh, dangling pointer, undo bypass, overflow, hash collision
- BUG-1 (HIGH): Replace dangling QVector reference with local copies in applyTypePopupResult
- BUG-2 (MEDIUM): Add missing upper-bound check in EditTarget::Name handler
- BUG-5 (LOW): Remove redundant unconditional refresh() at end of applyTypePopupResult
- BUG-6 (LOW): Use QPointer for m_cachedPopup to auto-null on parent destruction
- BUG-7 (LOW): Rewrite materializeRefChildren to use undo macro (cmd::Insert + cmd::Collapse)
- BUG-8 (LOW): Guard against integer overflow in byteSize() and clamp arrayLen/strLen in fromJson
- BUG-9 (LOW): Use QPair<uint64_t,uint64_t> key in collectPointerRanges visited set
2026-02-15 08:16:52 -07:00
Sen66
0ef9841f90 Added options dialog 2026-02-15 03:24:12 +01:00
IChooseYOu
0a8244dad4 Single-click type chooser, popup warmup fix, rename ProcessMemory plugin
- Type chooser popup now opens on single click (no need to pre-select node)
- Fix ~170ms first-open delay by pre-initializing Qt popup subsystem at startup
- Rename ProcessMemoryWindows -> ProcessMemory (already supports Linux)
2026-02-14 16:08:44 -07:00
IChooseYou
c856ba2697 WinDbg plugin, ProcessMemoryWindows, dialog cleanup, and misc fixes
- Add WinDbgMemory plugin with debug server connection support
- Replace ProcessMemory plugin with Windows-specific ProcessMemoryWindows
- Simplify WinDbg dialog: single panel, no tabs, palette-based theming
- Fix example text visibility on dark themes (QPalette::Dark -> Disabled WindowText)
- Fix "file" -> "File" capitalization in source menu
- Add windbg_provider and com_security tests
2026-02-14 13:40:58 -07:00
IChooseYou
b44dc9e96b Update screenshot 2026-02-13 17:58:27 -07:00
IChooseYou
0f2ded471f Update screenshot 2026-02-13 17:56:07 -07:00
IChooseYou
c9377c3afd Show Icon uses 24x24 icon instead of 16x16 2026-02-13 17:47:14 -07:00
IChooseYou
a86912add1 Theme system overhaul, UI polish, and VS2022 Dark theme
- Replaced hardcoded theme factories with JSON files + CMake build step
- Shared ThemeFieldMeta table for DRY serialization and editor UI
- Fixed live preview (auto-triggers on color change, no toggle button)
- Fixed duplicate theme entries when editing built-in themes
- Moved title bar from icon to bold "Reclass" text with View > Show Icon toggle
- MDI tabs: 24px height, unicode close button styled like TypeSelectorPopup
- Added VS2022 Dark theme with purple accent colors
- Status bar padding, removed monospace font overrides on tabs/statusbar
- Default startup opens Ball demo + Unnamed hex64 tabs
2026-02-13 16:23:12 -07:00
Sen66
5a9a6b754f Expand recursive structs instead of silently skipping cycle fields
Self-referential struct children (e.g. Ball containing Ball) now render
as collapsed struct headers with a cycle marker. Clicking the fold margin
materializes the referenced children and auto-expands the recursive child,
allowing unlimited depth exploration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:11:10 +01:00
Sen66
0df52e82b8 Added custom title bar & border color when focused 2026-02-13 19:09:11 +01:00
Sen66
9a342286ee codicon.ttf isn't actually used 2026-02-13 19:01:13 +01:00
IChooseYou
1af0e17ef8 Remove contrast tool from theme editor, keep color config only 2026-02-13 10:00:21 -07:00
IChooseYou
a45d66dd4e Remove live process note from README 2026-02-13 07:35:43 -07:00
IChooseYou
6922166e3c Add MCP bridge connection instructions to README 2026-02-13 07:34:08 -07:00
ichooseu
ffde3343dd Type chooser: fix composite type changes (struct, pointer, array modifiers) 2026-02-13 07:30:40 -07:00
iamacontributor
c86a6dbc73 Tab titles show root struct name instead of Untitled, sync MDI tab font with editor, rename MCP pipe/exe to ReclassMcpBridge 2026-02-13 06:51:09 -07:00
Sen66
b153665059 Properly delete selected nodes 2026-02-13 02:27:01 +01:00
Sen66
a88b584ca0 Show relative offsets or absolute on the left side 2026-02-13 02:11:09 +01:00
Sen66
3e827194b8 Adjustments to scrollbars 2026-02-13 01:55:52 +01:00
Sen66
9f1c85913c Fix highlighting of hover value for Vec & OOB checks for line 2026-02-13 01:52:03 +01:00
Sen66
cb151ab850 Fix fonts not being themed after switching 2026-02-13 00:45:51 +01:00
sysadmin
b0aa7cda67 Align hex byte preview column with value column using dynamic nameW 2026-02-12 13:54:52 -07:00
IChooseYou
4b1d3e9d3f Theme preview/revert, theme editor enhancements, build and deploy updates 2026-02-12 12:37:09 -07:00
IChooseYou
e73b783cda QMenu + QMenuBar hover: amber indHoverSpan text via QProxyStyle
MenuBarStyle.drawControl overrides CE_MenuBarItem and CE_MenuItem:
- Menu bar: strips hover state flags, swaps ButtonText to Link (amber),
  delegates to Fusion for identical layout (no text shift)
- Popup menus: patches Highlight→Mid, HighlightedText→Link, delegates
  to Fusion for icons/shortcuts/checkmarks

Test: real QEvent::Enter + MouseMove delivery, QScreen::grabWindow
screenshot, pixel-scan for amber, always saves PNGs for inspection.
2026-02-12 10:22:00 -07:00
IChooseYou
7e0b995f4d Update README with current project state 2026-02-11 14:04:15 -07:00
IChooseYou
52d65b4a23 Update screenshot with Vehicle demo 2026-02-11 13:54:48 -07:00
IChooseYou
db5d3ae311 Unified type popup: explicit TypeEntry model, modifier toggles, section headers
- Replace sentinel-id scheme (kPrimBase) with TypeEntry::Kind discriminant
- Merge showTypeSelectorPopup + showTypePickerPopup into single showTypePopup
- Merge applyTypePickerResult into applyTypePopupResult matching on entryKind
- Add modifier toggle buttons (plain, *, **, [n]) with array count input
- Add section headers (primitives / project types) with dim centered styling
- Root mode shows project types first; non-Root sorts same-size primitives first
- Remove popup outside border, flat "Create new type" button
- Add parseTypeSpec parser, update tests with new TypeEntry API
2026-02-11 13:39:43 -07:00
batallaion caputa
33a093ae7d Widen value column to 96 chars, remove Mat4x4 truncation
kColValue 32 -> 96 to prevent float truncation with ellipsis.
Mat4x4 rows bypass fit() entirely so long matrix rows display fully.
Updated test expectations to use kColValue instead of hardcoded 32.
2026-02-11 10:44:24 -07:00
IChooseYou
968476b65a Mat4x4 row labels, no scientific notation, per-component inline editing
- Add row0..row3 labels to Mat4x4 grid display with aligned columns
- Rewrite fmtFloat() to never use scientific notation (plain decimal + trim)
- Enable per-component inline editing for all 16 Mat4x4 floats
- Fix click-to-edit always selecting first component (thread column from hitTestTarget)
- Add isMatrixKind() helper, remove Mat4x4 from context menu edit exclusion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 10:36:15 -07:00
IChooseYou
df07b61144 Array element offset display, fold arrow UX, type picker popup, and provider cleanup
- Show relative hex offset on array element separators ([N] +0x...)
- Dim fold arrows and add hover highlight for better visibility
- Extend fold/chevron click areas for easier interaction
- Add type picker popup for array element type and pointer target editing
- Remove process_provider.h in favor of plugin-based provider system
- Expand compose/format to handle struct-of-array type names and widths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 09:13:17 -07:00
IChooseYou
3db051f4ba Fix computeDataExtent undercount, setNodeValue signed offset, and dead isStringArray
- computeDataExtent: use structSpan() for Struct/Array nodes instead of
  byteSize() which returns 0 for Array-of-Struct; use int64_t intermediates
  to prevent truncation of offsets beyond 2GB
- setNodeValue: guard against negative computeOffset results before
  casting to uint64_t (prevents wrapping to huge addresses on malformed trees)
- isStringArray: comment out unused method (was checking UInt8/UInt16
  instead of UTF8/UTF16); corrected version preserved in comment
2026-02-11 08:43:34 -07:00
IChooseYou
fc48fd6d2d Merge IChooseYou/Reclass qt5-support branch 2026-02-11 05:20:56 -07:00
Duncan Ogilvie
0ffb7d6f58 Fix test_type_selector for Qt5 and stale expectation
- Register uint64_t metatype for QSignalSpy in Qt5 (not needed in Qt6)
- Update command row expectation: "NoName" matches controller output
2026-02-10 23:08:24 +01:00
Duncan Ogilvie
0b0d9f23e1 Add dark title bar support for Qt5 2026-02-10 23:07:16 +01:00
Duncan Ogilvie
7194322831 Add support for Qt5 2026-02-10 23:06:55 +01:00
IChooseYou
5f1fd56171 Fix process memory provider base address sync and live refresh
Provider base address now stays in sync with tree base address when
changed via ChangeBase command, fixing reads from arbitrary memory
regions like KUSER_SHARED_DATA at 0x7FFE0000. ReadProcessMemory
handles partial reads gracefully. Snapshot extent uses tree-based
calculation instead of provider size to avoid oversized reads.
MCP source.switch gains pid parameter for programmatic process attach.
MCP server starts by default with logging and slow mode support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 13:04:28 -07:00
IChooseYou
6bd61a6b78 Fix indentation in README build instructions 2026-02-10 11:19:30 -07:00
IChooseYou
60990362a0 Remove live data line from README 2026-02-10 11:14:23 -07:00
IChooseYou
55ef83a39f Update README description and project status 2026-02-10 11:13:24 -07:00
IChooseYou
e034fe6f6c Remove outdated video link from README 2026-02-10 11:09:16 -07:00
IChooseYou
2e387f2dfc Update screenshot 2026-02-10 11:07:58 -07:00
IChooseYou
4295460597 Add MCP bridge for external tool integration
Embedded JSON-RPC server over named pipes (rcx-mcp) enabling external
tools like Claude Code to inspect and manipulate the node tree, read/write
hex data, switch sources, and trigger UI actions. Includes stdio adapter
(rcx-mcp-stdio) for stdin/stdout transport. Server is stopped by default;
user starts via File > Start MCP Server.

Also extracts MainWindow class declaration to mainwindow.h and improves
type selector popup Esc button styling.
2026-02-10 10:55:27 -07:00
IChooseYou
df566064ba Fix root class rename targeting wrong struct when using type selector
RootClassName and RootClassType edit handlers now use m_viewRootId
instead of blindly picking the first root struct. Default name for
unnamed structs changed from <no name> to NoName. Dim the opening
brace on the command row to match the rest of the bar's grey text.
2026-02-10 08:06:43 -07:00
IChooseYou
24a7e68136 Add theme system with Reclass Dark and Warm built-in themes
Replaces ~40 hardcoded color values with 27 semantic color roles.
Adds ThemeManager singleton, theme editor dialog, View > Theme menu,
JSON persistence for user themes, and fixes inline edit selection
color from blue #264f78 to #2b2b2b.
2026-02-10 07:46:18 -07:00
IChooseYou
8eab304538 Dim opening brace on root command row to match child struct headers 2026-02-10 06:19:34 -07:00
IChooseYou
9dd104ff34 Copy All as Text now includes margin offset text prepended to each line 2026-02-10 06:17:10 -07:00
IChooseYou
910b607b79 Rename ReclassX to Reclass in window titles and About dialog 2026-02-10 06:10:09 -07:00
IChooseYou
c415b11825 Replace About dialog with build timestamp and GitHub link 2026-02-10 06:06:18 -07:00
IChooseYou
1d6fddb51e Replace Iosevka with JetBrains Mono, fix scrollbar and inline edit UX
- Replace embedded Iosevka font with JetBrains Mono as default font
- Fix wide horizontal scrollbar by enabling SCI_SETSCROLLWIDTHTRACKING
- Remove 28-char trailing whitespace padding from all lines
- Fix arrow keys not collapsing selection in inline edit mode
- Dim struct/array braces ({ and };) to match hex node styling
- Resize margin immediately on font change
2026-02-10 06:00:17 -07:00
penguin time
276dcae444 Reduce status bar left padding to 10px 2026-02-10 04:26:42 -07:00
sysadmin
659fb7bd32 Merge remote-tracking branch 'origin/linux' 2026-02-10 04:25:39 -07:00
administrator
85b840379d Remove Load Binary, View C++/Reclass actions, Debug tab, and dead code
Data sources are now provided by source plugins. View switching is
handled by bottom tabs (Reclass / C/C++ only). Also applies grey
highlight/selected styling globally and removes per-menu stylesheet.
2026-02-09 14:06:22 -07:00
batallion2
f4149faa9a Add type selector popup, view root switching, and new type creation
- Type selector chevron [▸] on command row opens searchable popup
- Popup lists all root structs with filter, keyboard nav, side-triangle indicator
- Selecting a type switches the editor view via setViewRootId
- "Create new type" inserts a new root struct with no name
- Command row displays the active view root's name
- Tests for chevron detection, span compatibility, view switching, undo
2026-02-09 12:21:03 -07:00
Sen66
b6c713eb29 Updated Readme 2026-02-09 15:14:35 +01:00
Sen66
4029b05298 Added linux support (tested on Ubuntu)
CMakeList: fixed for building on linux
processpicker: linux process enumeration
main.cpp: "_Exit()" works on linux & windows
"ProcessMemory" plugin: added linux support
2026-02-09 15:09:42 +01:00
batallion caputa
0e65b9997e Update README with build instructions and project description 2026-02-09 06:42:19 -07:00
battalion caputa
4caa7daa44 Merge branch 'plugin-system' into main
Adds basic plugin system support with plugin manager, provider registry,
plugin interface, and ProcessMemory example plugin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 16:13:56 -07:00
201 changed files with 115476 additions and 5199 deletions

262
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,262 @@
name: Build
on:
push:
branches:
- "**"
pull_request:
branches: [main]
permissions:
contents: write
jobs:
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Qt6 and MinGW
uses: jurplel/install-qt-action@v4
with:
version: "6.8.1"
arch: "win64_mingw"
tools: "tools_mingw1310,qt.tools.win64_mingw1310"
cache: true
- name: Configure
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
gcc --version
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF \
-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
- name: Build
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
cmake --build build
- name: Install WDK NuGet
shell: pwsh
run: |
nuget install Microsoft.Windows.WDK.x64 -OutputDirectory wdk_pkg
$ntddk = Get-ChildItem wdk_pkg -Recurse -Filter "ntddk.h" |
Where-Object { $_.DirectoryName -like "*km*" } |
Select-Object -First 1
if (!$ntddk) { throw "ntddk.h not found in WDK NuGet package" }
$kmDir = $ntddk.DirectoryName
$incRoot = Split-Path $kmDir -Parent
Write-Host "WDK include root: $incRoot"
echo "WDK_INC_ROOT=$incRoot" >> $env:GITHUB_ENV
$ntos = Get-ChildItem wdk_pkg -Recurse -Filter "ntoskrnl.lib" |
Where-Object { $_.DirectoryName -like "*x64*" } |
Select-Object -First 1
if (!$ntos) { throw "ntoskrnl.lib not found in WDK NuGet package" }
$libRoot = Split-Path (Split-Path $ntos.DirectoryName -Parent) -Parent
Write-Host "WDK lib root: $libRoot"
echo "WDK_LIB_ROOT=$libRoot" >> $env:GITHUB_ENV
$specstr = Get-ChildItem wdk_pkg -Recurse -Filter "specstrings.h" |
Select-Object -First 1
if (!$specstr) { throw "specstrings.h not found in SDK NuGet package" }
$sdkIncRoot = Split-Path $specstr.DirectoryName -Parent
Write-Host "SDK include root: $sdkIncRoot"
echo "SDK_INC_ROOT=$sdkIncRoot" >> $env:GITHUB_ENV
- name: Build kernel driver
shell: cmd
run: call plugins\KernelMemory\driver\build_driver.bat
- name: Test
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
ctest --test-dir build --output-on-failure
- name: Package release zip
shell: bash
run: |
export PATH="$IQTA_TOOLS/mingw1310_64/bin:$PATH"
mkdir -p release
cp build/Reclass.exe release/
cp build/ReclassMcpBridge.exe release/
cp build/*.dll release/ 2>/dev/null || true
cp -r build/platforms release/ 2>/dev/null || true
cp -r build/styles release/ 2>/dev/null || true
cp -r build/imageformats release/ 2>/dev/null || true
cp -r build/iconengines release/ 2>/dev/null || true
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw release/Reclass.exe
mkdir -p release/Plugins
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
cp plugins/KernelMemory/driver/build/rcxdrv.sys release/Plugins/ 2>/dev/null || true
cp -r build/themes release/ 2>/dev/null || true
cp -r build/examples release/ 2>/dev/null || true
cp build/screenshot.png release/ 2>/dev/null || true
cd release && 7z a ../Reclass-win64-qt6.zip *
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: Reclass-win64-qt6
path: Reclass-win64-qt6.zip
linux:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Qt6
uses: jurplel/install-qt-action@v4
with:
version: "6.8.1"
cache: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y ninja-build libgl1-mesa-dev libfuse2 libxcb-cursor0
- name: Configure
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF
- name: Build
run: cmake --build build
- name: Test
run: ctest --test-dir build --output-on-failure
- name: Create AppImage
run: |
# Download linuxdeploy and Qt plugin
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage
# Build AppDir structure
mkdir -p AppDir/usr/bin AppDir/usr/share/icons/hicolor/256x256/apps
cp build/Reclass AppDir/usr/bin/
cp build/ReclassMcpBridge AppDir/usr/bin/
cp -r build/themes AppDir/usr/bin/ 2>/dev/null || true
cp -r build/examples AppDir/usr/bin/ 2>/dev/null || true
mkdir -p AppDir/usr/bin/Plugins
cp build/Plugins/*.so AppDir/usr/bin/Plugins/ 2>/dev/null || true
cp src/icons/class.png AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png
# Create AppImage with Qt libs bundled
# install-qt-action adds Qt bin to PATH; find qmake there
QMAKE_BIN=$(which qmake 2>/dev/null || which qmake6 2>/dev/null || find "$RUNNER_WORKSPACE" -name qmake -path "*/bin/*" | head -1)
echo "Found qmake at: $QMAKE_BIN"
export QMAKE="$QMAKE_BIN"
QT_ROOT=$(dirname "$(dirname "$QMAKE_BIN")")
export LD_LIBRARY_PATH="$QT_ROOT/lib:$LD_LIBRARY_PATH"
export EXTRA_QT_PLUGINS="svg;iconengines"
./linuxdeploy-x86_64.AppImage --appdir AppDir \
--desktop-file deploy/Reclass.desktop \
--icon-file AppDir/usr/share/icons/hicolor/256x256/apps/reclass.png \
--plugin qt \
--output appimage
# Rename to final name
ls Reclass-*.AppImage
mv Reclass-*.AppImage Reclass-linux64-qt6.AppImage
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: Reclass-linux64-qt6
path: Reclass-linux64-qt6.AppImage
macos:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-15
qt_arch: clang_arm64
artifact_name: Reclass-macos-arm64-qt6
zip_name: Reclass-macos-arm64-qt6.zip
- os: macos-15-intel
qt_arch: clang_64
artifact_name: Reclass-macos-x86_64-qt6
zip_name: Reclass-macos-x86_64-qt6.zip
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: |
brew update
brew install cmake ninja qt
- name: Configure Qt paths
run: |
QT_PREFIX="$(brew --prefix qt)"
echo "QT_PREFIX=$QT_PREFIX" >> "$GITHUB_ENV"
echo "PATH=$QT_PREFIX/bin:$PATH" >> "$GITHUB_ENV"
- name: Configure
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_UI_TESTS=OFF -DCMAKE_PREFIX_PATH="$QT_PREFIX"
- name: Build
run: cmake --build build
- name: Test
run: ctest --test-dir build --output-on-failure
- name: Package app zip
run: |
MACDEPLOYQT_BIN="$QT_PREFIX/bin/macdeployqt"
if [ ! -x "$MACDEPLOYQT_BIN" ]; then
MACDEPLOYQT_BIN=$(which macdeployqt 2>/dev/null || find "$RUNNER_WORKSPACE" -name macdeployqt -path "*/bin/*" | head -1)
fi
echo "Found macdeployqt at: $MACDEPLOYQT_BIN"
"$MACDEPLOYQT_BIN" build/Reclass.app -always-overwrite
codesign --force --deep --sign - build/Reclass.app
ditto -c -k --sequesterRsrc --keepParent build/Reclass.app "${{ matrix.zip_name }}"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: ${{ matrix.zip_name }}
release:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [windows, linux, macos]
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Get date tag
id: date
run: echo "tag=$(date +'%d-%m-%Y')" >> "$GITHUB_OUTPUT"
- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: snapshot-${{ steps.date.outputs.tag }}
name: Snapshot ${{ steps.date.outputs.tag }}
body: |
Automated snapshot from main branch.
Commit: ${{ github.sha }}
prerelease: false
files: |
artifacts/Reclass-win64-qt6/Reclass-win64-qt6.zip
artifacts/Reclass-linux64-qt6/Reclass-linux64-qt6.AppImage
artifacts/Reclass-macos-arm64-qt6/Reclass-macos-arm64-qt6.zip
artifacts/Reclass-macos-x86_64-qt6/Reclass-macos-x86_64-qt6.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored
View File

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

6
.gitmodules vendored
View File

@@ -1,3 +1,9 @@
[submodule "third_party/qscintilla"]
path = third_party/qscintilla
url = https://github.com/brCreate/QScintilla.git
[submodule "third_party/raw_pdb"]
path = third_party/raw_pdb
url = https://github.com/MolecularMatters/raw_pdb.git
[submodule "third_party/fadec"]
path = third_party/fadec
url = https://github.com/aengelke/fadec.git

View File

@@ -1,18 +1,97 @@
cmake_minimum_required(VERSION 3.20)
project(ReclassX VERSION 0.1 LANGUAGES CXX)
project(Reclass VERSION 0.1 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 11)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets PrintSupport Svg Concurrent)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# Find Qt6 or Qt5 (config mode first, then FindQt5.cmake module for auto-download)
set(_QT_COMPONENTS Core Widgets PrintSupport Svg Concurrent Network)
find_package(QT NAMES Qt6 Qt5 COMPONENTS ${_QT_COMPONENTS} QUIET)
if(NOT QT_FOUND)
find_package(Qt5 REQUIRED COMPONENTS ${_QT_COMPONENTS})
set(QT_VERSION_MAJOR 5)
endif()
# The NAMES variant only detects the version; load the actual component targets
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS ${_QT_COMPONENTS})
set(QT Qt${QT_VERSION_MAJOR})
message(STATUS "Using ${QT}: ${${QT}_DIR}")
# ── ABI sanity check: prevent MSVC ↔ MinGW Qt mismatch ──
# Building with MSVC against MinGW Qt (or vice versa) compiles fine but
# crashes immediately at runtime (ABI mismatch in QString/QSettings internals).
if(MSVC AND "${${QT}_DIR}" MATCHES "mingw")
message(FATAL_ERROR
"Qt installation was built with MinGW but this project is being compiled with MSVC.\n"
" Qt found at: ${${QT}_DIR}\n"
"This will compile but crash at startup due to ABI mismatch.\n"
"Fix: install Qt for MSVC (e.g. msvc2019_64) and set CMAKE_PREFIX_PATH to it:\n"
" cmake -DCMAKE_PREFIX_PATH=C:/Qt/6.5.2/msvc2019_64 ..")
elseif(MINGW AND "${${QT}_DIR}" MATCHES "msvc")
message(FATAL_ERROR
"Qt installation was built with MSVC but this project is being compiled with MinGW.\n"
" Qt found at: ${${QT}_DIR}\n"
"This will compile but crash at startup due to ABI mismatch.\n"
"Fix: install Qt for MinGW and set CMAKE_PREFIX_PATH to it:\n"
" cmake -DCMAKE_PREFIX_PATH=C:/Qt/6.5.2/mingw_64 ..")
endif()
# ── MSVC compile flags ──
if(MSVC)
# /utf-8: treat source and execution character sets as UTF-8
# /MP: multi-processor compilation
add_compile_options(/utf-8 /MP)
endif()
# Qt5 on Windows needs WinExtras for HICON conversion
set(_QT_WINEXTRAS "")
if(QT_VERSION_MAJOR EQUAL 5 AND WIN32)
find_package(Qt5 REQUIRED COMPONENTS WinExtras)
set(_QT_WINEXTRAS Qt5::WinExtras)
endif()
find_package(QScintilla REQUIRED)
add_executable(ReclassX
# RawPDB — direct PDB file reader (no DIA SDK / msdia140.dll dependency)
file(GLOB RAW_PDB_SRCS third_party/raw_pdb/src/*.cpp)
add_library(raw_pdb STATIC ${RAW_PDB_SRCS})
target_include_directories(raw_pdb PUBLIC third_party/raw_pdb/src)
target_compile_features(raw_pdb PRIVATE cxx_std_11)
# PDB_CRT.h forward-declares printf/memcmp/etc with __cdecl which conflicts
# with non-MSVC compilers (GCC, Clang, MinGW). Force-include a prefix header
# that pulls in the real CRT headers and strips __cdecl.
if(NOT MSVC)
target_compile_options(raw_pdb PUBLIC
-include "${CMAKE_CURRENT_SOURCE_DIR}/cmake/raw_pdb_prefix.h")
endif()
if(WIN32)
target_link_libraries(raw_pdb PRIVATE rpcrt4)
endif()
# Fadec — generate decode tables (.inc files) from instrs.txt at configure time
find_package(Python3 3.9 REQUIRED)
set(FADEC_DIR "${CMAKE_SOURCE_DIR}/third_party/fadec")
if(NOT EXISTS "${FADEC_DIR}/fadec-decode-public.inc")
message(STATUS "Generating fadec decode tables...")
execute_process(
COMMAND ${Python3_EXECUTABLE} "${FADEC_DIR}/parseinstrs.py" decode
"${FADEC_DIR}/instrs.txt"
"${FADEC_DIR}/fadec-decode-public.inc"
"${FADEC_DIR}/fadec-decode-private.inc"
--32 --64
RESULT_VARIABLE _fadec_result
)
if(NOT _fadec_result EQUAL 0)
message(FATAL_ERROR "Failed to generate fadec decode tables")
endif()
endif()
add_executable(Reclass
src/main.cpp
src/editor.h
src/editor.cpp
@@ -28,39 +107,176 @@ add_executable(ReclassX
src/resources.qrc
src/core.h
src/workspace_model.h
src/providers/buffer_provider.h src/providers/null_provider.h src/providers/process_provider.h src/providers/provider.h src/providers/snapshot_provider.h
src/providers/buffer_provider.h src/providers/null_provider.h src/providers/provider.h src/providers/snapshot_provider.h
src/providerregistry.cpp
src/providerregistry.h
src/pluginmanager.cpp
src/pluginmanager.h
src/typeselectorpopup.h
src/typeselectorpopup.cpp
src/themes/theme.h
src/themes/theme.cpp
src/themes/thememanager.h
src/themes/thememanager.cpp
src/themes/themeeditor.h
src/themes/themeeditor.cpp
src/imports/import_reclass_xml.h
src/imports/import_reclass_xml.cpp
src/imports/import_source.h
src/imports/import_source.cpp
src/imports/export_reclass_xml.h
src/imports/export_reclass_xml.cpp
src/imports/import_pdb.h
src/imports/import_pdb.cpp
src/imports/import_pdb_dialog.h
src/imports/import_pdb_dialog.cpp
src/scanner.h
src/scanner.cpp
src/scannerpanel.h
src/scannerpanel.cpp
src/mainwindow.h
src/startpage.h
src/dock_tab_buttons.h
src/optionsdialog.h
src/optionsdialog.cpp
src/titlebar.h
src/titlebar.cpp
src/macos_titlebar.h
$<$<PLATFORM_ID:Darwin>:src/macos_titlebar.mm>
src/mcp/mcp_bridge.h
src/mcp/mcp_bridge.cpp
src/addressparser.h
src/addressparser.cpp
src/symbolstore.h
src/symbolstore.cpp
src/symbol_downloader.h
src/symbol_downloader.cpp
src/imports/pe_debug_info.h
src/imports/pe_debug_info.cpp
src/disasm.h
src/disasm.cpp
third_party/fadec/decode.c
third_party/fadec/format.c
$<$<PLATFORM_ID:Windows>:src/app.rc>
)
target_include_directories(ReclassX PRIVATE src)
if(APPLE)
set_target_properties(Reclass PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_ICON_FILE "class.icns"
)
target_sources(Reclass PRIVATE src/icons/class.icns)
set_source_files_properties(src/icons/class.icns
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
endif()
target_link_libraries(ReclassX PRIVATE
Qt6::Widgets
Qt6::PrintSupport
Qt6::Svg
Qt6::Concurrent
target_include_directories(Reclass PRIVATE src third_party/fadec)
target_link_libraries(Reclass PRIVATE
${QT}::Widgets
${QT}::PrintSupport
${QT}::Svg
${QT}::Concurrent
${QT}::Network
QScintilla::QScintilla
dbghelp
psapi
${_QT_WINEXTRAS}
)
if(WIN32)
target_link_libraries(Reclass PRIVATE dbghelp dwmapi psapi raw_pdb)
add_custom_target(screenshot ALL
COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
DEPENDS ReclassX
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Capturing UI screenshot with class open..."
)
# Copy Debugging Tools dbghelp.dll next to Reclass.exe so the Windows
# loader picks it up (app dir > System32). The system dbghelp.dll
# lacks StackWalk2 which the tools dbgeng.dll needs for remote debug.
set(_DBG_TOOLS_DIRS
"C:/Program Files (x86)/Windows Kits/10/Debuggers/x64"
"C:/Program Files/Windows Kits/10/Debuggers/x64")
foreach(_dir ${_DBG_TOOLS_DIRS})
if(EXISTS "${_dir}/dbghelp.dll")
foreach(_dll dbghelp.dll dbgcore.dll symsrv.dll)
if(EXISTS "${_dir}/${_dll}")
add_custom_command(TARGET Reclass POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_dir}/${_dll}" $<TARGET_FILE_DIR:Reclass>
COMMENT "Copying ${_dll} from Debugging Tools")
endif()
endforeach()
break()
endif()
endforeach()
endif()
add_executable(ReclassMcpBridge tools/rcx-mcp-stdio.cpp)
target_link_libraries(ReclassMcpBridge PRIVATE ${QT}::Core ${QT}::Network)
if(APPLE)
add_custom_command(TARGET ReclassMcpBridge POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:ReclassMcpBridge>
$<TARGET_FILE_DIR:Reclass>/ReclassMcpBridge
COMMENT "Bundling ReclassMcpBridge into Reclass.app"
)
endif()
# Copy built-in theme JSON files next to the executable.
# For single-config generators (Ninja/Make) the exe is in ${CMAKE_BINARY_DIR},
# for multi-config generators (MSVC/Xcode) it's in ${CMAKE_BINARY_DIR}/<config>.
# Using a post-build copy with $<TARGET_FILE_DIR:Reclass> handles both.
file(GLOB _theme_files "${CMAKE_SOURCE_DIR}/src/themes/defaults/*.json")
# Single-config: configure_file for IDE convenience (available before first build)
if(NOT CMAKE_CONFIGURATION_TYPES)
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/themes")
foreach(_tf ${_theme_files})
get_filename_component(_name ${_tf} NAME)
configure_file(${_tf} "${CMAKE_BINARY_DIR}/themes/${_name}" COPYONLY)
endforeach()
endif()
# Post-build: always copy to the actual exe directory (works for all generators)
add_custom_command(TARGET Reclass POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/themes"
COMMENT "Creating themes directory next to executable")
foreach(_tf ${_theme_files})
get_filename_component(_name ${_tf} NAME)
add_custom_command(TARGET Reclass POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_tf}" "$<TARGET_FILE_DIR:Reclass>/themes/${_name}")
endforeach()
if(APPLE)
target_sources(Reclass PRIVATE ${_theme_files})
set_source_files_properties(${_theme_files}
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/themes")
endif()
# Copy example .rcx files next to the executable (same logic as themes)
file(GLOB _example_files "${CMAKE_SOURCE_DIR}/src/examples/*.rcx")
if(NOT CMAKE_CONFIGURATION_TYPES)
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/examples")
foreach(_ef ${_example_files})
get_filename_component(_name ${_ef} NAME)
configure_file(${_ef} "${CMAKE_BINARY_DIR}/examples/${_name}" COPYONLY)
endforeach()
endif()
add_custom_command(TARGET Reclass POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/examples"
COMMENT "Creating examples directory next to executable")
foreach(_ef ${_example_files})
get_filename_component(_name ${_ef} NAME)
add_custom_command(TARGET Reclass POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_ef}" "$<TARGET_FILE_DIR:Reclass>/examples/${_name}")
endforeach()
if(APPLE)
target_sources(Reclass PRIVATE ${_example_files})
set_source_files_properties(${_example_files}
PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/examples")
endif()
include(deploy)
add_custom_target(copy_demo ALL
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_BINARY_DIR}/demo.rcx
${CMAKE_SOURCE_DIR}/src/examples/demo.rcx
DEPENDS screenshot
COMMENT "Copying demo.rcx to src/examples..."
)
set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
file(WRITE ${_combine_script} "
@@ -77,7 +293,7 @@ foreach(_f
\"${CMAKE_SOURCE_DIR}/src/generator.cpp\"
\"${CMAKE_SOURCE_DIR}/src/main.cpp\")
file(READ \${_f} _content)
file(APPEND \${_out} \${_content})
file(APPEND \${_out} \"\${_content}\")
file(APPEND \${_out} \"\\n\")
endforeach()
message(STATUS \"Combined sources -> \${_out}\")
@@ -85,95 +301,310 @@ message(STATUS \"Combined sources -> \${_out}\")
add_custom_target(combined ALL
COMMAND ${CMAKE_COMMAND} -P ${_combine_script}
DEPENDS ReclassX
DEPENDS Reclass
COMMENT "Combining all source files into h_cpp_combined.txt"
)
include(CTest)
if(BUILD_TESTING)
find_package(Qt6 REQUIRED COMPONENTS Test)
find_package(${QT} REQUIRED COMPONENTS Test)
enable_testing()
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp)
# Disasm/Fadec sources needed by any test that links editor.cpp
set(DISASM_SRCS src/disasm.cpp third_party/fadec/decode.c third_party/fadec/format.c)
# ── Headless tests (Qt::Core only — safe for CI without a display) ──
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_core PRIVATE src)
target_link_libraries(test_core PRIVATE Qt6::Core Qt6::Test)
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_core COMMAND test_core)
add_executable(test_format tests/test_format.cpp src/format.cpp)
add_executable(test_typeinfer tests/test_typeinfer.cpp)
target_include_directories(test_typeinfer PRIVATE src)
target_link_libraries(test_typeinfer PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_typeinfer COMMAND test_typeinfer)
add_executable(test_format tests/test_format.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_format PRIVATE src)
target_link_libraries(test_format PRIVATE Qt6::Core Qt6::Test)
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_format COMMAND test_format)
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp)
add_executable(test_compose tests/test_compose.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_compose PRIVATE src)
target_link_libraries(test_compose PRIVATE Qt6::Core Qt6::Test)
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_compose COMMAND test_compose)
add_executable(test_editor tests/test_editor.cpp src/editor.cpp src/compose.cpp src/format.cpp src/providerregistry.cpp)
target_include_directories(test_editor PRIVATE src)
target_link_libraries(test_editor PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Test
QScintilla::QScintilla)
add_test(NAME test_editor COMMAND test_editor)
add_executable(test_provider tests/test_provider.cpp)
target_include_directories(test_provider PRIVATE src)
target_link_libraries(test_provider PRIVATE Qt6::Core Qt6::Test)
target_link_libraries(test_provider PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_provider COMMAND test_provider)
add_executable(test_command_row tests/test_command_row.cpp)
target_include_directories(test_command_row PRIVATE src)
target_link_libraries(test_command_row PRIVATE Qt6::Core Qt6::Test)
target_link_libraries(test_command_row PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_command_row COMMAND test_command_row)
add_executable(test_provider_getSymbol tests/test_provider_getSymbol.cpp)
target_include_directories(test_provider_getSymbol PRIVATE src)
target_link_libraries(test_provider_getSymbol PRIVATE Qt6::Core Qt6::Test)
if(WIN32)
target_link_libraries(test_provider_getSymbol PRIVATE psapi)
endif()
add_test(NAME test_provider_getSymbol COMMAND test_provider_getSymbol)
add_executable(test_controller tests/test_controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp)
target_include_directories(test_controller PRIVATE src)
target_link_libraries(test_controller PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi)
add_test(NAME test_controller COMMAND test_controller)
add_executable(test_validation tests/test_validation.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp)
target_include_directories(test_validation PRIVATE src)
target_link_libraries(test_validation PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi)
add_test(NAME test_validation COMMAND test_validation)
add_executable(test_generator tests/test_generator.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_generator PRIVATE src)
target_link_libraries(test_generator PRIVATE Qt6::Core Qt6::Test)
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_generator COMMAND test_generator)
add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp)
target_include_directories(test_context_menu PRIVATE src)
target_link_libraries(test_context_menu PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi)
add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_import_xml tests/test_import_xml.cpp
src/imports/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_xml PRIVATE src)
target_link_libraries(test_import_xml PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_import_xml COMMAND test_import_xml)
add_executable(test_new_features tests/test_new_features.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp)
target_include_directories(test_new_features PRIVATE src)
target_link_libraries(test_new_features PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi)
add_test(NAME test_new_features COMMAND test_new_features)
add_executable(test_import_source tests/test_import_source.cpp
src/imports/import_source.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_source PRIVATE src)
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_import_source COMMAND test_import_source)
add_executable(test_export_xml tests/test_export_xml.cpp
src/imports/export_reclass_xml.cpp src/imports/import_reclass_xml.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_export_xml PRIVATE src)
target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_export_xml COMMAND test_export_xml)
add_executable(test_disasm tests/test_disasm.cpp
src/disasm.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
third_party/fadec/decode.c third_party/fadec/format.c)
target_include_directories(test_disasm PRIVATE src third_party/fadec)
target_link_libraries(test_disasm PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_disasm COMMAND test_disasm)
add_executable(test_addressparser tests/test_addressparser.cpp src/addressparser.cpp)
target_include_directories(test_addressparser PRIVATE src)
target_link_libraries(test_addressparser PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_addressparser COMMAND test_addressparser)
add_executable(test_static_fields tests/test_static_fields.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_static_fields PRIVATE src)
target_link_libraries(test_static_fields PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_static_fields COMMAND test_static_fields)
add_executable(test_scanner tests/test_scanner.cpp src/scanner.cpp)
target_include_directories(test_scanner PRIVATE src)
target_link_libraries(test_scanner PRIVATE ${QT}::Core ${QT}::Concurrent ${QT}::Test)
add_test(NAME test_scanner COMMAND test_scanner)
add_executable(test_32bit_support tests/test_32bit_support.cpp
src/generator.cpp src/imports/import_source.cpp src/imports/import_reclass_xml.cpp
src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_32bit_support PRIVATE src
${CMAKE_SOURCE_DIR}/plugins/RemoteProcessMemory)
target_link_libraries(test_32bit_support PRIVATE ${QT}::Core ${QT}::Widgets ${QT}::Test)
add_test(NAME test_32bit_support COMMAND test_32bit_support)
set_tests_properties(test_32bit_support PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
if(WIN32)
add_executable(test_import_pdb tests/test_import_pdb.cpp
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(test_import_pdb PRIVATE src)
target_link_libraries(test_import_pdb PRIVATE
${QT}::Core ${QT}::Test raw_pdb)
add_test(NAME test_import_pdb COMMAND test_import_pdb)
add_executable(bench_import_pdb tests/bench_import_pdb.cpp
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)
target_include_directories(bench_import_pdb PRIVATE src)
target_link_libraries(bench_import_pdb PRIVATE
${QT}::Core ${QT}::Test raw_pdb)
add_test(NAME bench_import_pdb COMMAND bench_import_pdb)
endif()
# ── UI tests (require Qt::Widgets / QScintilla / display — skip on headless CI) ──
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
if(BUILD_UI_TESTS)
add_executable(test_controller tests/test_controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_controller PRIVATE src third_party/fadec)
target_link_libraries(test_controller PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_controller COMMAND test_controller)
add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
target_link_libraries(test_context_menu PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_source_management tests/test_source_management.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_source_management PRIVATE src third_party/fadec)
target_link_libraries(test_source_management PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_source_management PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_source_management COMMAND test_source_management)
add_executable(test_editor tests/test_editor.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_editor PRIVATE src third_party/fadec)
target_link_libraries(test_editor PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_editor COMMAND test_editor)
add_executable(test_rendered_view tests/test_rendered_view.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_rendered_view PRIVATE src)
target_link_libraries(test_rendered_view PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_rendered_view COMMAND test_rendered_view)
add_executable(test_type_selector tests/test_type_selector.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
target_link_libraries(test_type_selector PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_type_selector COMMAND test_type_selector)
add_executable(test_type_visibility tests/test_type_visibility.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_type_visibility PRIVATE src third_party/fadec)
target_link_libraries(test_type_visibility PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_type_visibility PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_type_visibility COMMAND test_type_visibility)
add_executable(test_options_dialog tests/test_options_dialog.cpp
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_options_dialog PRIVATE src)
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
add_test(NAME test_options_dialog COMMAND test_options_dialog)
add_executable(test_source_provider tests/test_source_provider.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
src/resources.qrc)
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
target_link_libraries(test_source_provider PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_source_provider COMMAND test_source_provider)
add_executable(test_scanner_ui tests/test_scanner_ui.cpp
src/scanner.cpp src/scannerpanel.cpp src/addressparser.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_scanner_ui PRIVATE src)
target_link_libraries(test_scanner_ui PRIVATE
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
add_executable(test_mcp tests/test_mcp.cpp)
target_include_directories(test_mcp PRIVATE src)
target_link_libraries(test_mcp PRIVATE ${QT}::Core ${QT}::Network ${QT}::Test)
add_test(NAME test_mcp COMMAND test_mcp)
if(WIN32)
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
src/scanner.cpp)
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
target_link_libraries(test_windbg_provider PRIVATE
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
add_executable(test_kernel_provider tests/test_kernel_provider.cpp
plugins/KernelMemory/KernelMemoryPlugin.cpp
src/processpicker.cpp src/processpicker.ui
src/scanner.cpp)
target_include_directories(test_kernel_provider PRIVATE
src plugins/KernelMemory)
target_link_libraries(test_kernel_provider PRIVATE
${QT}::Widgets ${QT}::Concurrent ${QT}::Test
psapi shell32 advapi32 ${_QT_WINEXTRAS})
add_test(NAME test_kernel_provider COMMAND test_kernel_provider)
endif()
add_executable(bench_large_class tests/bench_large_class.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(bench_large_class PRIVATE src third_party/fadec)
target_link_libraries(bench_large_class PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(bench_large_class PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME bench_large_class COMMAND bench_large_class)
add_executable(bench_project tests/bench_project.cpp)
target_include_directories(bench_project PRIVATE src)
target_link_libraries(bench_project PRIVATE ${QT}::Widgets ${QT}::Test)
if(WIN32)
target_link_libraries(bench_project PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME bench_project COMMAND bench_project)
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
# that links the broadest set of Qt modules; all test exes share the same output dir)
if(TARGET ${QT}::windeployqt)
add_custom_target(deploy_tests ALL
COMMAND $<TARGET_FILE:${QT}::windeployqt>
--no-compiler-runtime --no-translations
--no-opengl-sw --no-system-d3d-compiler
$<TARGET_FILE:test_controller>
DEPENDS test_controller
COMMENT "Deploying Qt runtime DLLs for tests..."
)
endif()
endif() # BUILD_UI_TESTS
endif()
add_subdirectory(plugins/ProcessMemory)
if(NOT APPLE)
add_subdirectory(plugins/RemoteProcessMemory)
endif()
if(WIN32)
add_subdirectory(plugins/KernelMemory)
add_subdirectory(plugins/WinDbgMemory)
add_subdirectory(plugins/RcNetPluginCompatLayer)
endif()

195
README.md
View File

@@ -1,16 +1,197 @@
# ReclassX
<div align="center">
An improvement over other reclass like editors.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/RECLASS_LIGHTMODE.svg" height="170">
<img src="docs/RECLASS_DARKMODE.svg" alt="Reclass" height="170" />
</picture>
https://github.com/IChooseYou/ReclassX/raw/main/video.mp4
**A structured binary editor for reverse engineering — inspect raw bytes as typed structs, arrays, and pointers.<p>Built from scratch as a modern replacement for ReClass.NET and ReClassEx**
![screenshot](screenshot.png)
[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%20%7C%20macOS-blue)]()
</div>
Reclass helps you inspect raw bytes and interpret them as types (structs, arrays, primitives, pointers, padding) instead of just hex. It is a debugging tool for figuring out unknown data structures — either at runtime from a live process, or from a static source like a binary file or crash dump.
Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor surface is rendered as formatted plain text with inline editing, fold markers, and hex/ASCII previews.
## Screenshots
![Base address tooltip with expression cheat sheet](docs/README_PIC5.png)
![Data source picker with saved sources](docs/README_PIC4.png)
![Windows — VTable with value history popup](docs/README_PIC1.png)
![macOS — project tree with kernel struct inspection](docs/README_PIC2.png)
![Memory scanner](docs/README_PIC3.png)
![MacOS - Process Memory scanner](docs/README_PIC6.png)
## Features
### Editor
- **Structured binary view** — render raw bytes as typed fields with columnar alignment
- **Inline editing** — click to edit type names, field names, values, base addresses, array metadata, pointer targets, enum members, bitfield members, static expressions, and comments — all with real-time validation
- **Tab-cycling** — tab through editable fields within a line
- **Type autocomplete** — cached popup type picker with search/filter for struct targets
- **Multi-select** — Ctrl+click individual nodes or Shift+click for range selection
- **Split views** — multiple synchronized editor panes over the same document
- **Find bar** — Ctrl+F in-editor search with indicator highlighting
- **Fold/collapse** — expand and collapse structs, arrays, and pointer expansions with embedded fold indicators
- **Hex + ASCII columns** — raw byte previews alongside the structured view with per-byte change highlighting
### Live Memory Analysis
- **Auto-refresh** — configurable interval (default 660ms) with async page-based reads for non-blocking UI
- **Value history & heatmap** — per-node ring buffer (10 samples with timestamps), color-coded heat indicators (static/cold/warm/hot) based on change frequency
- **Changed-byte highlighting** — per-byte change indicators within hex preview lines
- **Memory write-back** — edit values inline, writes propagate through the provider to live process memory
- **Pointer chasing** — automatic reads of dereferenced memory regions across pointer chains
- **Address parser** — formula expressions like `<module.exe>+0x1A0`, pointer dereference chains, symbol resolution
### Undo / Redo
Full command stack with 15 undoable operations: ChangeKind, Rename, Collapse, Insert, Remove, ChangeBase, WriteBytes, ChangeArrayMeta, ChangePointerRef, ChangeStructTypeName, ChangeClassKeyword, ChangeOffset, ChangeEnumMembers, ChangeOffsetExpr, ToggleStatic. Batch macro support for multi-node operations.
### Import / Export
| Format | Import | Export |
|--------|:------:|:------:|
| **Native JSON (.rcx)** | Full tree + metadata | Full tree + metadata |
| **C/C++ source** | Struct/class/union/enum parsing with offset comments | Header generation with optional static asserts |
| **ReClass XML** | Full compatibility with ReClass Classic | Full compatibility |
| **PDB symbols (Windows)** | UDT enumeration with selective recursive import via raw_pdb — no DIA SDK dependency | |
### Workspace & Navigation
- **Multi-document tabs** — MDI interface, one document per tab
- **Workspace dock** — project explorer tree with struct/enum/union icons, sorted by field count, quick navigation to members
- **Scanner dock** — integrated memory search panel
- **Dual view mode** — switch between ReClass tree view and rendered C/C++ output per tab
- **View root** — focus on a specific struct, hiding all others
- **Scroll to node** — programmatic navigation to any node by ID
## Data Sources
- **File** — open any binary file and inspect its contents as structured data
- **Process** — attach to a live process and read its memory in real time (Windows/Linux)
- **Kernel driver** — Windows kernel driver (IOCTL) for process memory, physical memory, page table walking, and CR3/VTOP translation
- **Remote Process** — read another process's memory over TCP with cross-architecture 32/64-bit support
- **WinDbg** — connect to live WinDbg debugging sessions or load crash dumps
- **Saved sources** — quick-switch between recently used data sources per tab
## Plugin System
DLL plugins loaded from a `Plugins` folder, auto or manual.
**Bundled plugins:**
| Plugin | Description |
|--------|-------------|
| **Process memory** | Attach to local processes on Windows and Linux — PID-based, with symbol resolution and module/region enumeration |
| **Kernel memory** | Windows kernel driver (IOCTL) for reading/writing process and physical memory, CR3 queries, virtual-to-physical translation, and full 4-level page table walking — supports 4KB, 2MB, and 1GB pages |
| **WinDbg** | Access data from live WinDbg debugging sessions |
| **Remote process memory** | TCP RPC-based remote process access with cross-architecture support |
| **ReClass.NET compatibility** | Load existing ReClass.NET native DLL plugins directly; optional .NET CLR hosting for managed plugins |
## MCP Integration
Built-in [Model Context Protocol](https://modelcontextprotocol.io/) bridge via `ReclassMcpBridge` — the first reverse engineering tool with native AI/LLM integration. The server uses JSON-RPC 2.0 over named pipes and can be toggled from the Tools menu or auto-started on launch.
**Available tools:**
| Tool | Description |
|------|-------------|
| `projectState` | Read current tree structure, base address, tab state |
| `treeApply` | Apply structural command deltas to the node tree |
| `sourceSwitch` | Switch the active data source |
| `hexRead` | Read bytes at an address |
| `hexWrite` | Write bytes at an address |
| `statusSet` | Update the status bar text |
| `uiAction` | Trigger menu actions programmatically |
| `treeSearch` | Search nodes by name or type |
| `nodeHistory` | Query value change history for a node |
**Notifications:** `notifyTreeChanged`, `notifyDataChanged`
A standalone stdio-to-pipe bridge binary is built alongside the main application. To connect, add this to your MCP client config (e.g. `.mcp.json`):
```json
{
"mcpServers": {
"ReclassMcpBridge": {
"command": "path/to/build/ReclassMcpBridge",
"args": []
}
}
}
```
## Build
Requires Qt 6, QScintilla, and MinGW on Windows.
### Prerequisites
- **Qt 6** (or Qt 5) with MinGW — [Qt Online Installer](https://doc.qt.io/qt-6/qt-online-installation.html) (select MinGW kit + CMake/Ninja from the Tools section)
- **CMake 3.20+** — [cmake.org](https://cmake.org/download/) (bundled with Qt)
- **Ninja** — bundled with the Qt installer
### Quick Build
```bash
git clone --recurse-submodules https://github.com/IChooseYou/Reclass.git
cd Reclass
.\scripts\build_qscintilla.ps1
.\scripts\build.ps1
```
cmake -B build -G Ninja
cmake --build build
The build script auto-detects your Qt install location.
### macOS Build
```bash
./scripts/build_macos.sh --qt-dir /opt/homebrew/opt/qt --build-type Release --package
```
If you installed Qt via Homebrew, `--qt-dir /opt/homebrew/opt/qt` is typical on Apple Silicon. You can also set `QTDIR` or `Qt6_DIR` instead of passing `--qt-dir`.
Note: macOS Gatekeeper may block unsigned apps. If the app won't open, go to **System Settings > Privacy & Security** and click **Open Anyway**.
### Manual Build (MinGW)
1. Clone with `--recurse-submodules` (or run `git submodule update --init --recursive` after cloning)
2. Build QScintilla: `qmake` + `mingw32-make` in `third_party/qscintilla/src`
3. Configure and build:
```bash
cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/mingw_64
cmake --build build
```
4. Optionally run `windeployqt` on the output executable
### Visual Studio 2022+
The `msvc/` folder contains a ready-made solution (`Reclass.slnx`) with projects for the main application, all plugins, and third-party libraries. Requires the [Qt Visual Studio Tools](https://marketplace.visualstudio.com/items?itemName=TheQtCompany.QtVisualStudioTools2022) extension with a Qt 6 MSVC kit configured.
### Running Tests
```bash
ctest --test-dir build --output-on-failure
```
30 tests covering composition, serialization, undo/redo, import/export, provider switching, type visibility, validation, scanning, and rendering.
## Alternatives
- [ReClass.NET](https://github.com/ReClassNET/ReClass.NET)
- [ReClassEx](https://github.com/ajkhoury/ReClassEx)
<div align="center">
<sub>MIT License</sub>
</div>

View File

@@ -1,5 +1,6 @@
set(_QSCI_ROOT "${CMAKE_SOURCE_DIR}/third_party/qscintilla")
# Try to find a pre-built library first
find_path(QScintilla_INCLUDE_DIR
NAMES Qsci/qsciscintilla.h
PATHS "${_QSCI_ROOT}/src" "${_QSCI_ROOT}/include"
@@ -7,7 +8,10 @@ find_path(QScintilla_INCLUDE_DIR
)
find_library(QScintilla_LIBRARY
NAMES qscintilla2_qt6 libqscintilla2_qt6
NAMES
qscintilla2_qt${QT_VERSION_MAJOR} libqscintilla2_qt${QT_VERSION_MAJOR}
qscintilla2_qt6 libqscintilla2_qt6
qscintilla2_qt5 libqscintilla2_qt5
PATHS
"${_QSCI_ROOT}/src/release"
"${_QSCI_ROOT}/src"
@@ -15,13 +19,11 @@ find_library(QScintilla_LIBRARY
NO_DEFAULT_PATH
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(QScintilla DEFAULT_MSG
QScintilla_LIBRARY QScintilla_INCLUDE_DIR)
if(QScintilla_FOUND)
set(QScintilla_INCLUDE_DIRS ${QScintilla_INCLUDE_DIR})
set(QScintilla_LIBRARIES ${QScintilla_LIBRARY})
if(QScintilla_LIBRARY AND QScintilla_INCLUDE_DIR)
# Use pre-built library
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(QScintilla DEFAULT_MSG
QScintilla_LIBRARY QScintilla_INCLUDE_DIR)
if(NOT TARGET QScintilla::QScintilla)
add_library(QScintilla::QScintilla STATIC IMPORTED)
set_target_properties(QScintilla::QScintilla PROPERTIES
@@ -29,4 +31,118 @@ if(QScintilla_FOUND)
INTERFACE_INCLUDE_DIRECTORIES "${QScintilla_INCLUDE_DIR}"
)
endif()
elseif(EXISTS "${_QSCI_ROOT}/src/qsciscintilla.cpp")
# Build from source
message(STATUS "Building QScintilla from source")
file(GLOB _QSCI_LEXER_SOURCES "${_QSCI_ROOT}/scintilla/lexers/*.cpp")
file(GLOB _QSCI_LEXLIB_SOURCES "${_QSCI_ROOT}/scintilla/lexlib/*.cpp")
file(GLOB _QSCI_SCI_SOURCES "${_QSCI_ROOT}/scintilla/src/*.cpp")
file(GLOB _QSCI_HEADERS "${_QSCI_ROOT}/src/Qsci/*.h")
set(_QSCI_QT_SOURCES
"${_QSCI_ROOT}/src/qsciscintilla.cpp"
"${_QSCI_ROOT}/src/qsciscintillabase.cpp"
"${_QSCI_ROOT}/src/qsciabstractapis.cpp"
"${_QSCI_ROOT}/src/qsciapis.cpp"
"${_QSCI_ROOT}/src/qscicommand.cpp"
"${_QSCI_ROOT}/src/qscicommandset.cpp"
"${_QSCI_ROOT}/src/qscidocument.cpp"
"${_QSCI_ROOT}/src/qscilexer.cpp"
"${_QSCI_ROOT}/src/qscilexerasm.cpp"
"${_QSCI_ROOT}/src/qscilexeravs.cpp"
"${_QSCI_ROOT}/src/qscilexerbash.cpp"
"${_QSCI_ROOT}/src/qscilexerbatch.cpp"
"${_QSCI_ROOT}/src/qscilexercmake.cpp"
"${_QSCI_ROOT}/src/qscilexercoffeescript.cpp"
"${_QSCI_ROOT}/src/qscilexercpp.cpp"
"${_QSCI_ROOT}/src/qscilexercsharp.cpp"
"${_QSCI_ROOT}/src/qscilexercss.cpp"
"${_QSCI_ROOT}/src/qscilexercustom.cpp"
"${_QSCI_ROOT}/src/qscilexerd.cpp"
"${_QSCI_ROOT}/src/qscilexerdiff.cpp"
"${_QSCI_ROOT}/src/qscilexeredifact.cpp"
"${_QSCI_ROOT}/src/qscilexerfortran.cpp"
"${_QSCI_ROOT}/src/qscilexerfortran77.cpp"
"${_QSCI_ROOT}/src/qscilexerhex.cpp"
"${_QSCI_ROOT}/src/qscilexerhtml.cpp"
"${_QSCI_ROOT}/src/qscilexeridl.cpp"
"${_QSCI_ROOT}/src/qscilexerintelhex.cpp"
"${_QSCI_ROOT}/src/qscilexerjava.cpp"
"${_QSCI_ROOT}/src/qscilexerjavascript.cpp"
"${_QSCI_ROOT}/src/qscilexerjson.cpp"
"${_QSCI_ROOT}/src/qscilexerlua.cpp"
"${_QSCI_ROOT}/src/qscilexermakefile.cpp"
"${_QSCI_ROOT}/src/qscilexermarkdown.cpp"
"${_QSCI_ROOT}/src/qscilexermasm.cpp"
"${_QSCI_ROOT}/src/qscilexermatlab.cpp"
"${_QSCI_ROOT}/src/qscilexernasm.cpp"
"${_QSCI_ROOT}/src/qscilexeroctave.cpp"
"${_QSCI_ROOT}/src/qscilexerpascal.cpp"
"${_QSCI_ROOT}/src/qscilexerperl.cpp"
"${_QSCI_ROOT}/src/qscilexerpostscript.cpp"
"${_QSCI_ROOT}/src/qscilexerpo.cpp"
"${_QSCI_ROOT}/src/qscilexerpov.cpp"
"${_QSCI_ROOT}/src/qscilexerproperties.cpp"
"${_QSCI_ROOT}/src/qscilexerpython.cpp"
"${_QSCI_ROOT}/src/qscilexerruby.cpp"
"${_QSCI_ROOT}/src/qscilexerspice.cpp"
"${_QSCI_ROOT}/src/qscilexersql.cpp"
"${_QSCI_ROOT}/src/qscilexersrec.cpp"
"${_QSCI_ROOT}/src/qscilexertcl.cpp"
"${_QSCI_ROOT}/src/qscilexertekhex.cpp"
"${_QSCI_ROOT}/src/qscilexertex.cpp"
"${_QSCI_ROOT}/src/qscilexerverilog.cpp"
"${_QSCI_ROOT}/src/qscilexervhdl.cpp"
"${_QSCI_ROOT}/src/qscilexerxml.cpp"
"${_QSCI_ROOT}/src/qscilexeryaml.cpp"
"${_QSCI_ROOT}/src/qscimacro.cpp"
"${_QSCI_ROOT}/src/qsciprinter.cpp"
"${_QSCI_ROOT}/src/qscistyle.cpp"
"${_QSCI_ROOT}/src/qscistyledtext.cpp"
"${_QSCI_ROOT}/src/InputMethod.cpp"
"${_QSCI_ROOT}/src/ListBoxQt.cpp"
"${_QSCI_ROOT}/src/PlatQt.cpp"
"${_QSCI_ROOT}/src/SciAccessibility.cpp"
"${_QSCI_ROOT}/src/SciClasses.cpp"
"${_QSCI_ROOT}/src/ScintillaQt.cpp"
)
add_library(qscintilla2 STATIC
${_QSCI_QT_SOURCES}
${_QSCI_HEADERS}
${_QSCI_LEXER_SOURCES}
${_QSCI_LEXLIB_SOURCES}
${_QSCI_SCI_SOURCES}
)
target_include_directories(qscintilla2 PUBLIC
"${_QSCI_ROOT}/src"
)
target_include_directories(qscintilla2 PRIVATE
"${_QSCI_ROOT}/scintilla/include"
"${_QSCI_ROOT}/scintilla/lexlib"
"${_QSCI_ROOT}/scintilla/src"
)
target_compile_definitions(qscintilla2 PRIVATE
SCINTILLA_QT
SCI_LEXER
INCLUDE_DEPRECATED_FEATURES
)
target_link_libraries(qscintilla2 PUBLIC
${QT}::Widgets
${QT}::PrintSupport
)
set_target_properties(qscintilla2 PROPERTIES AUTOMOC ON)
add_library(QScintilla::QScintilla ALIAS qscintilla2)
set(QScintilla_FOUND TRUE)
else()
set(QScintilla_FOUND FALSE)
if(QScintilla_FIND_REQUIRED)
message(FATAL_ERROR "Could NOT find QScintilla (missing source and pre-built library)")
endif()
endif()

36
cmake/FindQt5.cmake Normal file
View File

@@ -0,0 +1,36 @@
# Documentation: https://cmake.org/cmake/help/latest/manual/cmake-developer.7.html#find-modules
# Always try config mode for the requested components (handles repeated calls)
find_package(Qt5 COMPONENTS ${Qt5_FIND_COMPONENTS} QUIET CONFIG)
if(Qt5_FOUND)
if(NOT Qt5_FIND_QUIETLY)
message(STATUS "Qt5 found: ${Qt5_DIR}")
endif()
return()
endif()
if(Qt5_FIND_REQUIRED AND WIN32)
message(STATUS "Downloading Qt5...")
# Fix warnings about DOWNLOAD_EXTRACT_TIMESTAMP
if(POLICY CMP0135)
cmake_policy(SET CMP0135 NEW)
endif()
include(FetchContent)
set(FETCHCONTENT_QUIET OFF)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
FetchContent_Declare(Qt5
URL "https://github.com/x64dbg/deps/releases/download/2025.07.02/qt5.12.12-msvc2017_64.7z"
URL_HASH SHA256=770490bf09514982c8192ebde9a1fac8821108ba42b021f167bac54e85ada48a
)
else()
FetchContent_Declare(Qt5
URL "https://github.com/x64dbg/deps/releases/download/2025.07.02/qt5.12.12-msvc2017.7z"
URL_HASH SHA256=3ff2a58e5ed772be475643cd7bb2df3e5499d7169d794ddf1ed5df5c5e862cb6
)
endif()
FetchContent_MakeAvailable(Qt5)
unset(FETCHCONTENT_QUIET)
set(Qt5_ROOT ${qt5_SOURCE_DIR})
find_package(Qt5 COMPONENTS ${Qt5_FIND_COMPONENTS} CONFIG REQUIRED)
endif()

91
cmake/deploy.cmake Normal file
View File

@@ -0,0 +1,91 @@
# cmake/deploy.cmake - Dual-mode script for deploying Qt runtime DLLs
#
# Script mode: cmake -P deploy.cmake <target_exe> <windeployqt>
# Include mode: include(deploy) from CMakeLists.txt (creates "deploy" target + post-build)
if(CMAKE_SCRIPT_MODE_FILE)
set(TARGET_EXE ${CMAKE_ARGV3})
set(WINDEPLOYQT ${CMAKE_ARGV4})
get_filename_component(TARGET_DIR ${TARGET_EXE} DIRECTORY)
# Skip if already deployed for this build
if(EXISTS "${TARGET_DIR}/.qt_deployed")
return()
endif()
message(STATUS "Running windeployqt on ${TARGET_EXE}")
execute_process(
COMMAND ${WINDEPLOYQT}
--no-compiler-runtime
--no-translations
--no-opengl-sw
--no-system-d3d-compiler
--force
${TARGET_EXE}
RESULT_VARIABLE _result
)
if(_result EQUAL 0)
file(WRITE "${TARGET_DIR}/.qt_deployed" "")
message(STATUS "windeployqt completed successfully")
else()
message(WARNING "windeployqt failed with exit code ${_result}")
endif()
return()
endif()
# ── Include mode: configure the deploy target ──
if(NOT WIN32)
return()
endif()
# Discover windeployqt from qmake
if(NOT TARGET ${QT}::windeployqt AND TARGET ${QT}::qmake)
get_target_property(_qt_qmake_location ${QT}::qmake IMPORTED_LOCATION)
execute_process(
COMMAND "${_qt_qmake_location}" -query QT_INSTALL_PREFIX
RESULT_VARIABLE _return_code
OUTPUT_VARIABLE _qt_install_prefix
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(_windeployqt "${_qt_install_prefix}/bin/windeployqt.exe")
if(EXISTS ${_windeployqt})
add_executable(${QT}::windeployqt IMPORTED)
set_target_properties(${QT}::windeployqt PROPERTIES
IMPORTED_LOCATION ${_windeployqt}
)
message(STATUS "Found windeployqt: ${_windeployqt}")
else()
message(WARNING "windeployqt not found at ${_windeployqt}")
endif()
endif()
if(TARGET ${QT}::windeployqt)
# Standalone "deploy" target (can still be invoked manually)
add_custom_target(deploy
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
$<TARGET_FILE:Reclass>
$<TARGET_FILE:${QT}::windeployqt>
DEPENDS Reclass
COMMENT "Deploying Qt runtime DLLs..."
)
# Force re-deploy on rebuild
set_target_properties(deploy PROPERTIES
ADDITIONAL_CLEAN_FILES $<TARGET_FILE_DIR:Reclass>/.qt_deployed
)
# Auto-deploy as post-build step so the correct Qt DLLs are always next
# to the exe. Without this, MSVC builds load whatever Qt DLLs happen to
# be in PATH (often MinGW ones), causing instant ABI-mismatch crashes.
add_custom_command(TARGET Reclass POST_BUILD
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/deploy.cmake
$<TARGET_FILE:Reclass>
$<TARGET_FILE:${QT}::windeployqt>
COMMENT "Auto-deploying Qt runtime DLLs...")
endif()

29
cmake/raw_pdb_prefix.h Normal file
View File

@@ -0,0 +1,29 @@
// Force-included before every raw_pdb translation unit (and consumers).
// PDB_CRT.h forward-declares printf/memcmp/etc with extern "C" __cdecl,
// which conflicts with MinGW's CRT headers (C++ linkage, no __cdecl).
//
// Fix: include the real CRT headers, then include PDB_CRT.h with function
// names macro-renamed to harmless dummies. This triggers #pragma once so
// no raw_pdb source file ever processes PDB_CRT.h's conflicting declarations.
//
// Guarded with __cplusplus because PUBLIC propagation applies this to C
// sources (fadec) where PDB_CRT.h is irrelevant and <cstdio> doesn't exist.
#ifdef __cplusplus
#include <cstdio>
#include <cstring>
#undef __cdecl
#define __cdecl
#define printf _pdb_crt_unused_printf
#define memcmp _pdb_crt_unused_memcmp
#define memcpy _pdb_crt_unused_memcpy
#define strlen _pdb_crt_unused_strlen
#define strcmp _pdb_crt_unused_strcmp
#include "Foundation/PDB_CRT.h"
#undef printf
#undef memcmp
#undef memcpy
#undef strlen
#undef strcmp
#endif

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

BIN
docs/README_PIC2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

BIN
docs/README_PIC3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/README_PIC4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/README_PIC5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
docs/README_PIC6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

160
docs/RECLASS_DARKMODE.svg Normal file
View File

@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 186.01 52.79">
<defs>
<style>
.cls-1 {
fill: url(#Unbenannter_Verlauf_130-2);
}
.cls-2 {
fill: url(#Unbenannter_Verlauf_236-2);
}
.cls-3 {
fill: url(#Unbenannter_Verlauf_225-2);
}
.cls-4 {
fill: #1f2939;
}
.cls-5 {
fill: #5d9bd4;
}
.cls-6 {
fill: #1e3e88;
}
.cls-7 {
fill: #6e809a;
}
.cls-8 {
fill: url(#Unbenannter_Verlauf_225);
}
.cls-9 {
fill: url(#Unbenannter_Verlauf_236);
}
.cls-10 {
fill: url(#Unbenannter_Verlauf_130);
}
.cls-11 {
fill: url(#Unbenannter_Verlauf_170);
}
.cls-12 {
fill: url(#Unbenannter_Verlauf_161);
}
.cls-13 {
fill: url(#Unbenannter_Verlauf_183);
}
.cls-14 {
fill: #b06ba9;
}
.cls-15 {
fill: #826415;
}
.cls-16 {
fill: #e2aa11;
}
.cls-17 {
fill: #893089;
}
</style>
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.33" y1="8.33" x2="18.11" y2="18.11" gradientTransform="translate(13.22 -5.47) rotate(45)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f3db78"/>
<stop offset=".19" stop-color="#f4e188"/>
<stop offset=".34" stop-color="#f4e38d"/>
<stop offset=".38" stop-color="#f4df81"/>
<stop offset=".47" stop-color="#f5d86f"/>
<stop offset=".57" stop-color="#f5d463"/>
<stop offset=".67" stop-color="#f6d360"/>
<stop offset=".89" stop-color="#f1cc53"/>
<stop offset="1" stop-color="#efbe33"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.46" x2="10.98" y2="26.03" gradientTransform="translate(-4.95 39.45) rotate(-135)" gradientUnits="userSpaceOnUse">
<stop offset=".18" stop-color="#e2aa11"/>
<stop offset=".91" stop-color="#826415"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.46" y1=".41" x2="26.03" y2="10.98" gradientTransform="translate(31.39 24.39) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.97" y1="15.65" x2="42.34" y2="23.02" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#deb0d3"/>
<stop offset=".15" stop-color="#e1b5d6"/>
<stop offset=".3" stop-color="#e3b8d7"/>
<stop offset=".4" stop-color="#d7a8cd"/>
<stop offset=".53" stop-color="#cf9cc7"/>
<stop offset=".67" stop-color="#cd99c5"/>
<stop offset=".89" stop-color="#c68abc"/>
<stop offset="1" stop-color="#bb7db4"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.78" y1="20.14" x2="36.87" y2="28.24" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
<stop offset=".19" stop-color="#b06ba9"/>
<stop offset=".87" stop-color="#893089"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.45" y1="9.43" x2="47.55" y2="17.53" xlink:href="#Unbenannter_Verlauf_225"/>
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.88" y1="39.45" x2="42.29" y2="46.86" gradientTransform="translate(41.82 -14.64) rotate(45)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#91c4eb"/>
<stop offset=".2" stop-color="#9dc9ed"/>
<stop offset=".33" stop-color="#96c6ec"/>
<stop offset=".35" stop-color="#91c3ea"/>
<stop offset=".45" stop-color="#7fb8e5"/>
<stop offset=".56" stop-color="#73b2e2"/>
<stop offset=".67" stop-color="#70b0e1"/>
<stop offset=".89" stop-color="#60a7dc"/>
<stop offset="1" stop-color="#4d9bd5"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.83" y1="43.9" x2="36.92" y2="51.99" gradientTransform="translate(22.68 105.31) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
<stop offset=".19" stop-color="#5d9bd4"/>
<stop offset=".87" stop-color="#1e3e88"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.51" y1="33.23" x2="47.59" y2="41.32" gradientTransform="translate(48.45 94.63) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
</defs>
<g>
<rect class="cls-7" x="22.48" y="16.85" width="1.76" height="25.1"/>
<rect class="cls-7" x="17.08" y="16.85" width="19.7" height="1.82"/>
<rect class="cls-7" x="22.48" y="40.19" width="11.9" height="1.76"/>
<g>
<rect class="cls-12" x="2.56" y="6.31" width="21.31" height="13.82" transform="translate(-5.48 13.22) rotate(-45)"/>
<rect class="cls-15" x="17.52" y="6.88" width="1.15" height="22.44" transform="translate(18.1 -7.49) rotate(45)"/>
<g>
<rect class="cls-16" x="7.76" y="-2.88" width="1.15" height="22.44" transform="translate(8.34 -3.45) rotate(45)"/>
<rect class="cls-10" x="5.12" y="13.27" width="1.15" height="14.95" transform="translate(24.39 31.39) rotate(135)"/>
<rect class="cls-1" x="20.17" y="-1.78" width="1.15" height="14.95" transform="translate(39.45 -4.95) rotate(135)"/>
</g>
</g>
<g>
<polygon class="cls-11" points="40.33 10.29 29.64 21.02 36.98 28.38 47.67 17.66 40.33 10.29"/>
<polygon class="cls-17" points="37.01 29.1 36.29 28.38 47.68 16.96 48.39 17.68 37.01 29.1"/>
<polygon class="cls-14" points="29.67 21.74 28.95 21.02 40.34 9.6 41.05 10.31 29.67 21.74"/>
<polygon class="cls-8" points="28.95 21.02 29.67 20.3 37.72 28.38 37 29.1 28.95 21.02"/>
<polygon class="cls-3" points="39.63 10.31 40.34 9.6 48.39 17.67 47.68 18.39 39.63 10.31"/>
</g>
<g>
<rect class="cls-13" x="30.96" y="37.92" width="15.26" height="10.48" transform="translate(-19.21 39.92) rotate(-45)"/>
<g>
<rect class="cls-9" x="32.83" y="42.71" width="1.01" height="11.38" transform="translate(91.13 59.06) rotate(135)"/>
<rect class="cls-2" x="43.5" y="32.04" width="1.01" height="11.38" transform="translate(101.81 33.29) rotate(135)"/>
<rect class="cls-6" x="41.84" y="38.69" width="1.01" height="16.1" transform="translate(45.45 -16.25) rotate(45)"/>
<rect class="cls-5" x="34.5" y="31.35" width="1.01" height="16.1" transform="translate(38.11 -13.21) rotate(45)"/>
</g>
</g>
</g>
<g>
<path class="cls-4" d="M53.66,30.51v11.46h-2.57v-25.13h9.36c5.04,0,7.72,2.72,7.72,6.65,0,3.22-1.89,5.26-4.51,5.89,2.34.58,4.09,2.2,4.09,6.5v1.02c0,1.74-.11,4.07.33,5.07h-2.54c-.46-1.08-.39-3.1-.39-5.34v-.6c0-3.87-1.12-5.52-5.79-5.52h-5.7ZM53.66,28.26h5.79c4.15,0,6.03-1.56,6.03-4.64,0-2.9-1.89-4.54-5.57-4.54h-6.25v9.18Z"/>
<path class="cls-4" d="M86.55,29.87h-12.65v9.79h13.88l-.35,2.3h-16.06v-25.12h15.81v2.27h-13.28v8.49h12.65v2.27Z"/>
<path class="cls-4" d="M109.12,35.04c-1.15,4.11-4.2,7.19-9.68,7.19-7.34,0-11.13-5.72-11.13-12.79s3.76-12.96,11.21-12.96c5.64,0,8.84,3.18,9.63,7.37h-2.56c-1.04-3.02-3.01-5.13-7.18-5.13-5.92,0-8.38,5.4-8.38,10.66s2.39,10.62,8.52,10.62c3.99,0,5.89-2.16,7.01-4.95h2.57Z"/>
<path class="cls-4" d="M111.62,16.84h2.56v22.82h13.3l-.41,2.27h-15.46v-25.09Z"/>
<path class="cls-4" d="M133.03,33.77l-2.97,8.16h-2.58l9.09-25.09h3.11l9.48,25.09h-2.76l-3.05-8.16h-10.32ZM142.61,31.5c-2.61-7.07-3.99-10.62-4.51-12.4h-.04c-.61,2-2.16,6.36-4.27,12.4h8.82Z"/>
<path class="cls-4" d="M151.68,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.24,4.56,8.24,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
<path class="cls-4" d="M170.59,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.25,4.56,8.25,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

160
docs/RECLASS_LIGHTMODE.svg Normal file
View File

@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.55 52.66">
<defs>
<style>
.cls-1 {
fill: url(#Unbenannter_Verlauf_130-2);
}
.cls-2 {
fill: url(#Unbenannter_Verlauf_236-2);
}
.cls-3 {
fill: url(#Unbenannter_Verlauf_225-2);
}
.cls-4 {
fill: #5d9bd4;
}
.cls-5 {
fill: #e3e8f0;
}
.cls-6 {
fill: #1e3e88;
}
.cls-7 {
fill: #6e809a;
}
.cls-8 {
fill: url(#Unbenannter_Verlauf_225);
}
.cls-9 {
fill: url(#Unbenannter_Verlauf_236);
}
.cls-10 {
fill: url(#Unbenannter_Verlauf_130);
}
.cls-11 {
fill: url(#Unbenannter_Verlauf_170);
}
.cls-12 {
fill: url(#Unbenannter_Verlauf_161);
}
.cls-13 {
fill: url(#Unbenannter_Verlauf_183);
}
.cls-14 {
fill: #b06ba9;
}
.cls-15 {
fill: #826415;
}
.cls-16 {
fill: #e2aa11;
}
.cls-17 {
fill: #893089;
}
</style>
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.31" y1="8.31" x2="18.06" y2="18.06" gradientTransform="translate(13.19 -5.46) rotate(45)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f3db78"/>
<stop offset=".19" stop-color="#f4e188"/>
<stop offset=".34" stop-color="#f4e38d"/>
<stop offset=".38" stop-color="#f4df81"/>
<stop offset=".47" stop-color="#f5d86f"/>
<stop offset=".57" stop-color="#f5d463"/>
<stop offset=".67" stop-color="#f6d360"/>
<stop offset=".89" stop-color="#f1cc53"/>
<stop offset="1" stop-color="#efbe33"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.42" x2="10.95" y2="25.97" gradientTransform="translate(-4.94 39.35) rotate(-135)" gradientUnits="userSpaceOnUse">
<stop offset=".18" stop-color="#e2aa11"/>
<stop offset=".91" stop-color="#826415"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.42" y1=".41" x2="25.97" y2="10.95" gradientTransform="translate(31.32 24.33) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.88" y1="15.61" x2="42.24" y2="22.97" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#deb0d3"/>
<stop offset=".15" stop-color="#e1b5d6"/>
<stop offset=".3" stop-color="#e3b8d7"/>
<stop offset=".4" stop-color="#d7a8cd"/>
<stop offset=".53" stop-color="#cf9cc7"/>
<stop offset=".67" stop-color="#cd99c5"/>
<stop offset=".89" stop-color="#c68abc"/>
<stop offset="1" stop-color="#bb7db4"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.7" y1="20.09" x2="36.78" y2="28.17" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
<stop offset=".19" stop-color="#b06ba9"/>
<stop offset=".87" stop-color="#893089"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.35" y1="9.41" x2="47.43" y2="17.49" xlink:href="#Unbenannter_Verlauf_225"/>
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.79" y1="39.35" x2="42.18" y2="46.74" gradientTransform="translate(41.71 -14.61) rotate(45)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#91c4eb"/>
<stop offset=".2" stop-color="#9dc9ed"/>
<stop offset=".33" stop-color="#96c6ec"/>
<stop offset=".35" stop-color="#91c3ea"/>
<stop offset=".45" stop-color="#7fb8e5"/>
<stop offset=".56" stop-color="#73b2e2"/>
<stop offset=".67" stop-color="#70b0e1"/>
<stop offset=".89" stop-color="#60a7dc"/>
<stop offset="1" stop-color="#4d9bd5"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.76" y1="43.79" x2="36.83" y2="51.86" gradientTransform="translate(22.62 105.05) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
<stop offset=".19" stop-color="#5d9bd4"/>
<stop offset=".87" stop-color="#1e3e88"/>
</linearGradient>
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.41" y1="33.15" x2="47.47" y2="41.21" gradientTransform="translate(48.33 94.4) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
</defs>
<g>
<rect class="cls-7" x="22.43" y="16.81" width="1.75" height="25.04"/>
<rect class="cls-7" x="17.04" y="16.81" width="19.66" height="1.82"/>
<rect class="cls-7" x="22.43" y="40.09" width="11.87" height="1.75"/>
<g>
<rect class="cls-12" x="2.56" y="6.29" width="21.26" height="13.79" transform="translate(-5.46 13.19) rotate(-45)"/>
<rect class="cls-15" x="17.48" y="6.87" width="1.15" height="22.38" transform="translate(18.06 -7.47) rotate(45)"/>
<g>
<rect class="cls-16" x="7.74" y="-2.87" width="1.15" height="22.38" transform="translate(8.32 -3.45) rotate(45)"/>
<rect class="cls-10" x="5.1" y="13.24" width="1.15" height="14.92" transform="translate(24.33 31.32) rotate(135)"/>
<rect class="cls-1" x="20.12" y="-1.78" width="1.15" height="14.92" transform="translate(39.35 -4.94) rotate(135)"/>
</g>
</g>
<g>
<polygon class="cls-11" points="40.23 10.26 29.56 20.96 36.89 28.31 47.56 17.61 40.23 10.26"/>
<polygon class="cls-17" points="36.91 29.03 36.2 28.31 47.56 16.92 48.27 17.63 36.91 29.03"/>
<polygon class="cls-14" points="29.59 21.68 28.88 20.97 40.24 9.57 40.95 10.29 29.59 21.68"/>
<polygon class="cls-8" points="28.88 20.97 29.59 20.25 37.62 28.31 36.91 29.02 28.88 20.97"/>
<polygon class="cls-3" points="39.53 10.29 40.24 9.57 48.27 17.63 47.56 18.34 39.53 10.29"/>
</g>
<g>
<rect class="cls-13" x="30.88" y="37.82" width="15.22" height="10.45" transform="translate(-19.17 39.82) rotate(-45)"/>
<g>
<rect class="cls-9" x="32.75" y="42.61" width="1.01" height="11.36" transform="translate(90.91 58.91) rotate(135)"/>
<rect class="cls-2" x="43.4" y="31.96" width="1.01" height="11.36" transform="translate(101.56 33.21) rotate(135)"/>
<rect class="cls-6" x="41.73" y="38.59" width="1.01" height="16.06" transform="translate(45.34 -16.21) rotate(45)"/>
<rect class="cls-4" x="34.41" y="31.27" width="1.01" height="16.06" transform="translate(38.02 -13.18) rotate(45)"/>
</g>
</g>
</g>
<g>
<path class="cls-5" d="M53.53,30.44v11.43h-2.56v-25.07h9.34c5.03,0,7.7,2.71,7.7,6.63,0,3.21-1.88,5.24-4.5,5.87,2.33.58,4.08,2.19,4.08,6.49v1.01c0,1.74-.11,4.06.33,5.06h-2.54c-.46-1.08-.39-3.09-.39-5.33v-.59c0-3.87-1.12-5.51-5.78-5.51h-5.68ZM53.53,28.19h5.77c4.14,0,6.02-1.55,6.02-4.63,0-2.89-1.88-4.53-5.55-4.53h-6.24v9.16Z"/>
<path class="cls-5" d="M86.34,29.8h-12.62v9.77h13.84l-.35,2.3h-16.02v-25.06h15.77v2.26h-13.25v8.47h12.62v2.26Z"/>
<path class="cls-5" d="M108.85,34.96c-1.15,4.09-4.19,7.17-9.65,7.17-7.32,0-11.11-5.7-11.11-12.76s3.75-12.93,11.18-12.93c5.63,0,8.82,3.17,9.6,7.35h-2.56c-1.03-3.02-3-5.12-7.16-5.12-5.91,0-8.36,5.39-8.36,10.63s2.38,10.6,8.5,10.6c3.98,0,5.88-2.16,6.99-4.94h2.56Z"/>
<path class="cls-5" d="M111.34,16.8h2.56v22.77h13.27l-.4,2.26h-15.42v-25.03Z"/>
<path class="cls-5" d="M132.69,33.69l-2.96,8.14h-2.57l9.07-25.03h3.1l9.45,25.03h-2.75l-3.04-8.14h-10.3ZM142.25,31.43c-2.61-7.05-3.98-10.59-4.5-12.37h-.04c-.61,2-2.15,6.34-4.26,12.37h8.8Z"/>
<path class="cls-5" d="M151.31,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
<path class="cls-5" d="M170.17,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{8792F51B-4951-4BAD-B130-2F0EFDEFF64B}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<RootNamespace>ProcessMemoryPlugin</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<QtMoc Include="..\src\processpicker.h" />
</ItemGroup>
<ItemGroup>
<QtUic Include="..\src\processpicker.ui" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\plugins\ProcessMemory\ProcessMemoryPlugin.h" />
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\providers\provider.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\plugins\ProcessMemory\ProcessMemoryPlugin.cpp" />
<ClCompile Include="..\src\processpicker.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{6B775E9C-9CB6-45FD-86A0-BE948A778969}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<RootNamespace>RcNetCompatPlugin</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<PreprocessorDefinitions>HAS_CLR_BRIDGE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>HAS_CLR_BRIDGE=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<QtMoc Include="..\src\processpicker.h" />
</ItemGroup>
<ItemGroup>
<QtUic Include="..\src\processpicker.ui" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatPlugin.h" />
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatProvider.h" />
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\ReClassNET_Plugin.hpp" />
<ClInclude Include="..\plugins\RcNetPluginCompatLayer\ClrHost.h" />
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\providers\provider.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatPlugin.cpp" />
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\RcNetCompatProvider.cpp" />
<ClCompile Include="..\plugins\RcNetPluginCompatLayer\ClrHost.cpp" />
<ClCompile Include="..\src\processpicker.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

17
msvc/Reclass.slnx Normal file
View File

@@ -0,0 +1,17 @@
<Solution>
<Configurations>
<Platform Name="x64" />
</Configurations>
<Folder Name="/plugins/">
<Project Path="ProcessMemoryPlugin.vcxproj" Id="8792f51b-4951-4bad-b130-2f0efdeff64b" />
<Project Path="WinDbgMemoryPlugin.vcxproj" Id="e25d358e-20f0-448b-bb2f-55e9d1f8e7ca" />
<Project Path="RemoteProcessMemoryPlugin.vcxproj" Id="39e2ddf6-cb76-4063-b957-66ecf1252010" />
<Project Path="RcNetCompatPlugin.vcxproj" Id="6b775e9c-9cb6-45fd-86a0-be948a778969" />
</Folder>
<Folder Name="/third_party/">
<Project Path="../third_party/raw_pdb/build/RawPDB.vcxproj" Id="fbe3dbfa-20a7-4f99-9326-ed82c8b7b910" />
<Project Path="fadec.vcxproj" Id="6a30a4f0-1a8d-4c6e-82d4-0a0d9693aa40" />
<Project Path="qscintilla.vcxproj" Id="f7124b57-7682-4702-b725-4d844dc41ada" />
</Folder>
<Project Path="Reclass.vcxproj" Id="c369f1fe-37c2-4c66-ac6d-ecb2b2b4ad5e" />
</Solution>

211
msvc/Reclass.vcxproj Normal file
View File

@@ -0,0 +1,211 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="18.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{C369F1FE-37C2-4C66-AC6D-ECB2B2B4AD5E}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">10.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets;concurrent;network;svg</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets;concurrent;network;svg</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<PostBuildEvent>
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe
xcopy /Y /I "$(SolutionDir)..\src\examples\*.rcx" "$(SolutionDir)$(Platform)\$(Configuration)\examples\"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Link>
<AdditionalDependencies>dwmapi.lib;dbghelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<ClCompile>
<AdditionalIncludeDirectories>..\third_party\fadec\;..\third_party\raw_pdb\src\;..\third_party\qscintilla\src\;..\src\</AdditionalIncludeDirectories>
<PreprocessorDefinitions>NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<PostBuildEvent>
<Command>$(QtToolsPath)/windeployqt $(SolutionDir)$(Platform)\$(Configuration)\$(ProjectName).exe
xcopy /Y /I "$(SolutionDir)..\src\examples\*.rcx" "$(SolutionDir)$(Platform)\$(Configuration)\examples\"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ClCompile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ClCompile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<QtRcc Include="..\src\resources.qrc" />
</ItemGroup>
<ItemGroup>
<QtUic Include="..\src\processpicker.ui" />
</ItemGroup>
<ItemGroup>
<QtMoc Include="..\src\controller.h" />
<QtMoc Include="..\src\editor.h" />
<QtMoc Include="..\src\mainwindow.h" />
<QtMoc Include="..\src\optionsdialog.h" />
<QtMoc Include="..\src\processpicker.h" />
<QtMoc Include="..\src\scanner.h" />
<QtMoc Include="..\src\scannerpanel.h" />
<QtMoc Include="..\src\titlebar.h" />
<QtMoc Include="..\src\typeselectorpopup.h" />
<QtMoc Include="..\src\imports\import_pdb_dialog.h" />
<QtMoc Include="..\src\mcp\mcp_bridge.h" />
<QtMoc Include="..\src\themes\themeeditor.h" />
<QtMoc Include="..\src\themes\thememanager.h" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\src\addressparser.h" />
<ClInclude Include="..\src\core.h" />
<ClInclude Include="..\src\disasm.h" />
<QtMoc Include="..\src\dock_tab_buttons.h" />
<ClInclude Include="..\src\generator.h" />
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\pluginmanager.h" />
<ClInclude Include="..\src\providerregistry.h" />
<QtMoc Include="..\src\startpage.h" />
<ClInclude Include="..\src\workspace_model.h" />
<ClInclude Include="..\src\imports\export_reclass_xml.h" />
<ClInclude Include="..\src\imports\import_pdb.h" />
<ClInclude Include="..\src\imports\import_reclass_xml.h" />
<ClInclude Include="..\src\imports\import_source.h" />
<ClInclude Include="..\src\providers\buffer_provider.h" />
<ClInclude Include="..\src\providers\null_provider.h" />
<ClInclude Include="..\src\providers\provider.h" />
<ClInclude Include="..\src\providers\snapshot_provider.h" />
<ClInclude Include="..\src\themes\theme.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\src\addressparser.cpp" />
<ClCompile Include="..\src\compose.cpp" />
<ClCompile Include="..\src\controller.cpp" />
<ClCompile Include="..\src\disasm.cpp" />
<ClCompile Include="..\src\editor.cpp" />
<ClCompile Include="..\src\format.cpp" />
<ClCompile Include="..\src\generator.cpp" />
<ClCompile Include="..\src\main.cpp">
<DynamicSource Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">input</DynamicSource>
<QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(Filename).moc</QtMocFileName>
<DynamicSource Condition="'$(Configuration)|$(Platform)'=='Release|x64'">input</DynamicSource>
<QtMocFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(Filename).moc</QtMocFileName>
</ClCompile>
<ClCompile Include="..\src\optionsdialog.cpp" />
<ClCompile Include="..\src\pluginmanager.cpp" />
<ClCompile Include="..\src\processpicker.cpp" />
<ClCompile Include="..\src\providerregistry.cpp" />
<ClCompile Include="..\src\scanner.cpp" />
<ClCompile Include="..\src\scannerpanel.cpp" />
<ClCompile Include="..\src\titlebar.cpp" />
<ClCompile Include="..\src\typeselectorpopup.cpp" />
<ClCompile Include="..\src\imports\export_reclass_xml.cpp" />
<ClCompile Include="..\src\imports\import_pdb.cpp" />
<ClCompile Include="..\src\imports\import_pdb_dialog.cpp" />
<ClCompile Include="..\src\imports\import_reclass_xml.cpp" />
<ClCompile Include="..\src\imports\import_source.cpp" />
<ClCompile Include="..\src\mcp\mcp_bridge.cpp" />
<ClCompile Include="..\src\themes\theme.cpp" />
<ClCompile Include="..\src\themes\themeeditor.cpp" />
<ClCompile Include="..\src\themes\thememanager.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="fadec.vcxproj">
<Project>{6A30A4F0-1A8D-4C6E-82D4-0A0D9693AA40}</Project>
</ProjectReference>
<ProjectReference Include="qscintilla.vcxproj">
<Project>{F7124B57-7682-4702-B725-4D844DC41ADA}</Project>
</ProjectReference>
<ProjectReference Include="..\third_party\raw_pdb\build\RawPDB.vcxproj">
<Project>{fbe3dbfa-20a7-4f99-9326-ed82c8b7b910}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,229 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Source Files\imports">
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000001}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\mcp">
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000002}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\themes">
<UniqueIdentifier>{A1B2C3D4-0001-0001-0001-000000000003}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Header Files\imports">
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000001}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\mcp">
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000002}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\providers">
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000003}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\themes">
<UniqueIdentifier>{A1B2C3D4-0002-0001-0001-000000000004}</UniqueIdentifier>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Form Files">
<UniqueIdentifier>{99349809-55BA-4b9d-BF79-8FDBB0286EB3}</UniqueIdentifier>
<Extensions>ui</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<QtRcc Include="..\src\resources.qrc">
<Filter>Resource Files</Filter>
</QtRcc>
</ItemGroup>
<ItemGroup>
<QtUic Include="..\src\processpicker.ui">
<Filter>Form Files</Filter>
</QtUic>
</ItemGroup>
<ItemGroup>
<QtMoc Include="..\src\controller.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\editor.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\mainwindow.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\optionsdialog.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\processpicker.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\scanner.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\scannerpanel.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\titlebar.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\typeselectorpopup.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\imports\import_pdb_dialog.h">
<Filter>Header Files\imports</Filter>
</QtMoc>
<QtMoc Include="..\src\mcp\mcp_bridge.h">
<Filter>Header Files\mcp</Filter>
</QtMoc>
<QtMoc Include="..\src\themes\themeeditor.h">
<Filter>Header Files\themes</Filter>
</QtMoc>
<QtMoc Include="..\src\themes\thememanager.h">
<Filter>Header Files\themes</Filter>
</QtMoc>
<QtMoc Include="..\src\dock_tab_buttons.h">
<Filter>Header Files</Filter>
</QtMoc>
<QtMoc Include="..\src\startpage.h">
<Filter>Header Files</Filter>
</QtMoc>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\src\addressparser.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\core.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\disasm.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\generator.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\iplugin.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\pluginmanager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\providerregistry.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\workspace_model.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\src\imports\export_reclass_xml.h">
<Filter>Header Files\imports</Filter>
</ClInclude>
<ClInclude Include="..\src\imports\import_pdb.h">
<Filter>Header Files\imports</Filter>
</ClInclude>
<ClInclude Include="..\src\imports\import_reclass_xml.h">
<Filter>Header Files\imports</Filter>
</ClInclude>
<ClInclude Include="..\src\imports\import_source.h">
<Filter>Header Files\imports</Filter>
</ClInclude>
<ClInclude Include="..\src\providers\buffer_provider.h">
<Filter>Header Files\providers</Filter>
</ClInclude>
<ClInclude Include="..\src\providers\null_provider.h">
<Filter>Header Files\providers</Filter>
</ClInclude>
<ClInclude Include="..\src\providers\provider.h">
<Filter>Header Files\providers</Filter>
</ClInclude>
<ClInclude Include="..\src\providers\snapshot_provider.h">
<Filter>Header Files\providers</Filter>
</ClInclude>
<ClInclude Include="..\src\themes\theme.h">
<Filter>Header Files\themes</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\src\addressparser.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\compose.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\controller.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\disasm.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\editor.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\format.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\generator.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\optionsdialog.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\pluginmanager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\processpicker.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\providerregistry.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\scanner.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\scannerpanel.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\titlebar.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\typeselectorpopup.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\src\imports\export_reclass_xml.cpp">
<Filter>Source Files\imports</Filter>
</ClCompile>
<ClCompile Include="..\src\imports\import_pdb.cpp">
<Filter>Source Files\imports</Filter>
</ClCompile>
<ClCompile Include="..\src\imports\import_pdb_dialog.cpp">
<Filter>Source Files\imports</Filter>
</ClCompile>
<ClCompile Include="..\src\imports\import_reclass_xml.cpp">
<Filter>Source Files\imports</Filter>
</ClCompile>
<ClCompile Include="..\src\imports\import_source.cpp">
<Filter>Source Files\imports</Filter>
</ClCompile>
<ClCompile Include="..\src\mcp\mcp_bridge.cpp">
<Filter>Source Files\mcp</Filter>
</ClCompile>
<ClCompile Include="..\src\themes\theme.cpp">
<Filter>Source Files\themes</Filter>
</ClCompile>
<ClCompile Include="..\src\themes\themeeditor.cpp">
<Filter>Source Files\themes</Filter>
</ClCompile>
<ClCompile Include="..\src\themes\thememanager.cpp">
<Filter>Source Files\themes</Filter>
</ClCompile>
<ClCompile Include="..\src\main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{39E2DDF6-CB76-4063-B957-66ECF1252010}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<RootNamespace>RemoteProcessMemoryPlugin</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;..\plugins\RemoteProcessMemory;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;..\plugins\RemoteProcessMemory;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>psapi.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<QtMoc Include="..\src\processpicker.h" />
</ItemGroup>
<ItemGroup>
<QtUic Include="..\src\processpicker.ui" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\plugins\RemoteProcessMemory\RemoteProcessMemoryPlugin.h" />
<ClInclude Include="..\plugins\RemoteProcessMemory\rcx_rpc_protocol.h" />
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\providers\provider.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\plugins\RemoteProcessMemory\RemoteProcessMemoryPlugin.cpp" />
<ClCompile Include="..\src\processpicker.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{E25D358E-20F0-448B-BB2F-55E9D1F8E7CA}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<RootNamespace>WinDbgMemoryPlugin</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\Plugins\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>dbgeng.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>..\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>false</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>dbgeng.lib;ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<QtMoc Include="..\plugins\WinDbgMemory\WinDbgMemoryPlugin.h" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\src\iplugin.h" />
<ClInclude Include="..\src\providers\provider.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\plugins\WinDbgMemory\WinDbgMemoryPlugin.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

81
msvc/fadec.vcxproj Normal file
View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{6A30A4F0-1A8D-4C6E-82D4-0A0D9693AA40}</ProjectGuid>
<RootNamespace>fadec</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<FadecDir>..\third_party\fadec\</FadecDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(FadecDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<DisableSpecificWarnings>4018;4146;4244;4245;4267;4310</DisableSpecificWarnings>
<LanguageStandard_C>stdc11</LanguageStandard_C>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(FadecDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<DisableSpecificWarnings>4018;4146;4244;4245;4267;4310</DisableSpecificWarnings>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<LanguageStandard_C>stdc11</LanguageStandard_C>
</ClCompile>
</ItemDefinitionGroup>
<Target Name="GenerateFadecTables" BeforeTargets="ClCompile"
Inputs="$(FadecDir)instrs.txt;$(FadecDir)parseinstrs.py"
Outputs="$(FadecDir)fadec-decode-public.inc;$(FadecDir)fadec-decode-private.inc">
<Exec Command="python &quot;$(FadecDir)parseinstrs.py&quot; decode &quot;$(FadecDir)instrs.txt&quot; &quot;$(FadecDir)fadec-decode-public.inc&quot; &quot;$(FadecDir)fadec-decode-private.inc&quot; --32 --64" />
</Target>
<ItemGroup>
<ClCompile Include="..\third_party\fadec\decode.c" />
<ClCompile Include="..\third_party\fadec\format.c" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\third_party\fadec\fadec.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
</Project>

445
msvc/qscintilla.vcxproj Normal file
View File

@@ -0,0 +1,445 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="17.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{F7124B57-7682-4702-B725-4D844DC41ADA}</ProjectGuid>
<Keyword>QtVS_v304</Keyword>
<RootNamespace>qscintilla</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets;printsupport</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>Qt 6.10.2 MSVC</QtInstall>
<QtModules>core;gui;widgets;printsupport</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(QtMsBuild)\Qt.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<QScintillaDir>..\third_party\qscintilla\</QScintillaDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(QScintillaDir)src;$(QScintillaDir)scintilla\include;$(QScintillaDir)scintilla\lexlib;$(QScintillaDir)scintilla\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>TurnOffAllWarnings</WarningLevel>
<PreprocessorDefinitions>SCINTILLA_QT;SCI_LEXER;INCLUDE_DEPRECATED_FEATURES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ExceptionHandling>Sync</ExceptionHandling>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<ClCompile>
<AdditionalIncludeDirectories>$(QScintillaDir)src;$(QScintillaDir)scintilla\include;$(QScintillaDir)scintilla\lexlib;$(QScintillaDir)scintilla\src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WarningLevel>TurnOffAllWarnings</WarningLevel>
<PreprocessorDefinitions>SCINTILLA_QT;SCI_LEXER;INCLUDE_DEPRECATED_FEATURES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ExceptionHandling>Sync</ExceptionHandling>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
</ItemDefinitionGroup>
<!-- QtMoc headers (Q_OBJECT) — QScintilla Qt wrapper -->
<ItemGroup>
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciabstractapis.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciapis.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexer.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerasm.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeravs.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerbash.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerbatch.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercmake.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercoffeescript.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercpp.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercsharp.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercss.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexercustom.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerd.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerdiff.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeredifact.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerfortran.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerfortran77.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerhex.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerhtml.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeridl.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerintelhex.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjava.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjavascript.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerjson.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerlua.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermakefile.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermarkdown.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermasm.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexermatlab.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexernasm.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeroctave.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpascal.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerperl.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpostscript.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpo.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpov.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerproperties.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerpython.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerruby.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerspice.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexersql.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexersrec.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertcl.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertekhex.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexertex.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerverilog.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexervhdl.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexerxml.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscilexeryaml.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qscimacro.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciscintilla.h" />
<QtMoc Include="..\third_party\qscintilla\src\Qsci\qsciscintillabase.h" />
<QtMoc Include="..\third_party\qscintilla\src\SciClasses.h" />
<QtMoc Include="..\third_party\qscintilla\src\ScintillaQt.h" />
</ItemGroup>
<!-- ClInclude headers (no Q_OBJECT) -->
<ItemGroup>
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscicommand.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscicommandset.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscidocument.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qsciglobal.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qsciprinter.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscistyle.h" />
<ClInclude Include="..\third_party\qscintilla\src\Qsci\qscistyledtext.h" />
<ClInclude Include="..\third_party\qscintilla\src\ListBoxQt.h" />
<ClInclude Include="..\third_party\qscintilla\src\SciAccessibility.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ILexer.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ILoader.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Platform.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Sci_Position.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\SciLexer.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\Scintilla.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\include\ScintillaWidget.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\Accessor.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\CharacterCategory.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\CharacterSet.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\DefaultLexer.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexAccessor.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerBase.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerModule.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerNoExceptions.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\LexerSimple.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\OptionSet.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\PropSetSimple.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\SparseState.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\StringCopy.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\StyleContext.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\SubStyles.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\lexlib\WordList.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\AutoComplete.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CallTip.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CaseConvert.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CaseFolder.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Catalogue.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CellBuffer.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\CharClassify.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ContractionState.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\DBCS.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Decoration.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Document.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\EditModel.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Editor.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\EditView.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ElapsedPeriod.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ExternalLexer.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\FontQuality.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Indicator.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\IntegerRectangle.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\KeyMap.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\LineMarker.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\MarginView.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Partitioning.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\PerLine.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Position.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\PositionCache.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\RESearch.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\RunStyles.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ScintillaBase.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Selection.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\SparseVector.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\SplitVector.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\Style.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\UniConversion.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\UniqueString.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\ViewStyle.h" />
<ClInclude Include="..\third_party\qscintilla\scintilla\src\XPM.h" />
</ItemGroup>
<!-- QScintilla Qt wrapper sources -->
<ItemGroup>
<ClCompile Include="..\third_party\qscintilla\src\qsciscintilla.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qsciscintillabase.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qsciabstractapis.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qsciapis.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscicommand.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscicommandset.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscidocument.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexer.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerasm.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexeravs.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerbash.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerbatch.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercmake.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercoffeescript.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercpp.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercsharp.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercss.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexercustom.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerd.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerdiff.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexeredifact.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerfortran.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerfortran77.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerhex.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerhtml.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexeridl.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerintelhex.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjava.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjavascript.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerjson.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerlua.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexermakefile.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexermarkdown.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexermasm.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexermatlab.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexernasm.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexeroctave.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpascal.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerperl.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpostscript.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpo.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpov.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerproperties.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerpython.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerruby.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerspice.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexersql.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexersrec.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexertcl.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexertekhex.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexertex.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerverilog.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexervhdl.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexerxml.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscilexeryaml.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscimacro.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qsciprinter.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscistyle.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\qscistyledtext.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\InputMethod.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\ListBoxQt.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\PlatQt.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\SciAccessibility.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\SciClasses.cpp" />
<ClCompile Include="..\third_party\qscintilla\src\ScintillaQt.cpp" />
</ItemGroup>
<!-- Scintilla lexers -->
<ItemGroup>
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexA68K.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAPDL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexASY.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAU3.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAVE.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAVS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAbaqus.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAda.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAsm.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexAsn1.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBaan.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBash.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBasic.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBatch.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBibTeX.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexBullant.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCLW.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCOBOL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCPP.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCSS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCaml.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCmake.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCoffeeScript.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexConf.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCrontab.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexCsound.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexD.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDMAP.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDMIS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexDiff.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexECL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEDIFACT.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEScript.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexEiffel.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexErlang.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexErrorList.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexFlagship.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexForth.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexFortran.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexGAP.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexGui4Cli.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHTML.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHaskell.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexHex.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexIndent.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexInno.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexJSON.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexKVIrc.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexKix.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLaTeX.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLisp.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLout.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexLua.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMMIXAL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMPT.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMSSQL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMagik.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMake.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMarkdown.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMatlab.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMaxima.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMetapost.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexModula.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexMySQL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNimrod.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNsis.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexNull.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexOScript.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexOpal.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPB.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPLM.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPO.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPOV.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPascal.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPerl.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPowerPro.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPowerShell.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexProgress.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexProps.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexPython.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexR.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRebol.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRegistry.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRuby.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexRust.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSAS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSML.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSQL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSTTXT.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexScriptol.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSmalltalk.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSorcus.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSpecman.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexSpice.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexStata.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTACL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTADS3.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTAL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTCL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTCMD.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTeX.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexTxt2tags.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVB.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVHDL.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVerilog.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexVisualProlog.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexers\LexYAML.cpp" />
</ItemGroup>
<!-- Scintilla lexlib -->
<ItemGroup>
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\Accessor.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\CharacterCategory.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\CharacterSet.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\DefaultLexer.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerBase.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerModule.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerNoExceptions.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\LexerSimple.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\PropSetSimple.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\StyleContext.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\lexlib\WordList.cpp" />
</ItemGroup>
<!-- Scintilla core engine -->
<ItemGroup>
<ClCompile Include="..\third_party\qscintilla\scintilla\src\AutoComplete.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CallTip.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CaseConvert.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CaseFolder.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Catalogue.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CellBuffer.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\CharClassify.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ContractionState.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\DBCS.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Decoration.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Document.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\EditModel.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Editor.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\EditView.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ExternalLexer.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Indicator.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\KeyMap.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\LineMarker.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\MarginView.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\PerLine.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\PositionCache.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\RESearch.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\RunStyles.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ScintillaBase.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Selection.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\Style.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\UniConversion.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\ViewStyle.cpp" />
<ClCompile Include="..\third_party\qscintilla\scintilla\src\XPM.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">
<Import Project="$(QtMsBuild)\qt.targets" />
</ImportGroup>
<ImportGroup Label="ExtensionTargets" />
</Project>

View File

@@ -0,0 +1,63 @@
cmake_minimum_required(VERSION 3.20)
project(KernelMemoryPlugin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC OFF) # run uic manually to avoid dupbuild with ProcessMemoryPlugin
# ─── Generate ui_processpicker.h in our own build dir ────────────────
set(_UI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui")
set(_UI_HDR "${CMAKE_CURRENT_BINARY_DIR}/ui_processpicker.h")
add_custom_command(
OUTPUT "${_UI_HDR}"
COMMAND ${QT}::uic -o "${_UI_HDR}" "${_UI_SRC}"
DEPENDS "${_UI_SRC}"
COMMENT "UIC processpicker.ui (KernelMemoryPlugin)"
VERBATIM
)
# ─── Plugin DLL ──────────────────────────────────────────────────────
set(PLUGIN_SOURCES
KernelMemoryPlugin.h
KernelMemoryPlugin.cpp
rcx_drv_protocol.h
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
"${_UI_HDR}"
)
add_library(KernelMemoryPlugin SHARED ${PLUGIN_SOURCES})
target_link_libraries(KernelMemoryPlugin PRIVATE
${QT}::Widgets
${_QT_WINEXTRAS}
)
if(WIN32)
target_link_libraries(KernelMemoryPlugin PRIVATE psapi shell32 advapi32)
endif()
if(UNIX AND NOT APPLE)
target_compile_options(KernelMemoryPlugin PRIVATE -fvisibility=hidden)
endif()
target_include_directories(KernelMemoryPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR} # for ui_processpicker.h
)
set_target_properties(KernelMemoryPlugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)
install(TARGETS KernelMemoryPlugin
LIBRARY DESTINATION Plugins
RUNTIME DESTINATION Plugins
)

View File

@@ -0,0 +1,751 @@
#include "KernelMemoryPlugin.h"
#include "../../src/processpicker.h"
#include <QStyle>
#include <QApplication>
#include <QMenu>
#include <QMessageBox>
#include <QDir>
#include <QFileInfo>
#include <QGuiApplication>
#include <QLibrary>
#ifdef _WIN32
#include <windows.h>
#include <tlhelp32.h>
#include <psapi.h>
#include <shellapi.h>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <QtWin>
#endif
#endif
// ─────────────────────────────────────────────────────────────────────────
// Helper: DeviceIoControl wrapper
// ─────────────────────────────────────────────────────────────────────────
#ifdef _WIN32
static bool ioctlCall(HANDLE h, DWORD code,
const void* in, DWORD inLen,
void* out, DWORD outLen,
DWORD* bytesReturned = nullptr)
{
DWORD br = 0;
BOOL ok = DeviceIoControl(h, code, const_cast<LPVOID>(in), inLen,
out, outLen, &br, nullptr);
if (bytesReturned) *bytesReturned = br;
return ok != FALSE;
}
#endif // _WIN32
// ─────────────────────────────────────────────────────────────────────────
// KernelProcessProvider
// ─────────────────────────────────────────────────────────────────────────
KernelProcessProvider::KernelProcessProvider(void* driverHandle, uint32_t pid, const QString& processName)
: m_driverHandle(driverHandle)
, m_pid(pid)
, m_processName(processName)
{
if (m_driverHandle) {
queryPeb();
cacheModules();
}
}
bool KernelProcessProvider::read(uint64_t addr, void* buf, int len) const
{
#ifdef _WIN32
if (!m_driverHandle || len <= 0) return false;
if (len > RCX_DRV_MAX_VIRTUAL) len = RCX_DRV_MAX_VIRTUAL;
RcxDrvReadRequest req{};
req.pid = m_pid;
req.address = addr;
req.length = (uint32_t)len;
DWORD br = 0;
BOOL ok = DeviceIoControl((HANDLE)m_driverHandle,
IOCTL_RCX_READ_MEMORY,
&req, sizeof(req),
buf, (DWORD)len, &br, nullptr);
// Zero unread portion (partial copy)
if ((int)br < len)
memset((char*)buf + br, 0, len - br);
return ok || br > 0;
#else
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
return false;
#endif
}
int KernelProcessProvider::size() const
{
return m_driverHandle ? 0x10000 : 0;
}
bool KernelProcessProvider::write(uint64_t addr, const void* buf, int len)
{
#ifdef _WIN32
if (!m_driverHandle || len <= 0) return false;
if (len > RCX_DRV_MAX_VIRTUAL) return false;
// Build request: header + inline data
QByteArray packet(sizeof(RcxDrvWriteRequest) + len, Qt::Uninitialized);
auto* req = reinterpret_cast<RcxDrvWriteRequest*>(packet.data());
req->pid = m_pid;
req->_pad0 = 0;
req->address = addr;
req->length = (uint32_t)len;
req->_pad1 = 0;
memcpy(packet.data() + sizeof(RcxDrvWriteRequest), buf, len);
return ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_WRITE_MEMORY,
packet.constData(), (DWORD)packet.size(),
nullptr, 0);
#else
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
return false;
#endif
}
QString KernelProcessProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules) {
if (addr >= mod.base && addr < mod.base + mod.size) {
uint64_t offset = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(offset, 0, 16, QChar('0'));
}
}
return {};
}
uint64_t KernelProcessProvider::symbolToAddress(const QString& name) const
{
for (const auto& mod : m_modules) {
if (mod.name.compare(name, Qt::CaseInsensitive) == 0)
return mod.base;
}
return 0;
}
QVector<rcx::MemoryRegion> KernelProcessProvider::enumerateRegions() const
{
QVector<rcx::MemoryRegion> regions;
#ifdef _WIN32
if (!m_driverHandle) return regions;
RcxDrvQueryRegionsRequest req{};
req.pid = m_pid;
// Allocate generous output buffer for region entries
constexpr int kMaxEntries = 8192;
QByteArray outBuf(kMaxEntries * sizeof(RcxDrvRegionEntry), Qt::Uninitialized);
DWORD br = 0;
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_REGIONS,
&req, sizeof(req),
outBuf.data(), (DWORD)outBuf.size(), &br))
return regions;
int count = (int)(br / sizeof(RcxDrvRegionEntry));
auto* entries = reinterpret_cast<const RcxDrvRegionEntry*>(outBuf.constData());
for (int i = 0; i < count; ++i) {
const auto& e = entries[i];
// Only include committed, accessible regions
if (!(e.state & 0x1000)) continue; // MEM_COMMIT = 0x1000
uint32_t p = e.protect;
if (p & 0x01) continue; // PAGE_NOACCESS
if (p & 0x100) continue; // PAGE_GUARD
rcx::MemoryRegion region;
region.base = e.base;
region.size = e.size;
region.readable = true;
region.writable = (p & 0x04) || (p & 0x08) || (p & 0x40) || (p & 0x80);
region.executable = (p & 0x10) || (p & 0x20) || (p & 0x40) || (p & 0x80);
// Match module name
for (const auto& mod : m_modules) {
if (region.base >= mod.base && region.base < mod.base + mod.size) {
region.moduleName = mod.name;
break;
}
}
regions.append(region);
}
#endif
return regions;
}
void KernelProcessProvider::queryPeb()
{
#ifdef _WIN32
RcxDrvQueryPebRequest req{};
req.pid = m_pid;
RcxDrvQueryPebResponse resp{};
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_PEB,
&req, sizeof(req), &resp, sizeof(resp))) {
m_peb = resp.pebAddress;
if (resp.pointerSize == 4)
m_pointerSize = 4;
}
#endif
}
QVector<rcx::Provider::ThreadInfo> KernelProcessProvider::tebs() const
{
QVector<ThreadInfo> result;
#ifdef _WIN32
if (!m_driverHandle) return result;
RcxDrvQueryTebsRequest req{};
req.pid = m_pid;
constexpr int kMaxThreads = 4096;
QByteArray outBuf(kMaxThreads * sizeof(RcxDrvTebEntry), Qt::Uninitialized);
DWORD br = 0;
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_TEBS,
&req, sizeof(req),
outBuf.data(), (DWORD)outBuf.size(), &br))
return result;
int count = (int)(br / sizeof(RcxDrvTebEntry));
auto* entries = reinterpret_cast<const RcxDrvTebEntry*>(outBuf.constData());
for (int i = 0; i < count; ++i)
result.push_back(ThreadInfo{entries[i].tebAddress, entries[i].threadId});
#endif
return result;
}
void KernelProcessProvider::cacheModules()
{
#ifdef _WIN32
if (!m_driverHandle) return;
RcxDrvQueryModulesRequest req{};
req.pid = m_pid;
constexpr int kMaxModules = 1024;
QByteArray outBuf(kMaxModules * sizeof(RcxDrvModuleEntry), Qt::Uninitialized);
DWORD br = 0;
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_MODULES,
&req, sizeof(req),
outBuf.data(), (DWORD)outBuf.size(), &br))
return;
int count = (int)(br / sizeof(RcxDrvModuleEntry));
auto* entries = reinterpret_cast<const RcxDrvModuleEntry*>(outBuf.constData());
m_modules.reserve(count);
for (int i = 0; i < count; ++i) {
QString modName = QString::fromUtf16(reinterpret_cast<const char16_t*>(entries[i].name));
if (i == 0)
m_base = entries[i].base;
m_modules.push_back(ModuleInfo{modName, entries[i].base, entries[i].size});
}
#endif
}
// ─────────────────────────────────────────────────────────────────────────
// KernelProcessProvider — paging / address translation
// ─────────────────────────────────────────────────────────────────────────
uint64_t KernelProcessProvider::getCr3() const
{
#ifdef _WIN32
if (m_cr3Cache) return m_cr3Cache;
if (!m_driverHandle) return 0;
RcxDrvReadCr3Request req{};
req.pid = m_pid;
RcxDrvReadCr3Response resp{};
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_READ_CR3,
&req, sizeof(req), &resp, sizeof(resp))) {
m_cr3Cache = resp.cr3;
return m_cr3Cache;
}
#endif
return 0;
}
rcx::VtopResult KernelProcessProvider::translateAddress(uint64_t va) const
{
rcx::VtopResult result{};
#ifdef _WIN32
if (!m_driverHandle) return result;
RcxDrvVtopRequest req{};
req.pid = m_pid;
req.virtualAddress = va;
RcxDrvVtopResponse resp{};
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_VTOP,
&req, sizeof(req), &resp, sizeof(resp))) {
result.physical = resp.physicalAddress;
result.pml4e = resp.pml4e;
result.pdpte = resp.pdpte;
result.pde = resp.pde;
result.pte = resp.pte;
result.pageSize = resp.pageSize;
result.valid = resp.valid != 0;
}
#else
Q_UNUSED(va);
#endif
return result;
}
QVector<uint64_t> KernelProcessProvider::readPageTable(uint64_t physAddr, int startIdx, int count) const
{
QVector<uint64_t> entries;
#ifdef _WIN32
if (!m_driverHandle) return entries;
if (startIdx < 0 || startIdx >= 512) return entries;
if (count <= 0) return entries;
if (startIdx + count > 512) count = 512 - startIdx;
// Read the full 4KB page table via physical read
int byteOffset = startIdx * 8;
int byteLen = count * 8;
QByteArray buf(byteLen, 0);
RcxDrvPhysReadRequest req{};
req.physAddress = physAddr + byteOffset;
req.length = (uint32_t)byteLen;
req.width = 0; // memcpy mode
DWORD br = 0;
if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_READ_PHYS,
&req, sizeof(req), buf.data(), (DWORD)byteLen, &br)) {
entries.resize(count);
memcpy(entries.data(), buf.constData(), byteLen);
}
#else
Q_UNUSED(physAddr); Q_UNUSED(startIdx); Q_UNUSED(count);
#endif
return entries;
}
// ─────────────────────────────────────────────────────────────────────────
// KernelPhysProvider
// ─────────────────────────────────────────────────────────────────────────
KernelPhysProvider::KernelPhysProvider(void* driverHandle, uint64_t baseAddr)
: m_driverHandle(driverHandle)
, m_baseAddr(baseAddr)
{
}
bool KernelPhysProvider::read(uint64_t addr, void* buf, int len) const
{
#ifdef _WIN32
if (!m_driverHandle || len <= 0) return false;
// Read in 4KB chunks (driver cap)
int offset = 0;
while (offset < len) {
int chunk = qMin(len - offset, (int)RCX_DRV_MAX_PHYSICAL);
RcxDrvPhysReadRequest req{};
req.physAddress = addr + offset;
req.length = (uint32_t)chunk;
req.width = 0; // memcpy mode
DWORD br = 0;
BOOL ok = DeviceIoControl((HANDLE)m_driverHandle,
IOCTL_RCX_READ_PHYS,
&req, sizeof(req),
(char*)buf + offset, (DWORD)chunk, &br, nullptr);
if (!ok && br == 0) {
memset((char*)buf + offset, 0, len - offset);
return offset > 0;
}
if ((int)br < chunk)
memset((char*)buf + offset + br, 0, chunk - br);
offset += chunk;
}
return true;
#else
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
return false;
#endif
}
bool KernelPhysProvider::write(uint64_t addr, const void* buf, int len)
{
#ifdef _WIN32
if (!m_driverHandle || len <= 0) return false;
int offset = 0;
while (offset < len) {
int chunk = qMin(len - offset, (int)RCX_DRV_MAX_PHYSICAL);
QByteArray packet(sizeof(RcxDrvPhysWriteRequest) + chunk, Qt::Uninitialized);
auto* req = reinterpret_cast<RcxDrvPhysWriteRequest*>(packet.data());
req->physAddress = addr + offset;
req->length = (uint32_t)chunk;
req->width = 0;
memcpy(packet.data() + sizeof(RcxDrvPhysWriteRequest), (const char*)buf + offset, chunk);
if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_WRITE_PHYS,
packet.constData(), (DWORD)packet.size(),
nullptr, 0))
return false;
offset += chunk;
}
return true;
#else
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
return false;
#endif
}
// ─────────────────────────────────────────────────────────────────────────
// KernelMemoryPlugin
// ─────────────────────────────────────────────────────────────────────────
KernelMemoryPlugin::KernelMemoryPlugin()
{
}
KernelMemoryPlugin::~KernelMemoryPlugin()
{
stopDriver();
}
QIcon KernelMemoryPlugin::Icon() const
{
return qApp->style()->standardIcon(QStyle::SP_DriveHDIcon);
}
bool KernelMemoryPlugin::canHandle(const QString& target) const
{
return target.startsWith(QStringLiteral("km:"))
|| target.startsWith(QStringLiteral("phys:"));
}
std::unique_ptr<rcx::Provider> KernelMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
{
if (!ensureDriverLoaded(errorMsg))
return nullptr;
#ifdef _WIN32
if (target.startsWith(QStringLiteral("km:"))) {
// km:{pid}:{name}
QStringList parts = target.mid(3).split(':');
bool ok = false;
uint32_t pid = parts[0].toUInt(&ok);
if (!ok || pid == 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target: ") + target;
return nullptr;
}
QString name = parts.size() > 1 ? parts[1] : QStringLiteral("PID %1").arg(pid);
auto prov = std::make_unique<KernelProcessProvider>((void*)m_driverHandle, pid, name);
if (!prov->isValid()) {
if (errorMsg)
*errorMsg = QStringLiteral("Failed to read process %1 (PID: %2) via kernel driver.")
.arg(name).arg(pid);
return nullptr;
}
return prov;
}
if (target.startsWith(QStringLiteral("phys:"))) {
// phys:{baseAddr}
bool ok = false;
uint64_t baseAddr = target.mid(5).toULongLong(&ok, 16);
if (!ok) baseAddr = 0;
return std::make_unique<KernelPhysProvider>((void*)m_driverHandle, baseAddr);
}
#endif
if (errorMsg) *errorMsg = QStringLiteral("Unknown target format: ") + target;
return nullptr;
}
uint64_t KernelMemoryPlugin::getInitialBaseAddress(const QString& target) const
{
if (target.startsWith(QStringLiteral("phys:"))) {
bool ok = false;
uint64_t addr = target.mid(5).toULongLong(&ok, 16);
return ok ? addr : 0;
}
// For process mode, the provider discovers base via modules
return 0;
}
bool KernelMemoryPlugin::selectTarget(QWidget* parent, QString* target)
{
// Show process picker directly (physical memory is accessed via
// context menu "Browse Page Tables" / "Follow Physical Frame" on an
// attached kernel process).
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
QList<ProcessInfo> processes;
for (const auto& pinfo : pluginProcesses) {
ProcessInfo info;
info.pid = pinfo.pid;
info.name = pinfo.name;
info.path = pinfo.path;
info.icon = pinfo.icon;
info.is32Bit = pinfo.is32Bit;
processes.append(info);
}
ProcessPicker picker(processes, parent);
if (picker.exec() == QDialog::Accepted) {
uint32_t pid = picker.selectedProcessId();
QString name = picker.selectedProcessName();
*target = QStringLiteral("km:%1:%2").arg(pid).arg(name);
return true;
}
return false;
}
QVector<PluginProcessInfo> KernelMemoryPlugin::enumerateProcesses()
{
QVector<PluginProcessInfo> processes;
#ifdef _WIN32
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) return processes;
PROCESSENTRY32W entry;
entry.dwSize = sizeof(entry);
if (Process32FirstW(snapshot, &entry)) {
do {
PluginProcessInfo info;
info.pid = entry.th32ProcessID;
info.name = QString::fromWCharArray(entry.szExeFile);
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID);
if (hProcess) {
wchar_t path[MAX_PATH * 2];
DWORD pathLen = sizeof(path) / sizeof(wchar_t);
if (QueryFullProcessImageNameW(hProcess, 0, path, &pathLen)) {
info.path = QString::fromWCharArray(path);
SHFILEINFOW sfi = {};
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) {
if (sfi.hIcon) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QPixmap pixmap = QPixmap::fromImage(QImage::fromHICON(sfi.hIcon));
#else
QPixmap pixmap = QtWin::fromHICON(sfi.hIcon);
#endif
info.icon = QIcon(pixmap);
DestroyIcon(sfi.hIcon);
}
}
}
BOOL isWow64 = FALSE;
if (IsWow64Process(hProcess, &isWow64) && isWow64)
info.is32Bit = true;
CloseHandle(hProcess);
}
processes.append(info);
} while (Process32NextW(snapshot, &entry));
}
CloseHandle(snapshot);
#endif
return processes;
}
void KernelMemoryPlugin::populatePluginMenu(QMenu* menu)
{
if (!m_driverLoaded) return;
menu->addAction(QStringLiteral("Unload Kernel Driver"), [this]() { unloadDriver(); });
}
// ─────────────────────────────────────────────────────────────────────────
// Driver service management
// ─────────────────────────────────────────────────────────────────────────
QString KernelMemoryPlugin::driverPath() const
{
// Resolve rcxdrv.sys next to the plugin DLL
QString pluginDir = QCoreApplication::applicationDirPath() + QStringLiteral("/Plugins");
return pluginDir + QStringLiteral("/rcxdrv.sys");
}
bool KernelMemoryPlugin::ensureDriverLoaded(QString* errorMsg)
{
#ifdef _WIN32
// Already connected?
if (m_driverLoaded && m_driverHandle != INVALID_HANDLE_VALUE) {
RcxDrvPingResponse ping{};
if (ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping)))
return true;
// Handle went stale — close it and try to reconnect
CloseHandle(m_driverHandle);
m_driverHandle = INVALID_HANDLE_VALUE;
m_driverLoaded = false;
}
// Show wait cursor (SCM + StartService can take seconds on first load)
struct WaitCursorGuard {
WaitCursorGuard() { QGuiApplication::setOverrideCursor(Qt::WaitCursor); }
~WaitCursorGuard() { QGuiApplication::restoreOverrideCursor(); }
} waitCursor;
// Fast path: driver may already be running (previous session, or after disconnect).
// Just try to open the device handle directly.
m_driverHandle = CreateFileA(RCX_DRV_USERMODE_PATH,
GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (m_driverHandle != INVALID_HANDLE_VALUE) {
RcxDrvPingResponse ping{};
if (ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping))) {
m_driverLoaded = true;
return true;
}
CloseHandle(m_driverHandle);
m_driverHandle = INVALID_HANDLE_VALUE;
}
// Slow path: need to install/start the service.
QString sysPath = driverPath();
if (!QFileInfo::exists(sysPath)) {
if (errorMsg)
*errorMsg = QStringLiteral("Driver not found: %1\n\n"
"Place rcxdrv.sys in the Plugins folder next to the plugin DLL.").arg(sysPath);
return false;
}
SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
if (!scm) {
if (errorMsg)
*errorMsg = QStringLiteral("Failed to open Service Control Manager.\n"
"Run Reclass as Administrator to load the kernel driver.");
return false;
}
// Try to open existing service first
SC_HANDLE svc = OpenServiceW(scm, L"RcxDrv", SERVICE_ALL_ACCESS);
if (!svc) {
// Service doesn't exist — create it
std::wstring wPath = sysPath.toStdWString();
svc = CreateServiceW(scm, L"RcxDrv", L"RcxDrv",
SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
wPath.c_str(),
nullptr, nullptr, nullptr, nullptr, nullptr);
if (!svc) {
DWORD err = GetLastError();
if (errorMsg)
*errorMsg = QStringLiteral("Failed to create driver service (error %1).\n"
"Ensure test signing is enabled: bcdedit /set testsigning on").arg(err);
CloseServiceHandle(scm);
return false;
}
}
// Start service (ERROR_SERVICE_ALREADY_RUNNING is fine — means it's already up)
if (!StartServiceW(svc, 0, nullptr)) {
DWORD err = GetLastError();
if (err != ERROR_SERVICE_ALREADY_RUNNING) {
if (errorMsg)
*errorMsg = QStringLiteral("Failed to start driver (error %1).\n"
"Ensure test signing is enabled and the driver is properly signed.").arg(err);
CloseServiceHandle(svc);
CloseServiceHandle(scm);
return false;
}
}
// Done with SCM — don't hold handles open
CloseServiceHandle(svc);
CloseServiceHandle(scm);
// Open device handle
m_driverHandle = CreateFileA(RCX_DRV_USERMODE_PATH,
GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (m_driverHandle == INVALID_HANDLE_VALUE) {
if (errorMsg)
*errorMsg = QStringLiteral("Driver started but could not open device handle.\n"
"Device path: %1").arg(QString::fromLatin1(RCX_DRV_USERMODE_PATH));
return false;
}
// Verify with ping
RcxDrvPingResponse ping{};
if (!ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping))) {
if (errorMsg)
*errorMsg = QStringLiteral("Driver opened but ping failed.");
CloseHandle(m_driverHandle);
m_driverHandle = INVALID_HANDLE_VALUE;
return false;
}
m_driverLoaded = true;
return true;
#else
if (errorMsg)
*errorMsg = QStringLiteral("Kernel driver is only supported on Windows.");
return false;
#endif
}
void KernelMemoryPlugin::unloadDriver()
{
#ifdef _WIN32
// Close device handle only — service stays running so we can reconnect
if (m_driverHandle != INVALID_HANDLE_VALUE) {
CloseHandle(m_driverHandle);
m_driverHandle = INVALID_HANDLE_VALUE;
}
m_driverLoaded = false;
#endif
}
void KernelMemoryPlugin::stopDriver()
{
#ifdef _WIN32
unloadDriver();
// Full cleanup: stop + delete the service
SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
if (scm) {
SC_HANDLE svc = OpenServiceW(scm, L"RcxDrv", SERVICE_ALL_ACCESS);
if (svc) {
SERVICE_STATUS ss;
ControlService(svc, SERVICE_CONTROL_STOP, &ss);
DeleteService(svc);
CloseServiceHandle(svc);
}
CloseServiceHandle(scm);
}
#endif
}
// ─────────────────────────────────────────────────────────────────────────
// Plugin factory
// ─────────────────────────────────────────────────────────────────────────
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{
return new KernelMemoryPlugin();
}

View File

@@ -0,0 +1,142 @@
#pragma once
#include "../../src/iplugin.h"
#include "../../src/core.h"
#include "rcx_drv_protocol.h"
#include <cstdint>
#ifdef _WIN32
#include <windows.h>
#endif
// ─────────────────────────────────────────────────────────────────────────
// Provider variants
// ─────────────────────────────────────────────────────────────────────────
/**
* Kernel-mode process memory provider.
* Reads/writes target process virtual memory via IOCTL_RCX_READ/WRITE_MEMORY.
*/
class KernelProcessProvider : public rcx::Provider
{
public:
KernelProcessProvider(void* driverHandle, uint32_t pid, const QString& processName);
~KernelProcessProvider() override = default;
bool read(uint64_t addr, void* buf, int len) const override;
int size() const override;
bool write(uint64_t addr, const void* buf, int len) override;
bool isWritable() const override { return true; }
QString name() const override { return m_processName; }
QString kind() const override { return QStringLiteral("KernelProcess"); }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& name) const override;
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
int pointerSize() const override { return m_pointerSize; }
QVector<rcx::MemoryRegion> enumerateRegions() const override;
bool isReadable(uint64_t, int len) const override { return m_driverHandle && len >= 0; }
uint32_t pid() const { return m_pid; }
uint64_t peb() const override { return m_peb; }
QVector<ThreadInfo> tebs() const override;
// ── Paging / address translation ──
bool hasKernelPaging() const override { return true; }
uint64_t getCr3() const override;
rcx::VtopResult translateAddress(uint64_t va) const override;
QVector<uint64_t> readPageTable(uint64_t physAddr, int startIdx = 0, int count = 512) const override;
void* driverHandle() const { return m_driverHandle; }
private:
void queryPeb();
void cacheModules();
void* m_driverHandle;
uint32_t m_pid;
QString m_processName;
uint64_t m_base = 0;
int m_pointerSize = 8;
uint64_t m_peb = 0;
mutable uint64_t m_cr3Cache = 0;
struct ModuleInfo {
QString name;
uint64_t base;
uint64_t size;
};
QVector<ModuleInfo> m_modules;
};
/**
* Kernel-mode physical memory provider.
* Reads/writes raw physical addresses via IOCTL_RCX_READ/WRITE_PHYS.
*/
class KernelPhysProvider : public rcx::Provider
{
public:
KernelPhysProvider(void* driverHandle, uint64_t baseAddr);
~KernelPhysProvider() override = default;
bool read(uint64_t addr, void* buf, int len) const override;
int size() const override { return m_driverHandle ? 0x10000 : 0; }
bool write(uint64_t addr, const void* buf, int len) override;
bool isWritable() const override { return true; }
QString name() const override { return QStringLiteral("Physical Memory"); }
QString kind() const override { return QStringLiteral("Physical"); }
bool isLive() const override { return true; }
uint64_t base() const override { return m_baseAddr; }
bool isReadable(uint64_t, int len) const override { return m_driverHandle && len >= 0; }
void setBaseAddr(uint64_t addr) { m_baseAddr = addr; }
void* driverHandle() const { return m_driverHandle; }
private:
void* m_driverHandle;
uint64_t m_baseAddr;
};
// ─────────────────────────────────────────────────────────────────────────
// Plugin
// ─────────────────────────────────────────────────────────────────────────
class KernelMemoryPlugin : public IProviderPlugin
{
public:
KernelMemoryPlugin();
~KernelMemoryPlugin() override;
std::string Name() const override { return "Kernel Memory"; }
std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "Reclass"; }
std::string Description() const override { return "Read and write memory via kernel driver (IOCTL)"; }
k_ELoadType LoadType() const override { return k_ELoadTypeManual; }
QIcon Icon() const override;
bool canHandle(const QString& target) const override;
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
uint64_t getInitialBaseAddress(const QString& target) const override;
bool selectTarget(QWidget* parent, QString* target) override;
bool providesProcessList() const override { return true; }
QVector<PluginProcessInfo> enumerateProcesses() override;
void populatePluginMenu(QMenu* menu) override;
private:
bool ensureDriverLoaded(QString* errorMsg = nullptr);
void unloadDriver(); // close handle only — service stays running
void stopDriver(); // full cleanup: close handle + stop + delete service
QString driverPath() const;
#ifdef _WIN32
HANDLE m_driverHandle = INVALID_HANDLE_VALUE;
#endif
bool m_driverLoaded = false;
};
// Plugin export
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();

View File

@@ -0,0 +1,99 @@
@echo off
setlocal enabledelayedexpansion
:: ── Auto-detect MSVC (override with MSVC env var) ──
if not defined MSVC (
set "VSBASE=C:\Program Files\Microsoft Visual Studio\2022"
for %%E in (Enterprise Professional Community BuildTools) do (
if exist "!VSBASE!\%%E\VC\Tools\MSVC" (
for /f "delims=" %%V in ('dir /b /ad /o-n "!VSBASE!\%%E\VC\Tools\MSVC" 2^>nul') do (
if not defined MSVC set "MSVC=!VSBASE!\%%E\VC\Tools\MSVC\%%V"
)
)
)
)
if not defined MSVC (
echo ERROR: Could not find MSVC toolchain
exit /b 1
)
:: ── Auto-detect WDK (override with WDK_INC_ROOT and WDK_LIB_ROOT env vars) ──
:: SDK_INC_ROOT is optional; when WDK is installed traditionally, SDK shared
:: headers live alongside WDK headers. NuGet splits them into a separate package.
if not defined WDK_INC_ROOT (
set "WDK=C:\Program Files (x86)\Windows Kits\10"
set WDKVER=
for /f "delims=" %%V in ('dir /b /ad /o-n "!WDK!\Include" 2^>nul') do (
if exist "!WDK!\Include\%%V\km\ntddk.h" (
if not defined WDKVER set "WDKVER=%%V"
)
)
if not defined WDKVER (
echo ERROR: Could not find WDK headers under !WDK!\Include
echo Set WDK_INC_ROOT and WDK_LIB_ROOT environment variables to override.
exit /b 1
)
set "WDK_INC_ROOT=!WDK!\Include\!WDKVER!"
set "WDK_LIB_ROOT=!WDK!\Lib\!WDKVER!"
set "SDK_INC_ROOT=!WDK!\Include\!WDKVER!"
)
:: If SDK_INC_ROOT not set, default to WDK_INC_ROOT (traditional install has both)
if not defined SDK_INC_ROOT set "SDK_INC_ROOT=%WDK_INC_ROOT%"
echo Using MSVC: %MSVC%
echo Using WDK inc: %WDK_INC_ROOT%
echo Using SDK inc: %SDK_INC_ROOT%
echo Using WDK lib: %WDK_LIB_ROOT%
set "CL_EXE=%MSVC%\bin\Hostx64\x64\cl.exe"
set "LINK_EXE=%MSVC%\bin\Hostx64\x64\link.exe"
set "SRCDIR=%~dp0"
set "OUTDIR=%SRCDIR%build"
if not exist "%OUTDIR%" mkdir "%OUTDIR%"
echo === Compiling rcxdrv.c ===
"%CL_EXE%" /nologo /c /Zi /W4 /WX- /O2 /GS- ^
/D "NDEBUG" /D "_AMD64_" /D "AMD64" /D "_WIN64" /D "KERNEL" ^
/D "NTDDI_VERSION=0x0A000000" ^
/I "%WDK_INC_ROOT%\km" ^
/I "%WDK_INC_ROOT%\km\crt" ^
/I "%WDK_INC_ROOT%\shared" ^
/I "%SDK_INC_ROOT%\shared" ^
/I "%SDK_INC_ROOT%\ucrt" ^
/kernel ^
/Fo"%OUTDIR%\rcxdrv.obj" ^
"%SRCDIR%rcxdrv.c"
if errorlevel 1 goto :fail
echo === Linking rcxdrv.sys ===
"%LINK_EXE%" /nologo ^
/OUT:"%OUTDIR%\rcxdrv.sys" ^
/DRIVER:WDM ^
/SUBSYSTEM:NATIVE ^
/ENTRY:DriverEntry ^
/MACHINE:X64 ^
/NODEFAULTLIB ^
/RELEASE ^
/MERGE:.rdata=.text ^
/INTEGRITYCHECK ^
/PDBALTPATH:rcxdrv.pdb ^
/PDB:"%OUTDIR%\rcxdrv.pdb" ^
"%OUTDIR%\rcxdrv.obj" ^
"%WDK_LIB_ROOT%\km\x64\ntoskrnl.lib" ^
"%WDK_LIB_ROOT%\km\x64\hal.lib" ^
"%WDK_LIB_ROOT%\km\x64\BufferOverflowK.lib" ^
"%MSVC%\lib\x64\libcmt.lib"
if errorlevel 1 goto :fail
echo.
echo === SUCCESS ===
echo Output: %OUTDIR%\rcxdrv.sys
goto :eof
:fail
echo.
echo === BUILD FAILED ===
exit /b 1

View File

@@ -0,0 +1,808 @@
/*
* rcxdrv.c -- Minimal kernel-mode memory driver for Reclass.
*
* Provides: virtual memory R/W (per-process), physical memory R/W,
* region/PEB/module/TEB query, CR3 read, virtual-to-physical translation.
*
* Safety: all inputs validated, SEH around privileged instructions,
* MmCopyVirtualMemory for cross-process reads (no attach deadlock),
* METHOD_BUFFERED (no raw user pointers).
*/
#include <ntifs.h>
#include "../rcx_drv_protocol.h"
/* ── Undocumented but stable kernel exports (Vista+) ────────────── */
NTSTATUS NTAPI MmCopyVirtualMemory(
PEPROCESS SourceProcess, PVOID SourceAddress,
PEPROCESS TargetProcess, PVOID TargetAddress,
SIZE_T BufferSize, KPROCESSOR_MODE PreviousMode,
PSIZE_T ReturnSize);
PPEB NTAPI PsGetProcessPeb(PEPROCESS Process);
PVOID NTAPI PsGetProcessWow64Process(PEPROCESS Process);
PVOID NTAPI PsGetThreadTeb(PETHREAD Thread);
/*
* PsGetNextProcessThread is undocumented (not in any .lib).
* We resolve it dynamically via MmGetSystemRoutineAddress.
*/
typedef PETHREAD (NTAPI *PsGetNextProcessThread_t)(PEPROCESS Process, PETHREAD Thread);
static PsGetNextProcessThread_t g_PsGetNextProcessThread = NULL;
/* ── Manual structure definitions (kernel-mode) ─────────────────── */
/* These are partially opaque in WDK headers; define just the offsets we need. */
typedef struct _MEMORY_BASIC_INFORMATION_KM {
PVOID BaseAddress;
PVOID AllocationBase;
ULONG AllocationProtect;
SIZE_T RegionSize;
ULONG State;
ULONG Protect;
ULONG Type;
} MEMORY_BASIC_INFORMATION_KM;
#define MEM_COMMIT_KM 0x1000
/* PEB.Ldr minimal definition for module enumeration */
typedef struct _PEB_LDR_DATA_KM {
UCHAR Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InLoadOrderModuleList;
} PEB_LDR_DATA_KM;
/* PEB minimal: only need Ldr at offset 0x18 (x64) */
typedef struct _PEB_KM {
UCHAR Reserved1[2];
UCHAR BeingDebugged;
UCHAR Reserved2[0x15];
PEB_LDR_DATA_KM* Ldr; /* offset 0x18 on x64 */
} PEB_KM;
/* LDR_DATA_TABLE_ENTRY minimal for walking InLoadOrderModuleList */
typedef struct _LDR_DATA_TABLE_ENTRY_KM {
LIST_ENTRY InLoadOrderLinks; /* offset 0x00 */
LIST_ENTRY InMemoryOrderLinks; /* offset 0x10 */
LIST_ENTRY InInitializationOrderLinks; /* offset 0x20 */
PVOID DllBase; /* offset 0x30 */
PVOID EntryPoint; /* offset 0x38 */
ULONG SizeOfImage; /* offset 0x40 */
ULONG _pad;
UNICODE_STRING FullDllName; /* offset 0x48 */
UNICODE_STRING BaseDllName; /* offset 0x58 */
} LDR_DATA_TABLE_ENTRY_KM;
/* ── Forward declarations ────────────────────────────────────────── */
static NTSTATUS DispatchCreateClose(PDEVICE_OBJECT dev, PIRP irp);
static NTSTATUS DispatchIoctl(PDEVICE_OBJECT dev, PIRP irp);
DRIVER_UNLOAD DriverUnload;
/* ZwCurrentProcess() macro for ZwQueryVirtualMemory */
#ifndef ZwCurrentProcess
#define ZwCurrentProcess() ((HANDLE)(LONG_PTR)-1)
#endif
/* ── Helpers ─────────────────────────────────────────────────────── */
#define VALIDATE_INPUT(irp, stk, T) \
do { \
if ((stk)->Parameters.DeviceIoControl.InputBufferLength < sizeof(T)) { \
(irp)->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; \
(irp)->IoStatus.Information = 0; \
IoCompleteRequest((irp), IO_NO_INCREMENT); \
return STATUS_BUFFER_TOO_SMALL; \
} \
} while (0)
#define VALIDATE_OUTPUT(irp, stk, minSize) \
do { \
if ((stk)->Parameters.DeviceIoControl.OutputBufferLength < (ULONG)(minSize)) { \
(irp)->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; \
(irp)->IoStatus.Information = 0; \
IoCompleteRequest((irp), IO_NO_INCREMENT); \
return STATUS_BUFFER_TOO_SMALL; \
} \
} while (0)
static NTSTATUS LookupProcess(ULONG pid, PEPROCESS* proc)
{
return PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)pid, proc);
}
/* ── Safe physical mapping (MDL-based, avoids MmMapIoSpace BSOD) ── */
/*
* MmMapIoSpace/MmUnmapIoSpace BSODs (bugcheck 0x50 in
* MiClearMappingAndDereferenceIoSpace) when used on RAM-backed physical
* addresses. MDL-based mapping is safe for both RAM and MMIO.
*
* CRITICAL: cacheType must match the existing kernel mapping of the page.
* Use MmCached for RAM pages (already mapped cached by the kernel).
* Use MmNonCached ONLY for MMIO/device registers.
* Mismatched cache attributes (e.g. MmNonCached on RAM) cause silent
* kernel memory corruption via CPU cache coherency conflicts.
*/
typedef struct { PMDL mdl; PVOID base; } PHYS_MAP_CTX;
static PVOID MapPhysical(uint64_t physAddr, SIZE_T size,
MEMORY_CACHING_TYPE cacheType, PHYS_MAP_CTX* ctx)
{
ctx->mdl = NULL;
ctx->base = NULL;
ULONG_PTR pageOff = (ULONG_PTR)(physAddr & (PAGE_SIZE - 1));
SIZE_T totalSize = pageOff + size;
ULONG pages = (ULONG)((totalSize + PAGE_SIZE - 1) / PAGE_SIZE);
PMDL mdl = IoAllocateMdl(NULL, (ULONG)totalSize, FALSE, FALSE, NULL);
if (!mdl) return NULL;
PPFN_NUMBER pfn = MmGetMdlPfnArray(mdl);
PFN_NUMBER startPfn = (PFN_NUMBER)(physAddr / PAGE_SIZE);
for (ULONG i = 0; i < pages; i++)
pfn[i] = startPfn + i;
mdl->MdlFlags |= MDL_PAGES_LOCKED;
__try {
ctx->base = MmMapLockedPagesSpecifyCache(
mdl, KernelMode, cacheType, NULL, FALSE, NormalPagePriority);
} __except (EXCEPTION_EXECUTE_HANDLER) {
IoFreeMdl(mdl);
return NULL;
}
if (!ctx->base) { IoFreeMdl(mdl); return NULL; }
ctx->mdl = mdl;
return (PUCHAR)ctx->base + pageOff;
}
static void UnmapPhysical(PHYS_MAP_CTX* ctx)
{
if (ctx->base) MmUnmapLockedPages(ctx->base, ctx->mdl);
if (ctx->mdl) IoFreeMdl(ctx->mdl);
ctx->base = NULL;
ctx->mdl = NULL;
}
/* ── Virtual memory read ─────────────────────────────────────────── */
static NTSTATUS HandleReadMemory(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvReadRequest);
struct RcxDrvReadRequest* req = (struct RcxDrvReadRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_VIRTUAL)
return STATUS_INVALID_PARAMETER;
VALIDATE_OUTPUT(irp, stk, req->length);
/* Save request fields before MmCopyVirtualMemory overwrites SystemBuffer.
* METHOD_BUFFERED aliases input and output to the same buffer, so the
* copy destination (SystemBuffer) clobbers req->* fields. */
ULONG pid = req->pid;
uint64_t address = req->address;
ULONG length = req->length;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(pid, &proc);
if (!NT_SUCCESS(st)) return st;
SIZE_T bytesRead = 0;
st = MmCopyVirtualMemory(
proc, (PVOID)address,
PsGetCurrentProcess(), irp->AssociatedIrp.SystemBuffer,
(SIZE_T)length, KernelMode, &bytesRead);
ObDereferenceObject(proc);
/* Partial reads: zero remainder, report success */
if (st == STATUS_PARTIAL_COPY) {
RtlZeroMemory((PUCHAR)irp->AssociatedIrp.SystemBuffer + bytesRead,
length - bytesRead);
irp->IoStatus.Information = length;
return STATUS_SUCCESS;
}
irp->IoStatus.Information = NT_SUCCESS(st) ? length : 0;
return st;
}
/* ── Virtual memory write ────────────────────────────────────────── */
static NTSTATUS HandleWriteMemory(PIRP irp, PIO_STACK_LOCATION stk)
{
ULONG inputLen = stk->Parameters.DeviceIoControl.InputBufferLength;
if (inputLen < sizeof(struct RcxDrvWriteRequest))
return STATUS_BUFFER_TOO_SMALL;
struct RcxDrvWriteRequest* req = (struct RcxDrvWriteRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_VIRTUAL)
return STATUS_INVALID_PARAMETER;
if (inputLen < sizeof(struct RcxDrvWriteRequest) + req->length)
return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
PUCHAR data = (PUCHAR)req + sizeof(struct RcxDrvWriteRequest);
SIZE_T bytesWritten = 0;
st = MmCopyVirtualMemory(
PsGetCurrentProcess(), data,
proc, (PVOID)req->address,
(SIZE_T)req->length, KernelMode, &bytesWritten);
ObDereferenceObject(proc);
irp->IoStatus.Information = 0;
return st;
}
/* ── Physical memory read ────────────────────────────────────────── */
static NTSTATUS HandleReadPhys(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvPhysReadRequest);
struct RcxDrvPhysReadRequest* req = (struct RcxDrvPhysReadRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_PHYSICAL)
return STATUS_INVALID_PARAMETER;
if (req->width != 0 && req->width != 1 && req->width != 2 && req->width != 4)
return STATUS_INVALID_PARAMETER;
VALIDATE_OUTPUT(irp, stk, req->length);
/* Save request fields before SystemBuffer is overwritten (METHOD_BUFFERED
* aliases input and output to the same buffer). */
uint64_t physAddress = req->physAddress;
ULONG length = req->length;
ULONG width = req->width;
PUCHAR dst = (PUCHAR)irp->AssociatedIrp.SystemBuffer;
if (width == 0) {
/* Byte copy -- use MmCopyMemory (safe for both RAM and MMIO) */
MM_COPY_ADDRESS srcAddr;
srcAddr.PhysicalAddress.QuadPart = (LONGLONG)physAddress;
SIZE_T bytesCopied = 0;
NTSTATUS st = MmCopyMemory(dst, srcAddr, (SIZE_T)length,
MM_COPY_MEMORY_PHYSICAL, &bytesCopied);
if (!NT_SUCCESS(st)) return st;
if (bytesCopied < length)
RtlZeroMemory(dst + bytesCopied, length - bytesCopied);
irp->IoStatus.Information = length;
return STATUS_SUCCESS;
}
/* Width-aware MMIO reads -- map via MDL (safe for all physical addresses).
* Use MmNonCached: width>0 implies MMIO register access where uncached
* semantics are required for correct device interaction. */
PHYS_MAP_CTX mapCtx;
PUCHAR src = (PUCHAR)MapPhysical(physAddress, (SIZE_T)length, MmNonCached, &mapCtx);
if (!src) return STATUS_UNSUCCESSFUL;
__try {
ULONG off = 0;
while (off + width <= length) {
if (width == 1)
dst[off] = READ_REGISTER_UCHAR(&src[off]);
else if (width == 2)
*(USHORT*)(dst + off) = READ_REGISTER_USHORT((PUSHORT)(src + off));
else
*(ULONG*)(dst + off) = READ_REGISTER_ULONG((PULONG)(src + off));
off += width;
}
if (off < length)
RtlZeroMemory(dst + off, length - off);
} __except (EXCEPTION_EXECUTE_HANDLER) {
UnmapPhysical(&mapCtx);
return STATUS_UNSUCCESSFUL;
}
UnmapPhysical(&mapCtx);
irp->IoStatus.Information = length;
return STATUS_SUCCESS;
}
/* ── Physical memory write ───────────────────────────────────────── */
static NTSTATUS HandleWritePhys(PIRP irp, PIO_STACK_LOCATION stk)
{
ULONG inputLen = stk->Parameters.DeviceIoControl.InputBufferLength;
if (inputLen < sizeof(struct RcxDrvPhysWriteRequest))
return STATUS_BUFFER_TOO_SMALL;
struct RcxDrvPhysWriteRequest* req = (struct RcxDrvPhysWriteRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_PHYSICAL)
return STATUS_INVALID_PARAMETER;
if (req->width != 0 && req->width != 1 && req->width != 2 && req->width != 4)
return STATUS_INVALID_PARAMETER;
if (inputLen < sizeof(struct RcxDrvPhysWriteRequest) + req->length)
return STATUS_BUFFER_TOO_SMALL;
PUCHAR src = (PUCHAR)req + sizeof(struct RcxDrvPhysWriteRequest);
/* Map via MDL (safe for both RAM and MMIO).
* width==0 → RAM byte write (MmCached to avoid cache attribute conflict).
* width>0 → MMIO register write (MmNonCached for correct device semantics). */
MEMORY_CACHING_TYPE ct = (req->width == 0) ? MmCached : MmNonCached;
PHYS_MAP_CTX mapCtx;
PUCHAR dst = (PUCHAR)MapPhysical(req->physAddress, (SIZE_T)req->length, ct, &mapCtx);
if (!dst) return STATUS_UNSUCCESSFUL;
__try {
if (req->width == 0) {
RtlCopyMemory(dst, src, req->length);
} else {
ULONG off = 0;
while (off + req->width <= req->length) {
if (req->width == 1)
WRITE_REGISTER_UCHAR(&dst[off], src[off]);
else if (req->width == 2)
WRITE_REGISTER_USHORT((PUSHORT)(dst + off), *(USHORT*)(src + off));
else
WRITE_REGISTER_ULONG((PULONG)(dst + off), *(ULONG*)(src + off));
off += req->width;
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
UnmapPhysical(&mapCtx);
return STATUS_UNSUCCESSFUL;
}
UnmapPhysical(&mapCtx);
irp->IoStatus.Information = 0;
return STATUS_SUCCESS;
}
/* ── Ping ────────────────────────────────────────────────────────── */
static NTSTATUS HandlePing(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvPingResponse));
struct RcxDrvPingResponse* rsp = (struct RcxDrvPingResponse*)irp->AssociatedIrp.SystemBuffer;
rsp->version = RCX_DRV_VERSION;
rsp->driverBuild = __LINE__;
irp->IoStatus.Information = sizeof(struct RcxDrvPingResponse);
return STATUS_SUCCESS;
}
/* ── Query PEB ───────────────────────────────────────────────────── */
static NTSTATUS HandleQueryPeb(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryPebRequest);
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvQueryPebResponse));
struct RcxDrvQueryPebRequest* req = (struct RcxDrvQueryPebRequest*)irp->AssociatedIrp.SystemBuffer;
struct RcxDrvQueryPebResponse* rsp = (struct RcxDrvQueryPebResponse*)irp->AssociatedIrp.SystemBuffer;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
rsp->pebAddress = (uint64_t)(ULONG_PTR)PsGetProcessPeb(proc);
rsp->pointerSize = 8;
rsp->_pad = 0;
/* Detect WoW64 (32-bit process on 64-bit OS) */
PVOID wow64 = PsGetProcessWow64Process(proc);
if (wow64) {
rsp->pebAddress = (uint64_t)(ULONG_PTR)wow64;
rsp->pointerSize = 4;
}
ObDereferenceObject(proc);
irp->IoStatus.Information = sizeof(struct RcxDrvQueryPebResponse);
return STATUS_SUCCESS;
}
/* ── Query Regions ───────────────────────────────────────────────── */
static NTSTATUS HandleQueryRegions(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryRegionsRequest);
struct RcxDrvQueryRegionsRequest* req = (struct RcxDrvQueryRegionsRequest*)irp->AssociatedIrp.SystemBuffer;
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
ULONG maxEntries = outputLen / sizeof(struct RcxDrvRegionEntry);
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
/* Attach to target process to query its address space.
* IOCTLs arrive at PASSIVE_LEVEL; KeStackAttachProcess requires <= APC_LEVEL.
* ZwQueryVirtualMemory with ZwCurrentProcess() while attached queries the
* attached process's address space (correct). */
KAPC_STATE apcState;
KeStackAttachProcess(proc, &apcState);
struct RcxDrvRegionEntry* entries = (struct RcxDrvRegionEntry*)irp->AssociatedIrp.SystemBuffer;
ULONG count = 0;
PVOID addr = NULL;
MEMORY_BASIC_INFORMATION_KM mbi;
while (count < maxEntries) {
SIZE_T retLen = 0;
st = ZwQueryVirtualMemory(ZwCurrentProcess(), addr, 0 /*MemoryBasicInformation*/,
&mbi, sizeof(mbi), &retLen);
if (!NT_SUCCESS(st)) break;
if (mbi.State == MEM_COMMIT_KM) {
entries[count].base = (uint64_t)(ULONG_PTR)mbi.BaseAddress;
entries[count].size = (uint64_t)mbi.RegionSize;
entries[count].protect = mbi.Protect;
entries[count].state = mbi.State;
count++;
}
ULONG_PTR next = (ULONG_PTR)mbi.BaseAddress + mbi.RegionSize;
if (next <= (ULONG_PTR)addr) break; /* overflow */
addr = (PVOID)next;
}
KeUnstackDetachProcess(&apcState);
ObDereferenceObject(proc);
irp->IoStatus.Information = count * sizeof(struct RcxDrvRegionEntry);
return STATUS_SUCCESS;
}
/* ── Query Modules ───────────────────────────────────────────────── */
static NTSTATUS HandleQueryModules(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryModulesRequest);
struct RcxDrvQueryModulesRequest* req = (struct RcxDrvQueryModulesRequest*)irp->AssociatedIrp.SystemBuffer;
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
ULONG maxEntries = outputLen / sizeof(struct RcxDrvModuleEntry);
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
/* Attach to target process to read PEB->Ldr */
KAPC_STATE apcState;
KeStackAttachProcess(proc, &apcState);
struct RcxDrvModuleEntry* entries = (struct RcxDrvModuleEntry*)irp->AssociatedIrp.SystemBuffer;
ULONG count = 0;
__try {
/* Read PEB address */
PEB_KM* peb = (PEB_KM*)PsGetProcessPeb(proc);
if (!peb) goto done;
ProbeForRead(peb, sizeof(PEB_KM), 1);
/* PEB->Ldr at offset 0x18 (x64) */
PEB_LDR_DATA_KM* ldr = peb->Ldr;
if (!ldr) goto done;
ProbeForRead(ldr, sizeof(PEB_LDR_DATA_KM), 1);
/* Walk InLoadOrderModuleList */
LIST_ENTRY* head = &ldr->InLoadOrderModuleList;
LIST_ENTRY* cur = head->Flink;
while (cur != head && count < maxEntries) {
LDR_DATA_TABLE_ENTRY_KM* entry = CONTAINING_RECORD(cur, LDR_DATA_TABLE_ENTRY_KM, InLoadOrderLinks);
entries[count].base = (uint64_t)(ULONG_PTR)entry->DllBase;
entries[count].size = (uint64_t)entry->SizeOfImage;
/* Copy wide-char name (truncate to 259 chars + null) */
USHORT nameLen = entry->BaseDllName.Length / sizeof(WCHAR);
if (nameLen > 259) nameLen = 259;
if (entry->BaseDllName.Buffer) {
RtlCopyMemory(entries[count].name, entry->BaseDllName.Buffer,
nameLen * sizeof(uint16_t));
}
entries[count].name[nameLen] = 0;
count++;
cur = cur->Flink;
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
/* Partial results are fine */
}
done:
KeUnstackDetachProcess(&apcState);
ObDereferenceObject(proc);
irp->IoStatus.Information = count * sizeof(struct RcxDrvModuleEntry);
return STATUS_SUCCESS;
}
/* ── Query TEBs ──────────────────────────────────────────────────── */
/*
* Walk the target process's thread list to collect TEB addresses.
* Uses PsGetNextProcessThread (undocumented but stable since Vista).
*/
static NTSTATUS HandleQueryTebs(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryTebsRequest);
struct RcxDrvQueryTebsRequest* req = (struct RcxDrvQueryTebsRequest*)irp->AssociatedIrp.SystemBuffer;
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
ULONG maxEntries = outputLen / sizeof(struct RcxDrvTebEntry);
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
struct RcxDrvTebEntry* entries = (struct RcxDrvTebEntry*)irp->AssociatedIrp.SystemBuffer;
ULONG count = 0;
if (!g_PsGetNextProcessThread) {
ObDereferenceObject(proc);
return STATUS_NOT_SUPPORTED;
}
/* PsGetNextProcessThread increments the ref on the returned PETHREAD and
* dereferences the previous one. We must release the last thread if we
* exit the loop early (exception or maxEntries hit). */
{
PETHREAD thread = NULL;
__try {
while ((thread = g_PsGetNextProcessThread(proc, thread)) != NULL) {
if (count >= maxEntries) {
/* Hit limit — release the thread PsGetNextProcessThread just returned */
ObDereferenceObject(thread);
break;
}
PVOID teb = PsGetThreadTeb(thread);
if (teb) {
entries[count].tebAddress = (uint64_t)(ULONG_PTR)teb;
entries[count].threadId = (uint32_t)(ULONG_PTR)PsGetThreadId(thread);
entries[count]._pad = 0;
count++;
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
/* Exception mid-iteration: thread holds a referenced PETHREAD — release it */
if (thread)
ObDereferenceObject(thread);
}
}
ObDereferenceObject(proc);
irp->IoStatus.Information = count * sizeof(struct RcxDrvTebEntry);
return STATUS_SUCCESS;
}
/* ── Read CR3 (DirectoryTableBase) ────────────────────────────────── */
/*
* EPROCESS.DirectoryTableBase offset. Stable across Win10/11 x64.
* Verified: 0x028 on 1507-22H2+ (KPROCESS is at offset 0 of EPROCESS).
*/
#define KPROCESS_DIRECTORY_TABLE_BASE 0x028
static NTSTATUS HandleReadCr3(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvReadCr3Request);
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvReadCr3Response));
struct RcxDrvReadCr3Request* req = (struct RcxDrvReadCr3Request*)irp->AssociatedIrp.SystemBuffer;
struct RcxDrvReadCr3Response* rsp = (struct RcxDrvReadCr3Response*)irp->AssociatedIrp.SystemBuffer;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
__try {
rsp->cr3 = *(uint64_t*)((PUCHAR)proc + KPROCESS_DIRECTORY_TABLE_BASE);
/* Mask off PCID bits (bits 0-11) to get the PML4 physical address */
rsp->cr3 &= ~0xFFFULL;
rsp->kernelCr3 = rsp->cr3; /* same on non-KPTI; KPTI shadow is not easily accessible */
} __except (EXCEPTION_EXECUTE_HANDLER) {
ObDereferenceObject(proc);
return STATUS_UNSUCCESSFUL;
}
ObDereferenceObject(proc);
irp->IoStatus.Information = sizeof(struct RcxDrvReadCr3Response);
return STATUS_SUCCESS;
}
/* ── Virtual-to-Physical address translation ─────────────────────── */
/* NOTE: This walks the page table non-atomically via 4 sequential physical reads.
* The page table can be modified between reads (e.g., page-out, remap). This is
* an inherent limitation shared by WinDbg's !vtop and similar tools. For a
* debugging/reversing tool this tradeoff is acceptable. */
/* Extract physical frame address from a page table entry (bits 51:12) */
#define PTE_FRAME(pte) ((pte) & 0x000FFFFFFFFFF000ULL)
/* Check Present bit (bit 0) */
#define PTE_PRESENT(pte) ((pte) & 1ULL)
/* Check Page Size bit (bit 7) -- indicates large/huge page */
#define PTE_PS(pte) ((pte) & (1ULL << 7))
static NTSTATUS HandleVtop(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvVtopRequest);
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvVtopResponse));
struct RcxDrvVtopRequest* req = (struct RcxDrvVtopRequest*)irp->AssociatedIrp.SystemBuffer;
struct RcxDrvVtopResponse* rsp = (struct RcxDrvVtopResponse*)irp->AssociatedIrp.SystemBuffer;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
/* Read CR3 */
uint64_t cr3;
__try {
cr3 = *(uint64_t*)((PUCHAR)proc + KPROCESS_DIRECTORY_TABLE_BASE);
cr3 &= ~0xFFFULL;
} __except (EXCEPTION_EXECUTE_HANDLER) {
ObDereferenceObject(proc);
return STATUS_UNSUCCESSFUL;
}
ObDereferenceObject(proc);
uint64_t va = req->virtualAddress;
RtlZeroMemory(rsp, sizeof(*rsp));
/* Extract indices from virtual address:
* [47:39] = PML4 index, [38:30] = PDPT index,
* [29:21] = PD index, [20:12] = PT index,
* [11:0] = page offset */
ULONG pml4Idx = (ULONG)((va >> 39) & 0x1FF);
ULONG pdptIdx = (ULONG)((va >> 30) & 0x1FF);
ULONG pdIdx = (ULONG)((va >> 21) & 0x1FF);
ULONG ptIdx = (ULONG)((va >> 12) & 0x1FF);
MM_COPY_ADDRESS ca;
SIZE_T copied;
uint64_t entry;
/* Level 4: PML4 -- use MmCopyMemory (safe for RAM, unlike MmMapIoSpace) */
ca.PhysicalAddress.QuadPart = (LONGLONG)(cr3 + pml4Idx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pml4e = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
/* Level 3: PDPT */
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + pdptIdx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pdpte = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
if (PTE_PS(entry)) {
/* 1GB huge page: physical = frame[51:30] | va[29:0] */
rsp->physicalAddress = (entry & 0x000FFFFFC0000000ULL) | (va & 0x3FFFFFFFULL);
rsp->pageSize = 2;
rsp->valid = 1;
goto done;
}
/* Level 2: PD */
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + pdIdx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pde = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
if (PTE_PS(entry)) {
/* 2MB large page: physical = frame[51:21] | va[20:0] */
rsp->physicalAddress = (entry & 0x000FFFFFFFE00000ULL) | (va & 0x1FFFFFULL);
rsp->pageSize = 1;
rsp->valid = 1;
goto done;
}
/* Level 1: PT */
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + ptIdx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pte = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
/* 4KB page: physical = frame[51:12] | va[11:0] */
rsp->physicalAddress = PTE_FRAME(entry) | (va & 0xFFFULL);
rsp->pageSize = 0;
rsp->valid = 1;
done:
irp->IoStatus.Information = sizeof(struct RcxDrvVtopResponse);
return STATUS_SUCCESS;
}
/* ── IOCTL dispatch ──────────────────────────────────────────────── */
static NTSTATUS DispatchIoctl(PDEVICE_OBJECT dev, PIRP irp)
{
UNREFERENCED_PARAMETER(dev);
PIO_STACK_LOCATION stk = IoGetCurrentIrpStackLocation(irp);
NTSTATUS st;
switch (stk->Parameters.DeviceIoControl.IoControlCode) {
case IOCTL_RCX_READ_MEMORY: st = HandleReadMemory(irp, stk); break;
case IOCTL_RCX_WRITE_MEMORY: st = HandleWriteMemory(irp, stk); break;
case IOCTL_RCX_QUERY_REGIONS: st = HandleQueryRegions(irp, stk); break;
case IOCTL_RCX_QUERY_PEB: st = HandleQueryPeb(irp, stk); break;
case IOCTL_RCX_QUERY_MODULES: st = HandleQueryModules(irp, stk); break;
case IOCTL_RCX_QUERY_TEBS: st = HandleQueryTebs(irp, stk); break;
case IOCTL_RCX_PING: st = HandlePing(irp, stk); break;
case IOCTL_RCX_READ_PHYS: st = HandleReadPhys(irp, stk); break;
case IOCTL_RCX_WRITE_PHYS: st = HandleWritePhys(irp, stk); break;
case IOCTL_RCX_READ_CR3: st = HandleReadCr3(irp, stk); break;
case IOCTL_RCX_VTOP: st = HandleVtop(irp, stk); break;
default:
st = STATUS_INVALID_DEVICE_REQUEST;
irp->IoStatus.Information = 0;
break;
}
irp->IoStatus.Status = st;
IoCompleteRequest(irp, IO_NO_INCREMENT);
return st;
}
/* ── Create / Close (permit open/close) ──────────────────────────── */
static NTSTATUS DispatchCreateClose(PDEVICE_OBJECT dev, PIRP irp)
{
UNREFERENCED_PARAMETER(dev);
irp->IoStatus.Status = STATUS_SUCCESS;
irp->IoStatus.Information = 0;
IoCompleteRequest(irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
/* ── Unload ──────────────────────────────────────────────────────── */
void DriverUnload(PDRIVER_OBJECT drv)
{
UNICODE_STRING symlink = RTL_CONSTANT_STRING(L"\\DosDevices\\RcxDrv");
IoDeleteSymbolicLink(&symlink);
if (drv->DeviceObject)
IoDeleteDevice(drv->DeviceObject);
}
/* ── Entry point ─────────────────────────────────────────────────── */
NTSTATUS DriverEntry(PDRIVER_OBJECT drv, PUNICODE_STRING regPath)
{
UNREFERENCED_PARAMETER(regPath);
/* Resolve undocumented APIs */
UNICODE_STRING fnName = RTL_CONSTANT_STRING(L"PsGetNextProcessThread");
g_PsGetNextProcessThread = (PsGetNextProcessThread_t)MmGetSystemRoutineAddress(&fnName);
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\RcxDrv");
UNICODE_STRING symlink = RTL_CONSTANT_STRING(L"\\DosDevices\\RcxDrv");
PDEVICE_OBJECT devObj = NULL;
NTSTATUS st = IoCreateDevice(drv, 0, &devName, FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN, FALSE, &devObj);
if (!NT_SUCCESS(st)) return st;
st = IoCreateSymbolicLink(&symlink, &devName);
if (!NT_SUCCESS(st)) {
IoDeleteDevice(devObj);
return st;
}
drv->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose;
drv->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
drv->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctl;
drv->DriverUnload = DriverUnload;
devObj->Flags |= DO_BUFFERED_IO;
devObj->Flags &= ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}

View File

@@ -0,0 +1,17 @@
obj-m += rcxkm.o
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
install:
insmod rcxkm.ko
uninstall:
rmmod rcxkm
.PHONY: all clean install uninstall

View File

@@ -0,0 +1,132 @@
/*
* rcxkm.c -- Linux kernel module stub for Reclass kernel memory provider.
*
* Provides /dev/rcxkm char device with ioctl() dispatch using the same
* protocol structs as the Windows driver (rcx_drv_protocol.h).
*
* Build: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
*
* TODO: implement handlers (currently returns -ENOSYS for all IOCTLs).
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/pid.h>
#include <linux/mm.h>
#include "../rcx_drv_protocol.h"
#define DEVICE_NAME "rcxkm"
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Reclass");
MODULE_DESCRIPTION("Reclass kernel memory provider (stub)");
/* ── IOCTL dispatch ─────────────────────────────────────────────────── */
static long rcxkm_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
(void)filp;
(void)arg;
switch (cmd) {
case IOCTL_RCX_READ_MEMORY:
/* TODO: find_get_pid(pid) -> get_task_struct -> access_process_vm() */
return -ENOSYS;
case IOCTL_RCX_WRITE_MEMORY:
/* TODO: access_process_vm() with FOLL_WRITE */
return -ENOSYS;
case IOCTL_RCX_QUERY_REGIONS:
/* TODO: walk target mm->mmap via VMA iteration */
return -ENOSYS;
case IOCTL_RCX_QUERY_PEB:
/* N/A on Linux (no PEB); could return mm->start_brk or similar */
return -ENOSYS;
case IOCTL_RCX_QUERY_MODULES:
/* TODO: walk target /proc/pid/maps or mm VMAs */
return -ENOSYS;
case IOCTL_RCX_QUERY_TEBS:
/* N/A on Linux (no TEB) */
return -ENOSYS;
case IOCTL_RCX_PING: {
struct RcxDrvPingResponse resp = {
.version = RCX_DRV_VERSION,
.driverBuild = 1,
};
if (copy_to_user((void __user *)arg, &resp, sizeof(resp)))
return -EFAULT;
return 0;
}
case IOCTL_RCX_READ_PHYS:
/* TODO: ioremap() + memcpy_fromio() */
return -ENOSYS;
case IOCTL_RCX_WRITE_PHYS:
/* TODO: ioremap() + memcpy_toio() */
return -ENOSYS;
default:
return -EINVAL;
}
}
/* ── File operations ────────────────────────────────────────────────── */
static int rcxkm_open(struct inode *inode, struct file *filp)
{
(void)inode; (void)filp;
return 0;
}
static int rcxkm_release(struct inode *inode, struct file *filp)
{
(void)inode; (void)filp;
return 0;
}
static const struct file_operations rcxkm_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = rcxkm_ioctl,
.open = rcxkm_open,
.release = rcxkm_release,
};
static struct miscdevice rcxkm_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &rcxkm_fops,
};
/* ── Module init/exit ───────────────────────────────────────────────── */
static int __init rcxkm_init(void)
{
int ret = misc_register(&rcxkm_device);
if (ret) {
pr_err("rcxkm: failed to register misc device (err=%d)\n", ret);
return ret;
}
pr_info("rcxkm: loaded, device /dev/%s\n", DEVICE_NAME);
return 0;
}
static void __exit rcxkm_exit(void)
{
misc_deregister(&rcxkm_device);
pr_info("rcxkm: unloaded\n");
}
module_init(rcxkm_init);
module_exit(rcxkm_exit);

View File

@@ -0,0 +1,189 @@
/*
* RCX Driver Protocol -- shared between kernel driver and usermode plugin.
* No dependencies beyond standard C headers. Pure C, no Windows types.
*/
#pragma once
#ifdef KERNEL
/* Kernel mode build: avoid stdint.h (not in WDK km/crt) */
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned __int64 uint64_t;
typedef signed __int64 int64_t;
#else
#include <stdint.h>
#endif
/* ── Device / service names ───────────────────────────────────────── */
#define RCX_DRV_DEVICE_NAME L"\\Device\\RcxDrv"
#define RCX_DRV_SYMLINK_NAME L"\\DosDevices\\RcxDrv"
#define RCX_DRV_USERMODE_PATH "\\\\.\\RcxDrv"
#define RCX_DRV_SERVICE_NAME "RcxDrv"
/* ── Protocol version ─────────────────────────────────────────────── */
#define RCX_DRV_VERSION 1
/* ── Size limits ──────────────────────────────────────────────────── */
#define RCX_DRV_MAX_VIRTUAL (1024 * 1024) /* 1 MB per virtual read/write */
#define RCX_DRV_MAX_PHYSICAL 4096 /* 4 KB per physical read/write */
/* ── IOCTL codes ──────────────────────────────────────────────────── */
/* CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, function, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0) */
/* Virtual memory (per-process) */
#define IOCTL_RCX_READ_MEMORY 0x222000 /* function 0x800 */
#define IOCTL_RCX_WRITE_MEMORY 0x222004 /* function 0x801 */
#define IOCTL_RCX_QUERY_REGIONS 0x222008 /* function 0x802 */
#define IOCTL_RCX_QUERY_PEB 0x22200C /* function 0x803 */
#define IOCTL_RCX_QUERY_MODULES 0x222010 /* function 0x804 */
#define IOCTL_RCX_QUERY_TEBS 0x222014 /* function 0x805 */
#define IOCTL_RCX_PING 0x222018 /* function 0x806 */
/* Physical memory (MMIO) */
#define IOCTL_RCX_READ_PHYS 0x22201C /* function 0x807 */
#define IOCTL_RCX_WRITE_PHYS 0x222020 /* function 0x808 */
/* Paging / address translation */
#define IOCTL_RCX_READ_CR3 0x222044 /* function 0x811 */
#define IOCTL_RCX_VTOP 0x222048 /* function 0x812 */
/* ── Request / Response structures ────────────────────────────────── */
/* All structs are naturally aligned. Padding fields are explicit. */
/* -- Virtual memory -- */
struct RcxDrvReadRequest {
uint32_t pid;
uint32_t _pad0;
uint64_t address;
uint32_t length; /* max RCX_DRV_MAX_VIRTUAL */
uint32_t _pad1;
};
/* Write: input = header + inline data bytes */
struct RcxDrvWriteRequest {
uint32_t pid;
uint32_t _pad0;
uint64_t address;
uint32_t length; /* max RCX_DRV_MAX_VIRTUAL */
uint32_t _pad1;
/* uint8_t data[length] follows */
};
/* -- Region enumeration -- */
struct RcxDrvQueryRegionsRequest {
uint32_t pid;
uint32_t _pad;
};
struct RcxDrvRegionEntry {
uint64_t base;
uint64_t size;
uint32_t protect; /* raw PAGE_* flags */
uint32_t state; /* MEM_COMMIT etc. */
};
/* -- PEB -- */
struct RcxDrvQueryPebRequest {
uint32_t pid;
uint32_t _pad;
};
struct RcxDrvQueryPebResponse {
uint64_t pebAddress;
uint32_t pointerSize; /* 4 or 8 */
uint32_t _pad;
};
/* -- Modules -- */
struct RcxDrvQueryModulesRequest {
uint32_t pid;
uint32_t _pad;
};
struct RcxDrvModuleEntry {
uint64_t base;
uint64_t size;
uint16_t name[260]; /* wide-char, null-terminated */
};
/* -- TEBs -- */
struct RcxDrvQueryTebsRequest {
uint32_t pid;
uint32_t _pad;
};
struct RcxDrvTebEntry {
uint64_t tebAddress;
uint32_t threadId;
uint32_t _pad;
};
/* -- Ping -- */
struct RcxDrvPingResponse {
uint32_t version;
uint32_t driverBuild;
};
/* -- Physical memory -- */
struct RcxDrvPhysReadRequest {
uint64_t physAddress;
uint32_t length; /* max RCX_DRV_MAX_PHYSICAL */
uint32_t width; /* access width: 1, 2, or 4 (0 = memcpy) */
};
struct RcxDrvPhysWriteRequest {
uint64_t physAddress;
uint32_t length; /* max RCX_DRV_MAX_PHYSICAL */
uint32_t width; /* access width: 1, 2, or 4 (0 = memcpy) */
/* uint8_t data[length] follows */
};
/* -- Paging / address translation -- */
struct RcxDrvReadCr3Request {
uint32_t pid;
uint32_t _pad;
};
struct RcxDrvReadCr3Response {
uint64_t cr3; /* DirectoryTableBase (PML4 physical address) */
uint64_t kernelCr3; /* KernelDirectoryTableBase (KPTI shadow) */
};
struct RcxDrvVtopRequest {
uint32_t pid;
uint32_t _pad;
uint64_t virtualAddress;
};
struct RcxDrvVtopResponse {
uint64_t physicalAddress; /* final translated physical address (with page offset) */
uint64_t pml4e; /* raw PML4 entry value */
uint64_t pdpte; /* raw PDPT entry value */
uint64_t pde; /* raw PD entry value */
uint64_t pte; /* raw PT entry value (0 if large/huge page) */
uint8_t pageSize; /* 0=4KB, 1=2MB, 2=1GB */
uint8_t valid; /* 1 if translation succeeded, 0 if not present */
uint8_t _pad2[6];
};
/* ── Compile-time validation ──────────────────────────────────────── */
#ifdef __cplusplus
static_assert(sizeof(RcxDrvReadRequest) == 24, "ReadRequest layout");
static_assert(sizeof(RcxDrvWriteRequest) == 24, "WriteRequest layout");
static_assert(sizeof(RcxDrvRegionEntry) == 24, "RegionEntry layout");
static_assert(sizeof(RcxDrvModuleEntry) == 536, "ModuleEntry layout");
static_assert(sizeof(RcxDrvTebEntry) == 16, "TebEntry layout");
static_assert(sizeof(RcxDrvPingResponse) == 8, "PingResponse layout");
static_assert(sizeof(RcxDrvReadCr3Response) == 16, "ReadCr3Response layout");
static_assert(sizeof(RcxDrvVtopRequest) == 16, "VtopRequest layout");
static_assert(sizeof(RcxDrvVtopResponse) == 48, "VtopResponse layout");
#endif

View File

@@ -4,8 +4,7 @@ project(ProcessMemoryPlugin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find Qt
find_package(Qt6 REQUIRED COMPONENTS Widgets)
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
@@ -24,10 +23,20 @@ set(PLUGIN_SOURCES
add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
# Link Qt
target_link_libraries(ProcessMemoryPlugin PRIVATE Qt6::Widgets)
target_link_libraries(ProcessMemoryPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
# Platform-specific linking
if(WIN32)
target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32)
endif()
# On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported
if(UNIX AND NOT APPLE)
target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden)
endif()
# Include directories
target_include_directories(ProcessMemoryPlugin PRIVATE
target_include_directories(ProcessMemoryPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src
)
@@ -36,3 +45,12 @@ set_target_properties(ProcessMemoryPlugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)
if(APPLE AND TARGET Reclass)
add_custom_command(TARGET ProcessMemoryPlugin POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/../PlugIns"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:ProcessMemoryPlugin>"
"$<TARGET_FILE_DIR:Reclass>/../PlugIns/"
COMMENT "Copying ProcessMemoryPlugin into Reclass.app/Contents/PlugIns")
endif()

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,73 @@
#pragma once
#include "../../src/iplugin.h"
#include "../../src/core.h"
#include <windows.h>
#include <tlhelp32.h>
#include <psapi.h>
#include <shellapi.h>
#include <cstdint>
/**
* Windows process memory provider
* Reads/writes memory from a live process using Win32 API
* Process memory provider
* Reads/writes memory from a live process using platform APIs
*/
class ProcessMemoryProvider : public rcx::Provider {
class ProcessMemoryProvider : public rcx::Provider
{
public:
ProcessMemoryProvider(DWORD pid, const QString& processName);
ProcessMemoryProvider(uint32_t pid, const QString& processName);
~ProcessMemoryProvider() override;
// Required overrides
bool read(uint64_t addr, void* buf, int len) const override;
int size() const override { return m_handle ? INT_MAX : NULL; } // Process memory has no fixed size
int size() const override;
// Optional overrides
bool write(uint64_t addr, const void* buf, int len) override;
bool isWritable() const override { return m_writable; }
QString name() const override { return m_processName; }
QString kind() const override { return QStringLiteral("LocalProcess"); }
QString getSymbol(uint64_t addr) const override;
uint64_t symbolToAddress(const QString& name) const override;
bool isLive() const override { return true; }
uint64_t base() const override { return m_base; }
int pointerSize() const override { return m_pointerSize; }
QVector<rcx::MemoryRegion> enumerateRegions() const override;
bool isReadable(uint64_t, int len) const override {
#ifdef _WIN32
return m_handle && len >= 0;
#elif defined(__linux__)
return m_fd >= 0 && len >= 0;
#elif defined(__APPLE__)
return m_task != 0 && len >= 0;
#endif
}
// Process-specific helpers
DWORD pid() const { return m_pid; }
uint64_t baseAddress() const { return m_base; }
uint32_t pid() const { return m_pid; }
void refreshModules() { m_modules.clear(); cacheModules(); }
uint64_t peb() const override { return m_peb; }
QVector<ThreadInfo> tebs() const override;
QVector<ModuleEntry> enumerateModules() const override;
private:
void cacheModules();
private:
HANDLE m_handle;
DWORD m_pid;
#ifdef _WIN32
void* m_handle;
#elif defined(__linux__)
int m_fd;
#elif defined(__APPLE__)
uint32_t m_task;
#endif
uint32_t m_pid;
QString m_processName;
bool m_writable;
uint64_t m_base;
int m_pointerSize = 8;
uint64_t m_peb = 0;
struct ModuleInfo {
QString name;
QString fullPath;
uint64_t base;
uint64_t size;
};
@@ -52,24 +77,25 @@ private:
/**
* Plugin that provides ProcessMemoryProvider
*/
class ProcessMemoryPlugin : public IProviderPlugin {
class ProcessMemoryPlugin : public IProviderPlugin
{
public:
std::string Name() const override { return "Process Memory"; }
std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "ReclassX"; }
std::string Description() const override { return "Read and write memory from local running Windows processes"; }
std::string Author() const override { return "Reclass"; }
std::string Description() const override { return "Read and write memory from local running processes"; }
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
QIcon Icon() const override;
bool canHandle(const QString& target) const override;
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
uint64_t getInitialBaseAddress(const QString& target) const override;
bool selectTarget(QWidget* parent, QString* target) override;
// Optional: provide custom process list
bool providesProcessList() const override { return true; }
QVector<PluginProcessInfo> enumerateProcesses() override;
};
// Plugin export
extern "C" __declspec(dllexport) IPlugin* CreatePlugin();
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();

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,939 @@
#include "RemoteProcessMemoryPlugin.h"
#include "rcx_rpc_protocol.h"
#include "../../src/processpicker.h"
#include <QStyle>
#include <QApplication>
#include <QMessageBox>
#include <QPushButton>
#include <QDir>
#include <QFileInfo>
#include <QPixmap>
#include <QImage>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32)
#include <QtWin>
#endif
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
# include <tlhelp32.h>
# include <psapi.h>
# include <shellapi.h>
#else
# include <unistd.h>
# include <fcntl.h>
# include <dlfcn.h>
# include <sys/mman.h>
# include <sys/wait.h>
# include <sys/ptrace.h>
# include <sys/user.h>
# include <semaphore.h>
# include <signal.h>
# include <link.h>
# include <climits>
# include <cstring>
# include <fstream>
# include <sstream>
#endif
/* ══════════════════════════════════════════════════════════════════════
* IPC Client
* ══════════════════════════════════════════════════════════════════════ */
struct IpcClient {
#ifdef _WIN32
HANDLE hShm = nullptr;
HANDLE hReqEvent = nullptr;
HANDLE hRspEvent = nullptr;
#else
int shmFd = -1;
sem_t* reqSem = SEM_FAILED;
sem_t* rspSem = SEM_FAILED;
char shmNameBuf[128] = {};
char reqNameBuf[128] = {};
char rspNameBuf[128] = {};
#endif
void* mappedView = nullptr;
QMutex mutex;
bool connected = false;
RcxRpcHeader* header() const {
return mappedView ? reinterpret_cast<RcxRpcHeader*>(mappedView) : nullptr;
}
~IpcClient() { disconnect(); }
/* ── connect / disconnect ──────────────────────────────────────── */
bool connect(uint32_t pid, int timeoutMs = 5000)
{
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
#ifdef _WIN32
/* poll for shared memory to appear (payload creating it) */
auto deadline = GetTickCount64() + (uint64_t)timeoutMs;
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
if (GetTickCount64() >= deadline) return false;
Sleep(10);
}
mappedView = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!mappedView) { CloseHandle(hShm); hShm = nullptr; return false; }
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
if (!hReqEvent || !hRspEvent) { disconnect(); return false; }
#else
strncpy(shmNameBuf, shmName, sizeof(shmNameBuf) - 1);
strncpy(reqNameBuf, reqName, sizeof(reqNameBuf) - 1);
strncpy(rspNameBuf, rspName, sizeof(rspNameBuf) - 1);
/* poll for shared memory */
auto start = std::chrono::steady_clock::now();
while (true) {
shmFd = shm_open(shmName, O_RDWR, 0);
if (shmFd >= 0) break;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeoutMs) return false;
usleep(10000);
}
mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, shmFd, 0);
if (mappedView == MAP_FAILED) { mappedView = nullptr; close(shmFd); shmFd = -1; return false; }
reqSem = sem_open(reqName, 0);
rspSem = sem_open(rspName, 0);
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) { disconnect(); return false; }
#endif
/* wait for payloadReady */
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
#ifdef _WIN32
while (!hdr->payloadReady) {
if (GetTickCount64() >= deadline) { disconnect(); return false; }
Sleep(5);
}
#else
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeoutMs) { disconnect(); return false; }
usleep(5000);
}
#endif
connected = true;
return true;
}
void disconnect()
{
#ifdef _WIN32
if (mappedView) { UnmapViewOfFile(mappedView); mappedView = nullptr; }
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
#else
if (mappedView) { munmap(mappedView, RCX_RPC_SHM_SIZE); mappedView = nullptr; }
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
#endif
connected = false;
}
/* ── low-level RPC round-trip ──────────────────────────────────── */
bool signalAndWait(int timeoutMs = 2000)
{
#ifdef _WIN32
SetEvent(hReqEvent);
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
#else
sem_post(reqSem);
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeoutMs / 1000;
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
return sem_timedwait(rspSem, &ts) == 0;
#endif
}
/* ── public API ────────────────────────────────────────────────── */
bool readSingle(uint64_t addr, void* buf, int len)
{
QMutexLocker lock(&mutex);
if (!connected || len <= 0) return false;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_READ_BATCH;
hdr->requestCount = 1;
hdr->status = RCX_RPC_STATUS_OK;
auto* entry = reinterpret_cast<RcxRpcReadEntry*>(data);
entry->address = addr;
entry->length = (uint32_t)len;
entry->dataOffset = sizeof(RcxRpcReadEntry);
if (!signalAndWait()) { connected = false; return false; }
memcpy(buf, data + entry->dataOffset, len);
return true;
}
bool writeSingle(uint64_t addr, const void* buf, int len)
{
QMutexLocker lock(&mutex);
if (!connected || len <= 0) return false;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_WRITE;
hdr->writeAddress = addr;
hdr->writeLength = (uint32_t)len;
hdr->status = RCX_RPC_STATUS_OK;
memcpy(data, buf, len);
if (!signalAndWait()) { connected = false; return false; }
return hdr->status == RCX_RPC_STATUS_OK;
}
QVector<RemoteProcessProvider::ModuleInfo> enumerateModules()
{
QVector<RemoteProcessProvider::ModuleInfo> result;
QMutexLocker lock(&mutex);
if (!connected) return result;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_ENUM_MODULES;
hdr->status = RCX_RPC_STATUS_OK;
if (!signalAndWait()) { connected = false; return result; }
if (hdr->status != RCX_RPC_STATUS_OK) return result;
uint32_t count = hdr->responseCount;
result.reserve((int)count);
for (uint32_t i = 0; i < count; ++i) {
auto* entry = reinterpret_cast<const RcxRpcModuleEntry*>(
data + i * sizeof(RcxRpcModuleEntry));
QString modName;
#ifdef _WIN32
modName = QString::fromWCharArray(
reinterpret_cast<const wchar_t*>(data + entry->nameOffset),
(int)(entry->nameLength / sizeof(wchar_t)));
#else
modName = QString::fromUtf8(
reinterpret_cast<const char*>(data + entry->nameOffset),
(int)entry->nameLength);
#endif
result.push_back(RemoteProcessProvider::ModuleInfo{modName, entry->base, entry->size});
}
return result;
}
bool ping()
{
QMutexLocker lock(&mutex);
if (!connected) return false;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
hdr->command = RPC_CMD_PING;
hdr->status = RCX_RPC_STATUS_OK;
if (!signalAndWait()) { connected = false; return false; }
return true;
}
void shutdown()
{
QMutexLocker lock(&mutex);
if (!connected) return;
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
hdr->command = RPC_CMD_SHUTDOWN;
hdr->status = RCX_RPC_STATUS_OK;
signalAndWait(500);
connected = false;
}
};
/* ══════════════════════════════════════════════════════════════════════
* RemoteProcessProvider
* ══════════════════════════════════════════════════════════════════════ */
RemoteProcessProvider::RemoteProcessProvider(
uint32_t pid, const QString& processName,
std::shared_ptr<IpcClient> ipc)
: m_pid(pid)
, m_processName(processName)
, m_connected(ipc && ipc->connected)
, m_base(0)
, m_ipc(std::move(ipc))
{
if (m_connected) {
cacheModules();
// Read pointer size from payload's SHM header (0 means not set → default 8)
auto* hdr = m_ipc ? m_ipc->header() : nullptr;
if (hdr) {
uint32_t ps = hdr->pointerSize;
if (ps == 4 || ps == 8)
m_pointerSize = (int)ps;
}
}
}
RemoteProcessProvider::~RemoteProcessProvider() = default;
bool RemoteProcessProvider::read(uint64_t addr, void* buf, int len) const
{
if (!m_connected || len <= 0) return false;
bool ok = m_ipc->readSingle(addr, buf, len);
if (!ok) {
memset(buf, 0, (size_t)len);
/* update connectivity flag through mutable ipc */
const_cast<RemoteProcessProvider*>(this)->m_connected = m_ipc->connected;
}
return ok;
}
int RemoteProcessProvider::size() const
{
return m_connected ? 0x10000 : 0;
}
bool RemoteProcessProvider::write(uint64_t addr, const void* buf, int len)
{
if (!m_connected || len <= 0) return false;
bool ok = m_ipc->writeSingle(addr, buf, len);
if (!ok) m_connected = m_ipc->connected;
return ok;
}
QString RemoteProcessProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules) {
if (addr >= mod.base && addr < mod.base + mod.size) {
uint64_t off = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(off, 0, 16, QChar('0'));
}
}
return {};
}
uint64_t RemoteProcessProvider::symbolToAddress(const QString& n) const
{
for (const auto& mod : m_modules) {
if (mod.name.compare(n, Qt::CaseInsensitive) == 0)
return mod.base;
}
return 0;
}
void RemoteProcessProvider::cacheModules()
{
m_modules = m_ipc->enumerateModules();
if (!m_modules.isEmpty())
m_base = m_modules.first().base;
}
/* ══════════════════════════════════════════════════════════════════════
* Injection helpers
* ══════════════════════════════════════════════════════════════════════ */
namespace {
/* Resolve payload DLL/SO path next to this plugin DLL/SO */
static QString payloadPath()
{
#ifdef _WIN32
HMODULE hSelf = nullptr;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&payloadPath), &hSelf);
WCHAR buf[MAX_PATH];
GetModuleFileNameW(hSelf, buf, MAX_PATH);
QFileInfo fi(QString::fromWCharArray(buf));
return fi.absolutePath() + QStringLiteral("/rcx_payload.dll");
#else
Dl_info info;
dladdr(reinterpret_cast<void*>(&payloadPath), &info);
QFileInfo fi(QString::fromUtf8(info.dli_fname));
return fi.absolutePath() + QStringLiteral("/rcx_payload.so");
#endif
}
#ifdef _WIN32
/* ── Windows injection: CreateRemoteThread + LoadLibraryA ─────────── */
static bool injectPayload(uint32_t pid, QString* errorMsg)
{
QString path = payloadPath();
QByteArray pathUtf8 = QDir::toNativeSeparators(path).toLocal8Bit();
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProc) {
if (errorMsg)
*errorMsg = QStringLiteral("OpenProcess failed (error %1).\n"
"Try running as Administrator.")
.arg(GetLastError());
return false;
}
/* allocate + write path string in target */
SIZE_T pathLen = (SIZE_T)(pathUtf8.size() + 1);
void* remotePath = VirtualAllocEx(hProc, nullptr, pathLen,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!remotePath) {
if (errorMsg) *errorMsg = QStringLiteral("VirtualAllocEx failed.");
CloseHandle(hProc);
return false;
}
WriteProcessMemory(hProc, remotePath, pathUtf8.constData(), pathLen, nullptr);
/* Step 1: LoadLibraryA — loads the DLL (DllMain is minimal) */
HMODULE hK32 = GetModuleHandleA("kernel32.dll");
auto pLoadLib = reinterpret_cast<LPTHREAD_START_ROUTINE>(
GetProcAddress(hK32, "LoadLibraryA"));
HANDLE hThread = CreateRemoteThread(hProc, nullptr, 0,
pLoadLib, remotePath, 0, nullptr);
if (!hThread) {
if (errorMsg) *errorMsg = QStringLiteral("CreateRemoteThread failed (error %1).")
.arg(GetLastError());
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
CloseHandle(hProc);
return false;
}
WaitForSingleObject(hThread, 10000);
DWORD exitCode = 0;
GetExitCodeThread(hThread, &exitCode);
CloseHandle(hThread);
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
if (exitCode == 0) {
CloseHandle(hProc);
if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n"
"Ensure rcx_payload.dll is in: %1").arg(path);
return false;
}
/* Step 2: Call RcxPayloadInit() — safe to create timer queues now
(loader lock is no longer held after LoadLibrary returned) */
HMODULE hPayloadRemote = (HMODULE)(uintptr_t)exitCode;
auto pGetProcAddr = reinterpret_cast<FARPROC(WINAPI*)(HMODULE, LPCSTR)>(
GetProcAddress(hK32, "GetProcAddress"));
/* Write "RcxPayloadInit\0" into target, call GetProcAddress remotely */
const char initName[] = "RcxPayloadInit";
void* remoteInitName = VirtualAllocEx(hProc, nullptr, sizeof(initName),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (remoteInitName) {
WriteProcessMemory(hProc, remoteInitName, initName, sizeof(initName), nullptr);
/* We need to call GetProcAddress(hPayload, "RcxPayloadInit") then call the result.
Simpler approach: write small shellcode that does both calls. */
uint8_t shellcode[128];
int off = 0;
/* sub rsp, 40 ; shadow space + alignment */
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xEC; shellcode[off++] = 0x28;
/* mov rcx, hPayloadRemote ; first arg = module handle */
shellcode[off++] = 0x48; shellcode[off++] = 0xB9;
uint64_t hMod = (uint64_t)(uintptr_t)hPayloadRemote;
memcpy(shellcode + off, &hMod, 8); off += 8;
/* mov rdx, remoteInitName ; second arg = "RcxPayloadInit" */
shellcode[off++] = 0x48; shellcode[off++] = 0xBA;
uint64_t pName = (uint64_t)(uintptr_t)remoteInitName;
memcpy(shellcode + off, &pName, 8); off += 8;
/* mov rax, GetProcAddress */
shellcode[off++] = 0x48; shellcode[off++] = 0xB8;
uint64_t pGPA = (uint64_t)(uintptr_t)pGetProcAddr;
memcpy(shellcode + off, &pGPA, 8); off += 8;
/* call rax ; rax = RcxPayloadInit */
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
/* test rax, rax */
shellcode[off++] = 0x48; shellcode[off++] = 0x85; shellcode[off++] = 0xC0;
/* jz skip (jump over the call if null) */
shellcode[off++] = 0x74; shellcode[off++] = 0x02;
/* call rax ; RcxPayloadInit() */
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
/* skip: add rsp, 40 */
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xC4; shellcode[off++] = 0x28;
/* ret */
shellcode[off++] = 0xC3;
void* remoteCode = VirtualAllocEx(hProc, nullptr, (SIZE_T)off,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (remoteCode) {
WriteProcessMemory(hProc, remoteCode, shellcode, (SIZE_T)off, nullptr);
HANDLE hThread2 = CreateRemoteThread(hProc, nullptr, 0,
(LPTHREAD_START_ROUTINE)remoteCode, nullptr, 0, nullptr);
if (hThread2) {
WaitForSingleObject(hThread2, 10000);
CloseHandle(hThread2);
}
VirtualFreeEx(hProc, remoteCode, 0, MEM_RELEASE);
}
VirtualFreeEx(hProc, remoteInitName, 0, MEM_RELEASE);
}
CloseHandle(hProc);
return true;
}
#else
/* ── Linux injection: ptrace + dlopen ─────────────────────────────── */
static uint64_t findLibBase(pid_t pid, const char* libName)
{
char mapsPath[64];
snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid);
FILE* f = fopen(mapsPath, "r");
if (!f) return 0;
char line[1024];
while (fgets(line, sizeof(line), f)) {
if (strstr(line, libName)) {
uint64_t base;
if (sscanf(line, "%lx-", &base) == 1) {
fclose(f);
return base;
}
}
}
fclose(f);
return 0;
}
static uint64_t findSyscallInsn(pid_t pid)
{
char mapsPath[64];
snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid);
FILE* f = fopen(mapsPath, "r");
if (!f) return 0;
char line[1024];
while (fgets(line, sizeof(line), f)) {
if (strstr(line, "libc") && strstr(line, "r-xp")) {
uint64_t start, end;
if (sscanf(line, "%lx-%lx", &start, &end) != 2) continue;
fclose(f);
/* scan for 0F 05 (syscall) */
char memPath[64];
snprintf(memPath, sizeof(memPath), "/proc/%d/mem", pid);
int memFd = open(memPath, O_RDONLY);
if (memFd < 0) return 0;
uint8_t buf[4096];
for (uint64_t off = start; off < end; off += sizeof(buf)) {
ssize_t n = pread(memFd, buf, sizeof(buf), (off_t)off);
if (n <= 1) break;
for (ssize_t i = 0; i + 1 < n; ++i) {
if (buf[i] == 0x0F && buf[i + 1] == 0x05) {
close(memFd);
return off + (uint64_t)i;
}
}
}
close(memFd);
return 0;
}
}
fclose(f);
return 0;
}
static bool writeTargetMem(pid_t pid, uint64_t addr, const void* src, size_t len)
{
const uint8_t* p = static_cast<const uint8_t*>(src);
for (size_t i = 0; i < len; i += sizeof(long)) {
long val = 0;
size_t chunk = (len - i < sizeof(long)) ? (len - i) : sizeof(long);
if (chunk < sizeof(long)) {
errno = 0;
val = ptrace(PTRACE_PEEKDATA, pid, (void*)(addr + i), nullptr);
if (errno) return false;
}
memcpy(&val, p + i, chunk);
if (ptrace(PTRACE_POKEDATA, pid, (void*)(addr + i), (void*)val) < 0)
return false;
}
return true;
}
static bool injectPayload(uint32_t pid, QString* errorMsg)
{
QString path = payloadPath();
QByteArray pathUtf8 = path.toUtf8();
if (ptrace(PTRACE_ATTACH, (pid_t)pid, nullptr, nullptr) < 0) {
if (errorMsg)
*errorMsg = QStringLiteral("ptrace attach failed: %1\n"
"Check /proc/sys/kernel/yama/ptrace_scope or run as root.")
.arg(strerror(errno));
return false;
}
int status;
waitpid((pid_t)pid, &status, 0);
/* save registers */
struct user_regs_struct savedRegs, regs;
ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, &savedRegs);
regs = savedRegs;
/* find syscall instruction in target's libc */
uint64_t syscallAddr = findSyscallInsn((pid_t)pid);
if (!syscallAddr) {
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
if (errorMsg) *errorMsg = QStringLiteral("Could not find syscall instruction in target.");
return false;
}
/* find dlopen in target via libc offset technique */
void* ourDlopen = dlsym(RTLD_DEFAULT, "dlopen");
uint64_t ourLibcBase = findLibBase(getpid(), "libc");
uint64_t targetLibcBase = findLibBase((pid_t)pid, "libc");
if (!ourDlopen || !ourLibcBase || !targetLibcBase) {
ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr);
if (errorMsg) *errorMsg = QStringLiteral("Could not resolve dlopen address.");
return false;
}
uint64_t targetDlopen = targetLibcBase + ((uint64_t)ourDlopen - ourLibcBase);
/* call mmap in target via syscall: mmap(0, 4096, RWX, MAP_PRIVATE|MAP_ANON, -1, 0) */
regs.rax = 9; /* __NR_mmap */
regs.rdi = 0;
regs.rsi = 4096;
regs.rdx = 7; /* PROT_READ|PROT_WRITE|PROT_EXEC */
regs.r10 = 0x22; /* MAP_PRIVATE|MAP_ANONYMOUS */
regs.r8 = (uint64_t)-1;
regs.r9 = 0;
regs.rip = syscallAddr;
ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &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}:{name}" */
QStringList parts = target.split(':');
if (parts.size() < 3 || parts[0] != QStringLiteral("rpm")) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target;
return nullptr;
}
bool ok;
uint32_t pid = parts[1].toUInt(&ok);
QString name = parts.mid(2).join(':'); /* name may contain colons */
if (!ok || pid == 0) {
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target.");
return nullptr;
}
auto ipc = getOrCreateConnection(pid, errorMsg);
if (!ipc) return nullptr;
return std::make_unique<RemoteProcessProvider>(pid, name, ipc);
}
uint64_t RemoteProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
{
/* Read imageBase directly from the shared-memory header -- zero IPC cost.
The payload filled it at init from PEB->Ldr (Win) / /proc/self/maps (Linux). */
QStringList parts = target.split(':');
if (parts.size() < 2 || parts[0] != QStringLiteral("rpm"))
return 0;
bool ok;
uint32_t pid = parts[1].toUInt(&ok);
if (!ok) return 0;
QMutexLocker lock(&m_connectionsMutex);
auto it = m_connections.constFind(pid);
if (it == m_connections.constEnd() || !(*it)->connected)
return 0;
auto* hdr = static_cast<const RcxRpcHeader*>((*it)->mappedView);
return hdr->imageBase;
}
bool RemoteProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
{
/* ── 1. pick a process ── */
QVector<PluginProcessInfo> pluginProcs = enumerateProcesses();
QList<ProcessInfo> procs;
for (const auto& pi : pluginProcs) {
ProcessInfo info;
info.pid = pi.pid;
info.name = pi.name;
info.path = pi.path;
info.icon = pi.icon;
procs.append(info);
}
ProcessPicker picker(procs, parent);
if (picker.exec() != QDialog::Accepted) return false;
uint32_t pid = picker.selectedProcessId();
QString name = picker.selectedProcessName();
/* ── 2. ask inject or connect ── */
QMessageBox box(parent);
box.setWindowTitle(QStringLiteral("Remote Process Memory"));
box.setText(QStringLiteral("Connect to %1 (PID %2)").arg(name).arg(pid));
box.setInformativeText(QStringLiteral("Choose how to connect to the target:"));
QAbstractButton* injectBtn = box.addButton(QStringLiteral("Inject Payload"), QMessageBox::ActionRole);
QAbstractButton* connectBtn = box.addButton(QStringLiteral("Already Injected"), QMessageBox::ActionRole);
box.addButton(QMessageBox::Cancel);
box.exec();
QAbstractButton* clicked = box.clickedButton();
if (clicked == injectBtn) {
QString injectErr;
if (!injectPayload(pid, &injectErr)) {
QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr);
return false;
}
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
return true;
}
else if (clicked == connectBtn) {
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
return true;
}
return false;
}
QVector<PluginProcessInfo> RemoteProcessMemoryPlugin::enumerateProcesses()
{
QVector<PluginProcessInfo> procs;
#ifdef _WIN32
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return procs;
PROCESSENTRY32W entry;
entry.dwSize = sizeof(entry);
if (Process32FirstW(snap, &entry)) {
do {
PluginProcessInfo info;
info.pid = entry.th32ProcessID;
info.name = QString::fromWCharArray(entry.szExeFile);
HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
FALSE, entry.th32ProcessID);
if (hProc) {
wchar_t path[MAX_PATH * 2];
DWORD pathLen = sizeof(path) / sizeof(wchar_t);
if (QueryFullProcessImageNameW(hProc, 0, path, &pathLen)) {
info.path = QString::fromWCharArray(path);
SHFILEINFOW sfi = {};
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi),
SHGFI_ICON | SHGFI_SMALLICON) && sfi.hIcon) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
info.icon = QIcon(QPixmap::fromImage(QImage::fromHICON(sfi.hIcon)));
#else
info.icon = QIcon(QtWin::fromHICON(sfi.hIcon));
#endif
DestroyIcon(sfi.hIcon);
}
}
CloseHandle(hProc);
}
procs.append(info);
} while (Process32NextW(snap, &entry));
}
CloseHandle(snap);
#else
QDir procDir(QStringLiteral("/proc"));
QIcon defIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
for (const QString& entry : procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
bool ok;
uint32_t pid = entry.toUInt(&ok);
if (!ok || pid == 0) continue;
QFile commFile(QStringLiteral("/proc/%1/comm").arg(pid));
if (!commFile.open(QIODevice::ReadOnly)) continue;
QString procName = QString::fromUtf8(commFile.readAll()).trimmed();
commFile.close();
if (procName.isEmpty()) continue;
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
if (::access(memPath.toUtf8().constData(), R_OK) != 0) continue;
QFileInfo exeInfo(QStringLiteral("/proc/%1/exe").arg(pid));
PluginProcessInfo info;
info.pid = pid;
info.name = procName;
info.path = exeInfo.exists() ? exeInfo.symLinkTarget() : QString();
info.icon = defIcon;
procs.append(info);
}
#endif
return procs;
}
std::shared_ptr<IpcClient>
RemoteProcessMemoryPlugin::getOrCreateConnection(
uint32_t pid, QString* errorMsg)
{
QMutexLocker lock(&m_connectionsMutex);
auto it = m_connections.find(pid);
if (it != m_connections.end() && (*it)->connected)
return *it;
auto ipc = std::make_shared<IpcClient>();
if (!ipc->connect(pid)) {
if (errorMsg)
*errorMsg = QStringLiteral("Failed to connect IPC to PID %1.\n"
"Is the payload running?").arg(pid);
return nullptr;
}
m_connections[pid] = ipc;
return ipc;
}
/* ── Plugin factory ───────────────────────────────────────────────── */
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{
return new RemoteProcessMemoryPlugin();
}

View File

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

View File

@@ -0,0 +1,612 @@
/*
* rcx_payload -- injected into target process.
*
* Pure Win32 / POSIX, NO Qt, minimal footprint.
* Creates the main IPC channel (shared memory + events/semaphores)
* using PID-only naming and uses a timer queue for polling.
*/
#include "../rcx_rpc_protocol.h"
#ifdef _WIN32
/* ===================================================================
* WINDOWS implementation
* =================================================================== */
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
/* ── globals ──────────────────────────────────────────────────────── */
static HANDLE g_hShm = nullptr;
static void* g_mappedView = nullptr;
static HANDLE g_hReqEvent = nullptr;
static HANDLE g_hRspEvent = nullptr;
static HANDLE g_hTimerQueue = nullptr;
static HANDLE g_hPollTimer = nullptr;
static volatile LONG g_initialized = 0;
/* ── memory safety via VirtualQuery ────────────────────────────────── */
inline bool IsReadableProtect(DWORD p)
{
if (p & (PAGE_NOACCESS | PAGE_GUARD))
return false;
const DWORD readable =
PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
return (p & readable) != 0;
}
inline bool IsWritableProtect(DWORD p)
{
if (p & (PAGE_NOACCESS | PAGE_GUARD))
return false;
const DWORD writable =
PAGE_READWRITE | PAGE_WRITECOPY |
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
return (p & writable) != 0;
}
/* Check that the full range [addr, addr+len) is covered by readable pages. */
static bool IsRangeReadable(uintptr_t addr, uint32_t len)
{
uintptr_t end = addr + len;
uintptr_t cur = addr;
while (cur < end) {
MEMORY_BASIC_INFORMATION mbi{};
if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
return false;
if (mbi.State != MEM_COMMIT || !IsReadableProtect(mbi.Protect))
return false;
uintptr_t regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
cur = regionEnd;
}
return true;
}
static bool IsRangeWritable(uintptr_t addr, uint32_t len)
{
uintptr_t end = addr + len;
uintptr_t cur = addr;
while (cur < end) {
MEMORY_BASIC_INFORMATION mbi{};
if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
return false;
if (mbi.State != MEM_COMMIT || !IsWritableProtect(mbi.Protect))
return false;
uintptr_t regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
cur = regionEnd;
}
return true;
}
/* ── command handlers ─────────────────────────────────────────────── */
static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data)
{
auto* entries = reinterpret_cast<RcxRpcReadEntry*>(data);
for (uint32_t i = 0; i < hdr->requestCount; ++i) {
uint8_t* dest = data + entries[i].dataOffset;
uintptr_t src = static_cast<uintptr_t>(entries[i].address);
if (IsRangeReadable(src, entries[i].length)) {
memcpy(dest, reinterpret_cast<const void*>(src), entries[i].length);
} else {
memset(dest, 0, entries[i].length);
hdr->status = RCX_RPC_STATUS_PARTIAL;
}
/* SEH fallback (commented out, kept for reference):
__try {
memcpy(dest, reinterpret_cast<const void*>(src), entries[i].length);
} __except (EXCEPTION_EXECUTE_HANDLER) {
memset(dest, 0, entries[i].length);
hdr->status = RCX_RPC_STATUS_PARTIAL;
}
*/
}
hdr->responseCount = hdr->requestCount;
}
static void handle_write(RcxRpcHeader* hdr, uint8_t* data)
{
uintptr_t dst = static_cast<uintptr_t>(hdr->writeAddress);
if (IsRangeWritable(dst, hdr->writeLength)) {
memcpy(reinterpret_cast<void*>(dst), data, hdr->writeLength);
} else {
hdr->status = RCX_RPC_STATUS_ERROR;
}
/* SEH fallback (commented out, kept for reference):
__try {
memcpy(reinterpret_cast<void*>(dst), data, hdr->writeLength);
} __except (EXCEPTION_EXECUTE_HANDLER) {
hdr->status = RCX_RPC_STATUS_ERROR;
}
*/
}
static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
{
HANDLE hProc = GetCurrentProcess();
HMODULE mods[1024];
DWORD needed = 0;
if (!EnumProcessModules(hProc, mods, sizeof(mods), &needed)) {
hdr->status = RCX_RPC_STATUS_ERROR;
hdr->responseCount = 0;
return;
}
int count = (int)(needed / sizeof(HMODULE));
if (count > 1024) count = 1024;
uint32_t entryBytes = (uint32_t)(count * sizeof(RcxRpcModuleEntry));
uint32_t nameDataOff = entryBytes;
for (int i = 0; i < count; ++i) {
MODULEINFO mi{};
WCHAR modName[MAX_PATH];
GetModuleInformation(hProc, mods[i], &mi, sizeof(mi));
int nameLen = (int)GetModuleBaseNameW(hProc, mods[i], modName, MAX_PATH);
uint32_t nameBytes = (uint32_t)(nameLen * sizeof(WCHAR));
auto* entry = reinterpret_cast<RcxRpcModuleEntry*>(data + i * sizeof(RcxRpcModuleEntry));
entry->base = reinterpret_cast<uint64_t>(mi.lpBaseOfDll);
entry->size = static_cast<uint64_t>(mi.SizeOfImage);
entry->nameOffset = nameDataOff;
entry->nameLength = nameBytes;
if (nameDataOff + nameBytes <= RCX_RPC_DATA_SIZE) {
memcpy(data + nameDataOff, modName, nameBytes);
nameDataOff += nameBytes;
}
}
hdr->responseCount = (uint32_t)count;
hdr->totalDataUsed = nameDataOff;
hdr->status = RCX_RPC_STATUS_OK;
}
/* forward declaration */
void RcxPayloadCleanup();
/* ── timer callback (non-blocking poll) ───────────────────────────── */
static VOID CALLBACK RcxPollTimerCallback(PVOID, BOOLEAN)
{
if (!g_mappedView || !g_hReqEvent || !g_hRspEvent)
return;
/* non-blocking check: is there a pending request? */
DWORD rc = WaitForSingleObject(g_hReqEvent, 0);
if (rc != WAIT_OBJECT_0)
return;
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
hdr->status = RCX_RPC_STATUS_OK;
switch (static_cast<RcxRpcCommand>(hdr->command)) {
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
case RPC_CMD_WRITE: handle_write(hdr, data); break;
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
case RPC_CMD_PING: break;
case RPC_CMD_SHUTDOWN:
RcxPayloadCleanup();
return;
default:
hdr->status = RCX_RPC_STATUS_ERROR;
break;
}
SetEvent(g_hRspEvent);
}
/* ── cleanup ──────────────────────────────────────────────────────── */
void RcxPayloadCleanup()
{
if (!InterlockedCompareExchange(&g_initialized, 0, 0))
return;
/* stop the poll timer first */
if (g_hTimerQueue) {
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE); /* waits for callbacks */
g_hTimerQueue = nullptr;
g_hPollTimer = nullptr;
}
/* mark not-ready */
if (g_mappedView) {
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
}
if (g_mappedView) { UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; }
if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; }
if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; }
if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; }
InterlockedExchange(&g_initialized, 0);
}
/* ── init (called AFTER DllMain returns — safe for timer queues) ── */
extern "C" __declspec(dllexport)
bool RcxPayloadInit()
{
if (InterlockedCompareExchange(&g_initialized, 1, 0) != 0)
return true; /* already initialized */
uint32_t pid = GetCurrentProcessId();
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName);
if (!g_hShm) {
InterlockedExchange(&g_initialized, 0);
return false;
}
g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!g_mappedView) {
CloseHandle(g_hShm); g_hShm = nullptr;
InterlockedExchange(&g_initialized, 0);
return false;
}
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
hdr->version = RCX_RPC_VERSION;
/* image base from PEB */
{
uint64_t peb;
asm volatile("mov %%gs:0x60, %0" : "=r"(peb));
uint64_t ldr = *reinterpret_cast<uint64_t*>(peb + 0x18);
uint64_t firstLink = *reinterpret_cast<uint64_t*>(ldr + 0x10);
hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30);
}
g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName);
g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName);
if (!g_hReqEvent || !g_hRspEvent) {
RcxPayloadCleanup();
return false;
}
/* create dedicated timer queue + fast poll timer (10ms interval) */
g_hTimerQueue = CreateTimerQueue();
if (!g_hTimerQueue) {
RcxPayloadCleanup();
return false;
}
if (!CreateTimerQueueTimer(&g_hPollTimer, g_hTimerQueue,
RcxPollTimerCallback, nullptr,
0, /* start immediately */
10, /* 10ms repeat */
WT_EXECUTEDEFAULT)) {
RcxPayloadCleanup();
return false;
}
/* mark ready */
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
return true;
}
/* ── DllMain — minimal, no heavy work under loader lock ───────────── */
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_DETACH) {
RcxPayloadCleanup();
}
return TRUE;
}
#else
/* ===================================================================
* LINUX implementation
* =================================================================== */
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>
#include <time.h>
#include <signal.h>
/* ── globals ──────────────────────────────────────────────────────── */
static int g_shmFd = -1;
static void* g_mappedView = nullptr;
static sem_t* g_reqSem = SEM_FAILED;
static sem_t* g_rspSem = SEM_FAILED;
static pthread_t g_thread;
static volatile int g_shutdown = 0;
static volatile int g_threadRunning = 0;
static int g_memFd = -1; /* /proc/self/mem for safe access */
static char g_shmName[128];
static char g_reqName[128];
static char g_rspName[128];
/* ── safe memory access via /proc/self/mem ────────────────────────── */
static void safe_read(uint64_t addr, void* dest, uint32_t len, uint32_t* status)
{
ssize_t n = pread(g_memFd, dest, len, (off_t)addr);
if (n < (ssize_t)len) {
if (n > 0)
memset((uint8_t*)dest + n, 0, len - (uint32_t)n);
else
memset(dest, 0, len);
*status = RCX_RPC_STATUS_PARTIAL;
}
}
static void safe_write(uint64_t addr, const void* src, uint32_t len, uint32_t* status)
{
ssize_t n = pwrite(g_memFd, src, len, (off_t)addr);
if (n < (ssize_t)len)
*status = RCX_RPC_STATUS_ERROR;
}
/* ── command handlers ─────────────────────────────────────────────── */
static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data)
{
auto* entries = reinterpret_cast<RcxRpcReadEntry*>(data);
for (uint32_t i = 0; i < hdr->requestCount; ++i) {
uint8_t* dest = data + entries[i].dataOffset;
safe_read(entries[i].address, dest, entries[i].length, &hdr->status);
}
hdr->responseCount = hdr->requestCount;
}
static void handle_write(RcxRpcHeader* hdr, uint8_t* data)
{
safe_write(hdr->writeAddress, data, hdr->writeLength, &hdr->status);
}
static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
{
FILE* f = fopen("/proc/self/maps", "r");
if (!f) {
hdr->status = RCX_RPC_STATUS_ERROR;
hdr->responseCount = 0;
return;
}
/* first pass: collect unique file-backed mappings */
struct ModRange { uint64_t base; uint64_t end; char path[512]; };
static ModRange modules[512]; /* static to avoid large stack alloc */
int modCount = 0;
char line[1024];
while (fgets(line, sizeof(line), f) && modCount < 512) {
uint64_t start, end;
char perms[8] = {}, path[512] = {};
if (sscanf(line, "%lx-%lx %7s %*x %*x:%*x %*u %511[^\n]",
&start, &end, perms, path) < 4)
continue;
/* skip non-file / special mappings */
/* trim leading whitespace from path */
char* p = path;
while (*p == ' ' || *p == '\t') ++p;
if (*p != '/') continue;
if (strncmp(p, "/dev/", 5) == 0) continue;
if (strncmp(p, "/memfd:", 7) == 0) continue;
bool found = false;
for (int i = 0; i < modCount; ++i) {
if (strcmp(modules[i].path, p) == 0) {
if (start < modules[i].base) modules[i].base = start;
if (end > modules[i].end) modules[i].end = end;
found = true;
break;
}
}
if (!found) {
modules[modCount].base = start;
modules[modCount].end = end;
strncpy(modules[modCount].path, p, 511);
modules[modCount].path[511] = '\0';
++modCount;
}
}
fclose(f);
/* write entries + name strings into data region */
uint32_t entryBytes = (uint32_t)(modCount * sizeof(RcxRpcModuleEntry));
uint32_t nameDataOff = entryBytes;
for (int i = 0; i < modCount; ++i) {
const char* basename = strrchr(modules[i].path, '/');
basename = basename ? basename + 1 : modules[i].path;
uint32_t nameLen = (uint32_t)strlen(basename);
auto* entry = reinterpret_cast<RcxRpcModuleEntry*>(
data + (uint32_t)i * sizeof(RcxRpcModuleEntry));
entry->base = modules[i].base;
entry->size = modules[i].end - modules[i].base;
entry->nameOffset = nameDataOff;
entry->nameLength = nameLen;
if (nameDataOff + nameLen <= RCX_RPC_DATA_SIZE) {
memcpy(data + nameDataOff, basename, nameLen);
nameDataOff += nameLen;
}
}
hdr->responseCount = (uint32_t)modCount;
hdr->totalDataUsed = nameDataOff;
hdr->status = RCX_RPC_STATUS_OK;
}
/* ── server thread ────────────────────────────────────────────────── */
static void* server_thread_func(void*)
{
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
auto* data = reinterpret_cast<uint8_t*>(g_mappedView) + RCX_RPC_DATA_OFFSET;
__atomic_store_n(&hdr->payloadReady, 1, __ATOMIC_RELEASE);
while (!__atomic_load_n(&g_shutdown, __ATOMIC_ACQUIRE)) {
/* timed wait: 250ms */
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_nsec += 250000000;
if (ts.tv_nsec >= 1000000000) {
ts.tv_sec += 1;
ts.tv_nsec -= 1000000000;
}
int rc = sem_timedwait(g_reqSem, &ts);
if (rc != 0) {
if (errno == ETIMEDOUT) continue;
break;
}
hdr->status = RCX_RPC_STATUS_OK;
switch (static_cast<RcxRpcCommand>(hdr->command)) {
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
case RPC_CMD_WRITE: handle_write(hdr, data); break;
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
case RPC_CMD_PING: break;
case RPC_CMD_SHUTDOWN:
__atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE);
break;
default:
hdr->status = RCX_RPC_STATUS_ERROR;
break;
}
sem_post(g_rspSem);
if (static_cast<RcxRpcCommand>(hdr->command) == RPC_CMD_SHUTDOWN)
break;
}
__atomic_store_n(&hdr->payloadReady, 0, __ATOMIC_RELEASE);
__atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE);
return nullptr;
}
/* ── init / cleanup ───────────────────────────────────────────────── */
static void payload_cleanup()
{
__atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE);
/* wake the thread if blocked */
if (g_reqSem != SEM_FAILED) sem_post(g_reqSem);
if (__atomic_load_n(&g_threadRunning, __ATOMIC_ACQUIRE)) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 2;
pthread_timedjoin_np(g_thread, nullptr, &ts);
}
if (g_mappedView && g_mappedView != MAP_FAILED) {
munmap(g_mappedView, RCX_RPC_SHM_SIZE);
g_mappedView = nullptr;
}
if (g_shmFd >= 0) { close(g_shmFd); g_shmFd = -1; }
if (g_reqSem != SEM_FAILED) { sem_close(g_reqSem); g_reqSem = SEM_FAILED; }
if (g_rspSem != SEM_FAILED) { sem_close(g_rspSem); g_rspSem = SEM_FAILED; }
/* unlink named objects */
if (g_shmName[0]) shm_unlink(g_shmName);
if (g_reqName[0]) sem_unlink(g_reqName);
if (g_rspName[0]) sem_unlink(g_rspName);
if (g_memFd >= 0) { close(g_memFd); g_memFd = -1; }
}
__attribute__((constructor))
static void payload_init()
{
uint32_t pid = (uint32_t)getpid();
/* ── open /proc/self/mem for safe access ── */
g_memFd = open("/proc/self/mem", O_RDWR);
if (g_memFd < 0) return;
/* ── create main shared memory (PID-only naming) ── */
rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid);
rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid);
rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid);
g_shmFd = shm_open(g_shmName, O_CREAT | O_RDWR, 0600);
if (g_shmFd < 0) return;
if (ftruncate(g_shmFd, RCX_RPC_SHM_SIZE) != 0) {
close(g_shmFd); g_shmFd = -1; return;
}
g_mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, g_shmFd, 0);
if (g_mappedView == MAP_FAILED) {
g_mappedView = nullptr;
close(g_shmFd); g_shmFd = -1;
return;
}
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
hdr->version = RCX_RPC_VERSION;
/* image base from /proc/self/maps: first executable mapping */
{
FILE* f = fopen("/proc/self/maps", "r");
if (f) {
char line[256];
while (fgets(line, sizeof(line), f)) {
uint64_t start;
char perms[8] = {};
if (sscanf(line, "%lx-%*x %7s", &start, perms) >= 2 && perms[2] == 'x') {
hdr->imageBase = start;
break;
}
}
fclose(f);
}
}
/* ── create semaphores ── */
g_reqSem = sem_open(g_reqName, O_CREAT, 0600, 0);
g_rspSem = sem_open(g_rspName, O_CREAT, 0600, 0);
if (g_reqSem == SEM_FAILED || g_rspSem == SEM_FAILED) {
payload_cleanup();
return;
}
/* ── start server thread (it will set payloadReady = 1) ── */
__atomic_store_n(&g_threadRunning, 1, __ATOMIC_RELEASE);
if (pthread_create(&g_thread, nullptr, server_thread_func, nullptr) != 0) {
__atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE);
payload_cleanup();
return;
}
pthread_detach(g_thread);
}
__attribute__((destructor))
static void payload_deinit()
{
payload_cleanup();
}
#endif /* _WIN32 / linux */

View File

@@ -0,0 +1,115 @@
/*
* RCX RPC Protocol -- shared between plugin DLL and payload DLL/SO.
* No dependencies beyond standard C headers.
*/
#pragma once
#include <stdint.h>
#include <stdio.h>
#include <string.h>
/* ── constants ─────────────────────────────────────────────────────── */
#define RCX_RPC_VERSION 1
#define RCX_RPC_MAX_BATCH 256
#define RCX_RPC_SHM_SIZE (1024 * 1024) /* 1 MB */
#define RCX_RPC_HEADER_SIZE 4096
#define RCX_RPC_DATA_OFFSET RCX_RPC_HEADER_SIZE
#define RCX_RPC_DATA_SIZE (RCX_RPC_SHM_SIZE - RCX_RPC_DATA_OFFSET)
/* status codes */
#define RCX_RPC_STATUS_OK 0
#define RCX_RPC_STATUS_ERROR 1
#define RCX_RPC_STATUS_PARTIAL 2
/* ── commands ──────────────────────────────────────────────────────── */
#ifdef __cplusplus
enum RcxRpcCommand : uint32_t {
#else
typedef uint32_t RcxRpcCommand;
enum {
#endif
RPC_CMD_NONE = 0,
RPC_CMD_READ_BATCH = 1, /* batch read: N {address, length} pairs */
RPC_CMD_WRITE = 2, /* single write */
RPC_CMD_ENUM_MODULES = 3, /* enumerate loaded modules */
RPC_CMD_PING = 4, /* heartbeat */
RPC_CMD_SHUTDOWN = 5, /* graceful teardown */
};
/* ── wire structs (natural alignment, verified by static_assert) ─── */
struct RcxRpcReadEntry {
uint64_t address;
uint32_t length;
uint32_t dataOffset; /* offset into data region for response bytes */
};
struct RcxRpcModuleEntry {
uint64_t base;
uint64_t size;
uint32_t nameOffset; /* offset into data region, UTF-16 on Win, UTF-8 on Linux */
uint32_t nameLength; /* in bytes */
};
/*
* Header -- lives at shared-memory offset 0, padded to 4096 bytes.
*
* offset field
* ------ -----
* 0 version (4)
* 4 payloadReady (4)
* 8 command (4)
* 12 requestCount (4)
* 16 writeAddress (8)
* 24 writeLength (4)
* 28 status (4)
* 32 responseCount (4)
* 36 totalDataUsed (4)
* 40 imageBase (8) -- main module base from PEB / procfs
* 48 pointerSize (4) -- 4 for 32-bit, 8 for 64-bit payload
* 52 _pad[4044]
*/
struct RcxRpcHeader {
uint32_t version;
uint32_t payloadReady; /* payload sets to 1 after init */
uint32_t command; /* RcxRpcCommand */
uint32_t requestCount;
uint64_t writeAddress;
uint32_t writeLength;
uint32_t status; /* RCX_RPC_STATUS_* */
uint32_t responseCount;
uint32_t totalDataUsed;
uint64_t imageBase; /* main module base (PEB on Win, /proc on Linux) */
uint32_t pointerSize; /* 4 for 32-bit, 8 for 64-bit payload */
uint8_t _pad[RCX_RPC_HEADER_SIZE - 52];
};
/* ── name formatting helpers (PID-only, no nonce) ─────────────────── */
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_SHM_%u", pid);
#else
snprintf(buf, n, "/rcx_shm_%u", pid);
#endif
}
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_REQ_%u", pid);
#else
snprintf(buf, n, "/rcx_req_%u", pid);
#endif
}
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid) {
#ifdef _WIN32
snprintf(buf, n, "Local\\RCX_RSP_%u", pid);
#else
snprintf(buf, n, "/rcx_rsp_%u", pid);
#endif
}
#ifdef __cplusplus
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
#endif

View File

@@ -0,0 +1,593 @@
/*
* test_rpc_client -- connects to a running test_rpc_host (or spawns one),
* exercises every RPC command, and benchmarks throughput.
*
* Usage:
* test_rpc_client (auto-spawn host)
* test_rpc_client <pid> [testbuf_hex testlen]
*/
#include "../rcx_rpc_protocol.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include <chrono>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#else
# include <unistd.h>
# include <fcntl.h>
# include <sys/mman.h>
# include <semaphore.h>
# include <libgen.h>
# include <limits.h>
#endif
/* ══════════════════════════════════════════════════════════════════════
* Minimal standalone IPC client (no Qt, mirrors plugin's IpcClient)
* ══════════════════════════════════════════════════════════════════════ */
struct TestIpcClient {
#ifdef _WIN32
HANDLE hShm = nullptr;
HANDLE hReqEvent = nullptr;
HANDLE hRspEvent = nullptr;
#else
int shmFd = -1;
sem_t* reqSem = SEM_FAILED;
sem_t* rspSem = SEM_FAILED;
#endif
void* view = nullptr;
bool ok = false;
bool connect(uint32_t pid, int timeoutMs = 5000)
{
char shmName[128], reqName[128], rspName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
#ifdef _WIN32
ULONGLONG deadline = GetTickCount64() + (ULONGLONG)timeoutMs;
while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) {
if (GetTickCount64() >= deadline) return false;
Sleep(10);
}
view = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
if (!view) { CloseHandle(hShm); hShm = nullptr; return false; }
hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName);
hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName);
if (!hReqEvent || !hRspEvent) return false;
#else
auto start = std::chrono::steady_clock::now();
while (true) {
shmFd = shm_open(shmName, O_RDWR, 0);
if (shmFd >= 0) break;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeoutMs) return false;
usleep(10000);
}
view = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, shmFd, 0);
if (view == MAP_FAILED) { view = nullptr; close(shmFd); shmFd = -1; return false; }
reqSem = sem_open(reqName, 0);
rspSem = sem_open(rspName, 0);
if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) return false;
#endif
/* wait for payloadReady */
auto* hdr = (RcxRpcHeader*)view;
#ifdef _WIN32
while (!hdr->payloadReady) {
if (GetTickCount64() >= deadline) return false;
Sleep(5);
}
#else
while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
if (elapsed >= timeoutMs) return false;
usleep(5000);
}
#endif
ok = true;
return true;
}
void disconnect()
{
#ifdef _WIN32
if (view) { UnmapViewOfFile(view); view = nullptr; }
if (hShm) { CloseHandle(hShm); hShm = nullptr; }
if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; }
if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; }
#else
if (view) { munmap(view, RCX_RPC_SHM_SIZE); view = nullptr; }
if (shmFd >= 0) { close(shmFd); shmFd = -1; }
if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; }
if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; }
#endif
ok = false;
}
bool signalAndWait(int timeoutMs = 2000)
{
#ifdef _WIN32
SetEvent(hReqEvent);
return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0;
#else
sem_post(reqSem);
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeoutMs / 1000;
ts.tv_nsec += (timeoutMs % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
return sem_timedwait(rspSem, &ts) == 0;
#endif
}
/* ── RPC helpers ──────────────────────────────────────────────── */
bool rpc_ping()
{
auto* hdr = (RcxRpcHeader*)view;
hdr->command = RPC_CMD_PING;
hdr->status = RCX_RPC_STATUS_OK;
return signalAndWait();
}
bool rpc_read(uint64_t addr, void* buf, uint32_t len)
{
auto* hdr = (RcxRpcHeader*)view;
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_READ_BATCH;
hdr->requestCount = 1;
hdr->status = RCX_RPC_STATUS_OK;
auto* entry = (RcxRpcReadEntry*)data;
entry->address = addr;
entry->length = len;
entry->dataOffset = sizeof(RcxRpcReadEntry);
if (!signalAndWait()) return false;
memcpy(buf, data + entry->dataOffset, len);
return true;
}
bool rpc_read_batch(const uint64_t* addrs, const uint32_t* lens,
uint32_t count, uint8_t* outBuf)
{
auto* hdr = (RcxRpcHeader*)view;
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_READ_BATCH;
hdr->requestCount = count;
hdr->status = RCX_RPC_STATUS_OK;
/* lay out entries, then data offsets after all entries */
uint32_t entriesSize = count * (uint32_t)sizeof(RcxRpcReadEntry);
uint32_t dataOff = entriesSize;
for (uint32_t i = 0; i < count; ++i) {
auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry));
e->address = addrs[i];
e->length = lens[i];
e->dataOffset = dataOff;
dataOff += lens[i];
}
if (!signalAndWait()) return false;
/* copy out response data */
uint32_t off = 0;
for (uint32_t i = 0; i < count; ++i) {
auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry));
memcpy(outBuf + off, data + e->dataOffset, e->length);
off += e->length;
}
return true;
}
bool rpc_write(uint64_t addr, const void* buf, uint32_t len)
{
auto* hdr = (RcxRpcHeader*)view;
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_WRITE;
hdr->writeAddress = addr;
hdr->writeLength = len;
hdr->status = RCX_RPC_STATUS_OK;
memcpy(data, buf, len);
if (!signalAndWait()) return false;
return hdr->status == RCX_RPC_STATUS_OK;
}
struct ModInfo { uint64_t base; uint64_t size; char name[256]; };
int rpc_enum_modules(ModInfo* out, int maxOut)
{
auto* hdr = (RcxRpcHeader*)view;
auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET;
hdr->command = RPC_CMD_ENUM_MODULES;
hdr->status = RCX_RPC_STATUS_OK;
if (!signalAndWait()) return -1;
if (hdr->status != RCX_RPC_STATUS_OK) return -1;
int count = (int)hdr->responseCount;
if (count > maxOut) count = maxOut;
for (int i = 0; i < count; ++i) {
auto* entry = (RcxRpcModuleEntry*)(data + i * sizeof(RcxRpcModuleEntry));
out[i].base = entry->base;
out[i].size = entry->size;
#ifdef _WIN32
/* names are UTF-16 on Windows */
int wchars = (int)(entry->nameLength / sizeof(wchar_t));
WideCharToMultiByte(CP_UTF8, 0,
(const wchar_t*)(data + entry->nameOffset), wchars,
out[i].name, 255, nullptr, nullptr);
out[i].name[255] = '\0';
#else
int nLen = (int)entry->nameLength;
if (nLen > 255) nLen = 255;
memcpy(out[i].name, data + entry->nameOffset, nLen);
out[i].name[nLen] = '\0';
#endif
}
return count;
}
void rpc_shutdown()
{
auto* hdr = (RcxRpcHeader*)view;
hdr->command = RPC_CMD_SHUTDOWN;
hdr->status = RCX_RPC_STATUS_OK;
signalAndWait(500);
}
};
/* ══════════════════════════════════════════════════════════════════════
* Auto-spawn host
* ══════════════════════════════════════════════════════════════════════ */
#ifdef _WIN32
static HANDLE g_hostProcess = nullptr;
#else
static pid_t g_hostPid = 0;
#endif
static FILE* g_hostPipe = nullptr;
static bool spawn_host(uint32_t* outPid,
uint64_t* outTestBuf, uint32_t* outTestLen)
{
/* resolve path to test_rpc_host next to ourselves */
char cmd[2048];
#ifdef _WIN32
char exePath[MAX_PATH];
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
char* slash = strrchr(exePath, '\\');
if (!slash) slash = strrchr(exePath, '/');
if (slash) *(slash + 1) = '\0';
snprintf(cmd, sizeof(cmd), "\"%stest_rpc_host.exe\" autotest", exePath);
g_hostPipe = _popen(cmd, "r");
#else
char exePath[PATH_MAX];
ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
if (n <= 0) return false;
exePath[n] = '\0';
char* dir = dirname(exePath);
snprintf(cmd, sizeof(cmd), "%s/test_rpc_host autotest", dir);
g_hostPipe = popen(cmd, "r");
#endif
if (!g_hostPipe) {
fprintf(stderr, "ERROR: cannot spawn host: %s\n", cmd);
return false;
}
/* read READY line */
char line[512];
if (!fgets(line, sizeof(line), g_hostPipe)) {
fprintf(stderr, "ERROR: no output from host\n");
return false;
}
/* parse: READY pid=X testbuf=0xZ testlen=N */
unsigned long long tbuf = 0;
unsigned tlen = 0;
if (sscanf(line, "READY pid=%u testbuf=0x%llx testlen=%u",
outPid, &tbuf, &tlen) < 1) {
fprintf(stderr, "ERROR: cannot parse host output: %s\n", line);
return false;
}
*outTestBuf = (uint64_t)tbuf;
*outTestLen = (uint32_t)tlen;
return true;
}
static void cleanup_host()
{
if (g_hostPipe) {
#ifdef _WIN32
_pclose(g_hostPipe);
#else
pclose(g_hostPipe);
#endif
g_hostPipe = nullptr;
}
}
/* ══════════════════════════════════════════════════════════════════════
* Printing helpers
* ══════════════════════════════════════════════════════════════════════ */
static void print_pass(const char* name) { printf(" [PASS] %s\n", name); }
static void print_fail(const char* name) { printf(" [FAIL] %s\n", name); exit(1); }
/* ══════════════════════════════════════════════════════════════════════
* main
* ══════════════════════════════════════════════════════════════════════ */
int main(int argc, char** argv)
{
uint32_t pid = 0;
uint64_t testBuf = 0;
uint32_t testLen = 0;
bool autoMode = false;
if (argc >= 2) {
pid = (uint32_t)atoi(argv[1]);
if (argc >= 4) {
testBuf = (uint64_t)strtoull(argv[2], nullptr, 0);
testLen = (uint32_t)atoi(argv[3]);
}
} else {
autoMode = true;
printf("Auto-spawning test_rpc_host...\n");
if (!spawn_host(&pid, &testBuf, &testLen)) return 1;
}
printf("Connecting to PID=%u testbuf=0x%llx testlen=%u\n\n",
pid, (unsigned long long)testBuf, testLen);
/* ── connect ── */
TestIpcClient ipc;
if (!ipc.connect(pid)) {
fprintf(stderr, "ERROR: IPC connect failed\n");
if (autoMode) cleanup_host();
return 1;
}
printf("=== Functional Tests ===\n");
/* ── test: ping ── */
if (ipc.rpc_ping()) print_pass("Ping");
else print_fail("Ping");
/* ── test: enumerate modules ── */
TestIpcClient::ModInfo mods[512];
int modCount = ipc.rpc_enum_modules(mods, 512);
if (modCount > 0) {
printf(" [PASS] EnumModules (%d modules)\n", modCount);
printf(" first: %s base=0x%llx size=0x%llx\n",
mods[0].name,
(unsigned long long)mods[0].base,
(unsigned long long)mods[0].size);
} else {
print_fail("EnumModules");
}
/* ── test: read module header (MZ / ELF magic) ── */
if (modCount > 0) {
uint8_t header[4] = {};
if (ipc.rpc_read(mods[0].base, header, 4)) {
#ifdef _WIN32
if (header[0] == 'M' && header[1] == 'Z')
print_pass("ReadModuleHeader (MZ)");
else
print_fail("ReadModuleHeader (expected MZ)");
#else
if (header[0] == 0x7F && header[1] == 'E' &&
header[2] == 'L' && header[3] == 'F')
print_pass("ReadModuleHeader (ELF)");
else
print_fail("ReadModuleHeader (expected ELF)");
#endif
} else {
print_fail("ReadModuleHeader (read failed)");
}
}
/* ── test: read test buffer (known pattern) ── */
if (testBuf && testLen >= 4096) {
uint8_t buf[4096];
if (ipc.rpc_read(testBuf, buf, 4096)) {
bool good = true;
for (int i = 0; i < 4096; ++i) {
if (buf[i] != (uint8_t)(i & 0xFF)) { good = false; break; }
}
if (good) print_pass("ReadTestBuffer (4096 bytes, pattern verified)");
else print_fail("ReadTestBuffer (pattern mismatch)");
} else {
print_fail("ReadTestBuffer (read failed)");
}
}
/* ── test: write ── */
if (testBuf && testLen >= 16) {
uint8_t patch[4] = {0xDE, 0xAD, 0xBE, 0xEF};
if (ipc.rpc_write(testBuf, patch, 4)) {
uint8_t verify[4] = {};
ipc.rpc_read(testBuf, verify, 4);
if (memcmp(verify, patch, 4) == 0)
print_pass("Write + ReadBack (0xDEADBEEF)");
else
print_fail("Write + ReadBack (readback mismatch)");
} else {
print_fail("Write (write failed)");
}
}
/* ── test: batch read ── */
if (testBuf && testLen >= 8192) {
const uint32_t N = 4;
uint64_t addrs[N];
uint32_t lens[N];
for (uint32_t i = 0; i < N; ++i) {
addrs[i] = testBuf + i * 1024;
lens[i] = 1024;
}
uint8_t out[4096];
if (ipc.rpc_read_batch(addrs, lens, N, out)) {
print_pass("BatchRead (4 x 1024 bytes)");
} else {
print_fail("BatchRead");
}
}
printf("\n=== Benchmarks ===\n");
/* choose a valid address for benchmarking */
uint64_t benchAddr = testBuf ? testBuf : (modCount > 0 ? mods[0].base : 0);
if (!benchAddr) {
printf(" (no valid address for benchmarks, skipping)\n");
} else {
/* ── benchmark: single 4 KB reads ── */
{
const int ITERS = 10000;
const int PAGE = 4096;
uint8_t tmp[4096];
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERS; ++i)
ipc.rpc_read(benchAddr, tmp, PAGE);
auto t1 = std::chrono::high_resolution_clock::now();
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
double secs = us / 1e6;
double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0);
printf(" Single 4 KB reads:\n");
printf(" Iterations : %d\n", ITERS);
printf(" Total data : %.2f MB\n", totalMB);
printf(" Wall time : %.3f s\n", secs);
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
printf(" Avg latency: %.2f us/read\n", us / ITERS);
}
/* ── benchmark: single 64 B reads (pointer-chase-size) ── */
{
const int ITERS = 50000;
const int SZ = 64;
uint8_t tmp[64];
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERS; ++i)
ipc.rpc_read(benchAddr, tmp, SZ);
auto t1 = std::chrono::high_resolution_clock::now();
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
double secs = us / 1e6;
double totalKB = (double)ITERS * SZ / 1024.0;
printf(" Single 64 B reads (pointer-chase):\n");
printf(" Iterations : %d\n", ITERS);
printf(" Total data : %.2f KB\n", totalKB);
printf(" Wall time : %.3f s\n", secs);
printf(" Throughput : %.2f KB/s\n", totalKB / secs);
printf(" Avg latency: %.2f us/read\n", us / ITERS);
}
/* ── benchmark: batch read (50 x 4 KB, simulating refresh) ── */
{
const int ITERS = 2000;
const uint32_t BATCH = 50;
const uint32_t PAGE = 4096;
uint64_t addrs[BATCH];
uint32_t lens[BATCH];
for (uint32_t i = 0; i < BATCH; ++i) {
/* wrap within test buffer or module */
addrs[i] = benchAddr + (i * PAGE) % 65536;
lens[i] = PAGE;
}
/* allocate response buffer */
uint8_t* outBuf = (uint8_t*)malloc(BATCH * PAGE);
if (!outBuf) {
printf(" (batch malloc failed, skipping)\n");
} else {
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERS; ++i)
ipc.rpc_read_batch(addrs, lens, BATCH, outBuf);
auto t1 = std::chrono::high_resolution_clock::now();
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
double secs = us / 1e6;
double totalMB = (double)ITERS * BATCH * PAGE / (1024.0 * 1024.0);
printf(" Batch read (%u x %u B, simulating refresh):\n", BATCH, PAGE);
printf(" Iterations : %d\n", ITERS);
printf(" Total data : %.2f MB\n", totalMB);
printf(" Wall time : %.3f s\n", secs);
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
printf(" Avg latency: %.2f us/batch\n", us / ITERS);
printf(" Per-page : %.2f us/page\n", us / (ITERS * BATCH));
free(outBuf);
}
}
/* ── benchmark: write 4 KB ── */
if (testBuf && testLen >= 4096) {
const int ITERS = 10000;
const int PAGE = 4096;
uint8_t tmp[4096];
memset(tmp, 0x42, sizeof(tmp));
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERS; ++i)
ipc.rpc_write(testBuf, tmp, PAGE);
auto t1 = std::chrono::high_resolution_clock::now();
double us = (double)std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
double secs = us / 1e6;
double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0);
printf(" Write 4 KB:\n");
printf(" Iterations : %d\n", ITERS);
printf(" Total data : %.2f MB\n", totalMB);
printf(" Wall time : %.3f s\n", secs);
printf(" Throughput : %.2f MB/s\n", totalMB / secs);
printf(" Avg latency: %.2f us/write\n", us / ITERS);
}
}
/* ── shutdown ── */
printf("\nSending shutdown...\n");
ipc.rpc_shutdown();
ipc.disconnect();
if (autoMode) {
/* wait for host to exit */
#ifdef _WIN32
Sleep(500);
#else
usleep(500000);
#endif
cleanup_host();
}
printf("Done.\n");
return 0;
}

View File

@@ -0,0 +1,187 @@
/*
* test_rpc_host -- loads rcx_payload in-process, acts as the "target".
*
* Usage: test_rpc_host
*
* Prints a READY line (machine-parseable), then waits for the payload
* to shut down (RPC_CMD_SHUTDOWN from the client).
*/
#include "../rcx_rpc_protocol.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
#else
# include <unistd.h>
# include <dlfcn.h>
# include <fcntl.h>
# include <sys/mman.h>
# include <semaphore.h>
# include <libgen.h>
# include <limits.h>
#endif
/* ── Helpers ──────────────────────────────────────────────────────── */
static uint32_t current_pid()
{
#ifdef _WIN32
return (uint32_t)GetCurrentProcessId();
#else
return (uint32_t)getpid();
#endif
}
static void sleep_ms(int ms)
{
#ifdef _WIN32
Sleep((DWORD)ms);
#else
usleep((useconds_t)ms * 1000);
#endif
}
/* Resolve payload path relative to this executable */
static int payload_path(char* out, int outLen)
{
#ifdef _WIN32
char exePath[MAX_PATH];
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
char* slash = strrchr(exePath, '\\');
if (!slash) slash = strrchr(exePath, '/');
if (slash) *(slash + 1) = '\0';
snprintf(out, outLen, "%srcx_payload.dll", exePath);
#else
char exePath[PATH_MAX];
ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
if (n <= 0) return -1;
exePath[n] = '\0';
char* dir = dirname(exePath);
snprintf(out, outLen, "%s/rcx_payload.so", dir);
#endif
return 0;
}
/* Open the main shared memory (read-only, just to monitor payloadReady) */
static void* open_main_shm(uint32_t pid)
{
char shmName[128];
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
#ifdef _WIN32
HANDLE h = nullptr;
for (int i = 0; i < 500; ++i) {
h = OpenFileMappingA(FILE_MAP_READ, FALSE, shmName);
if (h) break;
sleep_ms(10);
}
if (!h) return nullptr;
void* v = MapViewOfFile(h, FILE_MAP_READ, 0, 0, sizeof(RcxRpcHeader));
return v;
#else
int fd = -1;
for (int i = 0; i < 500; ++i) {
fd = shm_open(shmName, O_RDONLY, 0);
if (fd >= 0) break;
sleep_ms(10);
}
if (fd < 0) return nullptr;
void* v = mmap(nullptr, sizeof(RcxRpcHeader), PROT_READ, MAP_SHARED, fd, 0);
close(fd);
return (v == MAP_FAILED) ? nullptr : v;
#endif
}
/* ── Test buffer (known pattern for client to verify reads/writes) ── */
static uint8_t g_testBuf[65536];
/* ── main ─────────────────────────────────────────────────────────── */
int main(int, char**)
{
uint32_t pid = current_pid();
/* fill test buffer with known pattern */
for (int i = 0; i < (int)sizeof(g_testBuf); ++i)
g_testBuf[i] = (uint8_t)(i & 0xFF);
/* load payload */
char plPath[1024];
if (payload_path(plPath, sizeof(plPath)) != 0) {
fprintf(stderr, "ERROR: cannot determine payload path\n");
return 1;
}
#ifdef _WIN32
HMODULE hPayload = LoadLibraryA(plPath);
if (!hPayload) {
fprintf(stderr, "ERROR: LoadLibrary(%s) failed (%lu)\n",
plPath, GetLastError());
return 1;
}
/* Call RcxPayloadInit() — DllMain is minimal, init must be explicit */
typedef bool (*RcxPayloadInitFn)();
auto pfnInit = (RcxPayloadInitFn)GetProcAddress(hPayload, "RcxPayloadInit");
if (!pfnInit || !pfnInit()) {
fprintf(stderr, "ERROR: RcxPayloadInit() failed or not found\n");
FreeLibrary(hPayload);
return 1;
}
#else
void* hPayload = dlopen(plPath, RTLD_NOW);
if (!hPayload) {
fprintf(stderr, "ERROR: dlopen(%s): %s\n", plPath, dlerror());
return 1;
}
#endif
/* open main shm and wait for payloadReady */
void* shmView = open_main_shm(pid);
if (!shmView) {
fprintf(stderr, "ERROR: failed to open main shared memory\n");
return 1;
}
RcxRpcHeader* hdr = (RcxRpcHeader*)shmView;
for (int i = 0; i < 500; ++i) {
if (hdr->payloadReady) break;
sleep_ms(10);
}
if (!hdr->payloadReady) {
fprintf(stderr, "ERROR: payload did not become ready\n");
return 1;
}
/* print READY line for the client to parse */
printf("READY pid=%u testbuf=0x%llx testlen=%u\n",
pid,
(unsigned long long)(uintptr_t)g_testBuf,
(unsigned)sizeof(g_testBuf));
fflush(stdout);
/* wait until payload shuts down */
while (hdr->payloadReady)
sleep_ms(100);
printf("Payload shut down, exiting.\n");
#ifdef _WIN32
/* give the timer queue a moment to drain */
Sleep(200);
FreeLibrary(hPayload);
if (shmView) UnmapViewOfFile(shmView);
#else
usleep(200000);
dlclose(hPayload);
if (shmView) munmap(shmView, sizeof(RcxRpcHeader));
#endif
return 0;
}

View File

@@ -0,0 +1,34 @@
cmake_minimum_required(VERSION 3.20)
project(WinDbgMemoryPlugin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
# Plugin sources
set(PLUGIN_SOURCES
WinDbgMemoryPlugin.h
WinDbgMemoryPlugin.cpp
)
# Create shared library (DLL)
add_library(WinDbgMemoryPlugin SHARED ${PLUGIN_SOURCES})
# Link Qt + DbgEng
target_link_libraries(WinDbgMemoryPlugin PRIVATE ${QT}::Widgets ole32)
# Include directories
target_include_directories(WinDbgMemoryPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src
)
# Output to Plugins folder
set_target_properties(WinDbgMemoryPlugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)

View File

@@ -0,0 +1,777 @@
#include "WinDbgMemoryPlugin.h"
#include <QStyle>
#include <QApplication>
#include <QMessageBox>
#include <QDialog>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QDebug>
#include <QClipboard>
#include <QGuiApplication>
#include <QFileDialog>
#include <QFileInfo>
#include <QSettings>
#ifdef _WIN32
#include <windows.h>
#include <initguid.h>
#include <dbgeng.h>
// dbgeng.dll is loaded dynamically — see loadDbgEngTools()
// The system dbgeng.dll (C:\Windows\System32) does not support remote
// connections (DebugConnect returns 0x8007053d). The full version lives
// in the Debugging Tools for Windows directory. We load it dynamically
// so the plugin works without requiring the debugger tools on PATH.
static const char* const kDbgToolsDirs[] = {
"C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64",
"C:\\Program Files\\Windows Kits\\10\\Debuggers\\x64",
};
static const char* const kSettingsKey = "WinDbgPlugin/DbgToolsDir";
typedef HRESULT (STDAPICALLTYPE *PFN_DebugConnect)(PCSTR, REFIID, PVOID*);
typedef HRESULT (STDAPICALLTYPE *PFN_DebugCreate)(REFIID, PVOID*);
static QString s_loadedDir;
static HMODULE s_hDbgEng = nullptr;
static HMODULE tryLoadFrom(const char* dir) {
SetDllDirectoryA(dir);
// Pre-load dependencies so the tools versions are used instead of
// the older System32 copies (e.g. dbghelp.dll without StackWalk2).
char path[MAX_PATH];
for (auto dep : {"dbghelp.dll", "dbgcore.dll", "symsrv.dll"}) {
snprintf(path, sizeof(path), "%s\\%s", dir, dep);
LoadLibraryA(path); // OK if missing
}
snprintf(path, sizeof(path), "%s\\dbgeng.dll", dir);
HMODULE h = LoadLibraryA(path);
if (h) {
s_loadedDir = QString::fromLocal8Bit(dir);
qDebug() << "[WinDbg] Loaded dbgeng.dll from" << dir;
}
return h;
}
static HMODULE loadDbgEngTools() {
if (s_hDbgEng) return s_hDbgEng;
// 1. Try user-configured path from settings
QSettings settings;
QString userDir = settings.value(kSettingsKey).toString();
if (!userDir.isEmpty()) {
s_hDbgEng = tryLoadFrom(userDir.toLocal8Bit().constData());
if (s_hDbgEng) return s_hDbgEng;
}
// 2. Try well-known install paths
for (auto dir : kDbgToolsDirs) {
s_hDbgEng = tryLoadFrom(dir);
if (s_hDbgEng) return s_hDbgEng;
}
SetDllDirectoryA(nullptr);
return nullptr;
}
static bool dbgToolsFound() {
loadDbgEngTools();
return s_hDbgEng != nullptr;
}
static PFN_DebugConnect getDebugConnect() {
static PFN_DebugConnect pfn = nullptr;
static bool tried = false;
if (!tried) {
tried = true;
HMODULE h = loadDbgEngTools();
if (h) pfn = (PFN_DebugConnect)GetProcAddress(h, "DebugConnect");
if (!pfn) qWarning() << "[WinDbg] DebugConnect not available — Debugging Tools not found";
}
return pfn;
}
static PFN_DebugCreate getDebugCreate() {
static PFN_DebugCreate pfn = nullptr;
static bool tried = false;
if (!tried) {
tried = true;
HMODULE h = loadDbgEngTools();
if (h) pfn = (PFN_DebugCreate)GetProcAddress(h, "DebugCreate");
if (!pfn) qWarning() << "[WinDbg] DebugCreate not available — Debugging Tools not found";
}
return pfn;
}
#endif
// ──────────────────────────────────────────────────────────────────────────
// Thread dispatch helper
// ──────────────────────────────────────────────────────────────────────────
template<typename Fn>
void WinDbgMemoryProvider::dispatchToOwner(Fn&& fn) const
{
if (!m_dispatcher) { fn(); return; }
if (QThread::currentThread() == m_dispatcher->thread()) {
// Already on the owning thread — call directly
fn();
} else {
// Marshal to the owning thread and block until done
QMetaObject::invokeMethod(m_dispatcher, std::forward<Fn>(fn),
Qt::BlockingQueuedConnection);
}
}
// ──────────────────────────────────────────────────────────────────────────
// WinDbgMemoryProvider implementation
// ──────────────────────────────────────────────────────────────────────────
WinDbgMemoryProvider::WinDbgMemoryProvider(const QString& target)
{
// Create a dedicated thread for all DbgEng COM operations.
// DbgEng's remote transport (TCP/named-pipe) is thread-affine — all
// calls must happen on the thread that called DebugConnect/DebugCreate.
// A private thread with its own event loop guarantees:
// 1. dispatchToOwner() works from any calling thread (main, thread-pool, etc.)
// 2. No deadlock — the DbgEng thread is never blocked by the caller
m_dbgThread = new QThread();
m_dbgThread->setObjectName(QStringLiteral("DbgEngThread"));
m_dbgThread->start();
m_dispatcher = new DbgEngDispatcher();
m_dispatcher->moveToThread(m_dbgThread);
#ifdef _WIN32
// Run all DbgEng initialization on the dedicated thread.
// BlockingQueuedConnection blocks us until the lambda finishes,
// so member variables written inside are visible after the call.
dispatchToOwner([this, &target]() {
HRESULT hr;
// COM must be initialized on this thread for DbgEng
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
qDebug() << "[WinDbg] Opening target:" << target
<< "on DbgEng thread" << QThread::currentThread();
if (target.startsWith("tcp:", Qt::CaseInsensitive)
|| target.startsWith("npipe:", Qt::CaseInsensitive))
{
// ── Remote: connect to existing WinDbg debug server ──
auto pfnConnect = getDebugConnect();
if (!pfnConnect) { qWarning() << "[WinDbg] Debugging Tools required for remote connections"; return; }
QByteArray connUtf8 = target.toUtf8();
qDebug() << "[WinDbg] DebugConnect:" << target;
hr = pfnConnect(connUtf8.constData(), IID_IDebugClient, (void**)&m_client);
qDebug() << "[WinDbg] DebugConnect hr=" << Qt::hex << (unsigned long)hr
<< "client=" << (void*)m_client;
if (FAILED(hr) || !m_client) {
qWarning() << "[WinDbg] DebugConnect FAILED hr=0x" << Qt::hex << (unsigned long)hr;
return;
}
m_isRemote = true;
}
else
{
// ── Local: create debug client for pid/dump ──
auto pfnCreate = getDebugCreate();
if (!pfnCreate) { qWarning() << "[WinDbg] Debugging Tools required"; return; }
hr = pfnCreate(IID_IDebugClient, (void**)&m_client);
qDebug() << "[WinDbg] DebugCreate hr=" << Qt::hex << (unsigned long)hr
<< "client=" << (void*)m_client;
if (FAILED(hr) || !m_client) {
qWarning() << "[WinDbg] DebugCreate FAILED hr=0x" << Qt::hex << (unsigned long)hr;
return;
}
if (target.startsWith("pid:", Qt::CaseInsensitive))
{
bool ok = false;
ULONG pid = target.mid(4).trimmed().toULong(&ok);
if (!ok || pid == 0) {
qWarning() << "[WinDbg] Invalid PID in target:" << target;
cleanup();
return;
}
qDebug() << "[WinDbg] Attaching to PID" << pid << "(non-invasive)";
hr = m_client->AttachProcess(
0, pid,
DEBUG_ATTACH_NONINVASIVE | DEBUG_ATTACH_NONINVASIVE_NO_SUSPEND);
qDebug() << "[WinDbg] AttachProcess hr=" << Qt::hex << (unsigned long)hr;
if (FAILED(hr)) {
qWarning() << "[WinDbg] AttachProcess FAILED";
cleanup();
return;
}
}
else if (target.startsWith("dump:", Qt::CaseInsensitive))
{
QString path = target.mid(5).trimmed();
QByteArray pathUtf8 = path.toUtf8();
qDebug() << "[WinDbg] Opening dump file:" << path;
hr = m_client->OpenDumpFile(pathUtf8.constData());
qDebug() << "[WinDbg] OpenDumpFile hr=" << Qt::hex << (unsigned long)hr;
if (FAILED(hr)) {
qWarning() << "[WinDbg] OpenDumpFile FAILED";
cleanup();
return;
}
}
else
{
qWarning() << "[WinDbg] Unknown target format:" << target;
cleanup();
return;
}
}
initInterfaces();
// WaitForEvent to finalize the attach/dump load.
// For remote connections the server session is already active — skip.
if (m_control && !m_isRemote) {
qDebug() << "[WinDbg] WaitForEvent...";
hr = m_control->WaitForEvent(0, 10000);
qDebug() << "[WinDbg] WaitForEvent hr=" << Qt::hex << (unsigned long)hr;
}
querySessionInfo();
});
#else
Q_UNUSED(target);
#endif
}
void WinDbgMemoryProvider::initInterfaces()
{
#ifdef _WIN32
if (!m_client) return;
HRESULT hr;
hr = m_client->QueryInterface(IID_IDebugDataSpaces, (void**)&m_dataSpaces);
qDebug() << "[WinDbg] IDebugDataSpaces hr=" << Qt::hex << (unsigned long)hr
<< "ptr=" << (void*)m_dataSpaces;
hr = m_client->QueryInterface(IID_IDebugDataSpaces2, (void**)&m_dataSpaces2);
qDebug() << "[WinDbg] IDebugDataSpaces2 hr=" << Qt::hex << (unsigned long)hr
<< "ptr=" << (void*)m_dataSpaces2;
hr = m_client->QueryInterface(IID_IDebugControl, (void**)&m_control);
qDebug() << "[WinDbg] IDebugControl hr=" << Qt::hex << (unsigned long)hr
<< "ptr=" << (void*)m_control;
hr = m_client->QueryInterface(IID_IDebugSymbols, (void**)&m_symbols);
qDebug() << "[WinDbg] IDebugSymbols hr=" << Qt::hex << (unsigned long)hr
<< "ptr=" << (void*)m_symbols;
if (!m_dataSpaces) {
qWarning() << "[WinDbg] No IDebugDataSpaces — cleaning up";
cleanup();
}
#endif
}
void WinDbgMemoryProvider::querySessionInfo()
{
#ifdef _WIN32
if (!m_client) return;
HRESULT hr;
if (m_control) {
ULONG debugClass = 0, debugQualifier = 0;
hr = m_control->GetDebuggeeType(&debugClass, &debugQualifier);
qDebug() << "[WinDbg] GetDebuggeeType hr=" << Qt::hex << (unsigned long)hr
<< "class=" << debugClass << "qualifier=" << debugQualifier;
if (SUCCEEDED(hr)) {
m_isLive = (debugQualifier < DEBUG_DUMP_SMALL);
m_writable = m_isLive;
}
}
// Query effective processor type for pointer size detection
if (m_control) {
ULONG procType = 0;
hr = m_control->GetEffectiveProcessorType(&procType);
if (SUCCEEDED(hr)) {
// IMAGE_FILE_MACHINE_I386 = 0x014C
if (procType == 0x014C)
m_pointerSize = 4;
qDebug() << "[WinDbg] EffectiveProcessorType=" << Qt::hex << procType
<< "pointerSize=" << m_pointerSize;
}
}
// WinDbg provides access to the entire virtual address space.
// Do NOT auto-select a module as base — let the user set their
// own base address. m_base stays 0 so the controller won't
// override tree.baseAddress.
m_name = m_isLive ? QStringLiteral("WinDbg (Live)")
: QStringLiteral("WinDbg (Dump)");
qDebug() << "[WinDbg] Ready. name=" << m_name
<< "isLive=" << m_isLive;
#endif
}
WinDbgMemoryProvider::~WinDbgMemoryProvider()
{
#ifdef _WIN32
// Dispatch COM cleanup to the DbgEng thread (thread-affine release)
if (m_dbgThread && m_dbgThread->isRunning() && m_dispatcher) {
dispatchToOwner([this]() {
if (m_client) {
if (m_isRemote)
m_client->EndSession(DEBUG_END_DISCONNECT);
else
m_client->DetachProcesses();
}
cleanup();
CoUninitialize();
});
} else {
// Thread not running — clean up directly (best-effort)
if (m_client) {
if (m_isRemote)
m_client->EndSession(DEBUG_END_DISCONNECT);
else
m_client->DetachProcesses();
}
cleanup();
}
#else
cleanup();
#endif
// Stop the dedicated thread
if (m_dbgThread) {
m_dbgThread->quit();
m_dbgThread->wait(3000);
delete m_dbgThread;
m_dbgThread = nullptr;
}
delete m_dispatcher;
m_dispatcher = nullptr;
}
void WinDbgMemoryProvider::cleanup()
{
#ifdef _WIN32
if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; }
if (m_control) { m_control->Release(); m_control = nullptr; }
if (m_dataSpaces2) { m_dataSpaces2->Release(); m_dataSpaces2 = nullptr; }
if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; }
if (m_client) { m_client->Release(); m_client = nullptr; }
#endif
}
bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
{
#ifdef _WIN32
if (!m_dataSpaces || len <= 0) return false;
bool result = false;
dispatchToOwner([&]() {
ULONG bytesRead = 0;
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
if (SUCCEEDED(hr) && (int)bytesRead >= len) {
result = true;
return;
}
// Partial or failed read — zero-fill remainder and log
memset((char*)buf + bytesRead, 0, len - bytesRead);
++m_readFailCount;
if (m_readFailCount <= 5 || (m_readFailCount % 100) == 0)
qDebug() << "[WinDbg] ReadVirtual FAILED addr=0x" << Qt::hex << addr
<< "len=" << Qt::dec << len
<< "hr=0x" << Qt::hex << (unsigned long)hr
<< "got=" << Qt::dec << bytesRead;
result = bytesRead > 0;
});
return result;
#else
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
return false;
#endif
}
bool WinDbgMemoryProvider::write(uint64_t addr, const void* buf, int len)
{
#ifdef _WIN32
if (!m_dataSpaces || !m_writable || len <= 0) return false;
bool result = false;
dispatchToOwner([&]() {
ULONG bytesWritten = 0;
HRESULT hr = m_dataSpaces->WriteVirtual(addr, const_cast<void*>(buf),
(ULONG)len, &bytesWritten);
result = SUCCEEDED(hr) && bytesWritten == (ULONG)len;
});
return result;
#else
Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len);
return false;
#endif
}
int WinDbgMemoryProvider::size() const
{
#ifdef _WIN32
return m_dataSpaces ? 0x10000 : 0;
#else
return 0;
#endif
}
bool WinDbgMemoryProvider::isReadable(uint64_t /*addr*/, int len) const
{
#ifdef _WIN32
// DbgEng's ReadVirtual can read any mapped virtual address.
return m_dataSpaces != nullptr && len >= 0;
#else
return false;
#endif
}
QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
{
#ifdef _WIN32
if (!m_symbols) return {};
QString result;
dispatchToOwner([&]() {
char nameBuf[512] = {};
ULONG nameSize = 0;
ULONG64 displacement = 0;
HRESULT hr = m_symbols->GetNameByOffset(addr, nameBuf, sizeof(nameBuf),
&nameSize, &displacement);
if (SUCCEEDED(hr) && nameSize > 0) {
result = QString::fromUtf8(nameBuf);
if (displacement > 0)
result += QStringLiteral("+0x%1").arg(displacement, 0, 16);
}
});
return result;
#else
Q_UNUSED(addr);
return {};
#endif
}
QVector<rcx::MemoryRegion> WinDbgMemoryProvider::enumerateRegions() const
{
QVector<rcx::MemoryRegion> regions;
#ifdef _WIN32
if (!m_dataSpaces) return regions;
// Enumerate modules — used for tagging (user-mode) or as the primary
// source of regions (kernel-mode, where QueryVirtual is unavailable).
struct ModInfo { uint64_t base; uint64_t size; QString name; };
QVector<ModInfo> modules;
if (m_symbols) {
dispatchToOwner([&]() {
ULONG loaded = 0, unloaded = 0;
if (FAILED(m_symbols->GetNumberModules(&loaded, &unloaded)))
return;
for (ULONG i = 0; i < loaded; i++) {
ULONG64 modBase = 0;
if (FAILED(m_symbols->GetModuleByIndex(i, &modBase)))
continue;
DEBUG_MODULE_PARAMETERS params = {};
if (FAILED(m_symbols->GetModuleParameters(1, &modBase, 0, &params)))
continue;
char nameBuf[256] = {};
ULONG nameSize = 0;
m_symbols->GetModuleNames(i, 0,
nullptr, 0, nullptr,
nameBuf, sizeof(nameBuf), &nameSize,
nullptr, 0, nullptr);
ModInfo mi;
mi.base = modBase;
mi.size = params.Size;
mi.name = QString::fromUtf8(nameBuf);
modules.append(mi);
}
});
}
// Try QueryVirtual first (user-mode debugging / user-mode dumps).
// MSDN: "This method is not available in kernel-mode debugging."
if (m_dataSpaces2) {
dispatchToOwner([&]() {
ULONG64 addr = 0;
int safety = 0;
constexpr int kMaxRegions = 500000;
while (safety++ < kMaxRegions) {
MEMORY_BASIC_INFORMATION64 mbi = {};
HRESULT hr = m_dataSpaces2->QueryVirtual(addr, &mbi);
if (FAILED(hr))
break;
if (mbi.State == MEM_COMMIT &&
!(mbi.Protect & PAGE_NOACCESS) &&
!(mbi.Protect & PAGE_GUARD))
{
rcx::MemoryRegion region;
region.base = mbi.BaseAddress;
region.size = mbi.RegionSize;
region.readable = true;
region.writable = (mbi.Protect & PAGE_READWRITE) ||
(mbi.Protect & PAGE_WRITECOPY) ||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
region.executable = (mbi.Protect & PAGE_EXECUTE) ||
(mbi.Protect & PAGE_EXECUTE_READ) ||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
for (const auto& mod : modules) {
if (region.base >= mod.base && region.base < mod.base + mod.size) {
region.moduleName = mod.name;
break;
}
}
regions.append(region);
}
ULONG64 next = mbi.BaseAddress + mbi.RegionSize;
if (next <= addr) break;
addr = next;
}
});
}
// Fallback for kernel-mode debugging: QueryVirtual is unavailable,
// so use loaded modules as scannable regions. Each module image
// becomes one region — the scanner reads through module code/data.
if (regions.isEmpty() && !modules.isEmpty()) {
for (const auto& mod : modules) {
if (mod.size == 0) continue;
rcx::MemoryRegion region;
region.base = mod.base;
region.size = mod.size;
region.readable = true;
region.writable = false;
region.executable = true;
region.moduleName = mod.name;
regions.append(region);
}
}
#endif
return regions;
}
// ──────────────────────────────────────────────────────────────────────────
// WinDbgMemoryPlugin implementation
// ──────────────────────────────────────────────────────────────────────────
QIcon WinDbgMemoryPlugin::Icon() const
{
return qApp->style()->standardIcon(QStyle::SP_DriveNetIcon);
}
bool WinDbgMemoryPlugin::canHandle(const QString& target) const
{
return target.startsWith("tcp:", Qt::CaseInsensitive)
|| target.startsWith("npipe:", Qt::CaseInsensitive)
|| target.startsWith("pid:", Qt::CaseInsensitive)
|| target.startsWith("dump:", Qt::CaseInsensitive);
}
std::unique_ptr<rcx::Provider> WinDbgMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
{
auto provider = std::make_unique<WinDbgMemoryProvider>(target);
if (!provider->isValid())
{
if (errorMsg) {
if (target.startsWith("tcp:", Qt::CaseInsensitive)
|| target.startsWith("npipe:", Qt::CaseInsensitive))
*errorMsg = QString("Failed to connect to debug server.\n\n"
"Target: %1\n\n"
"Make sure WinDbg is running with a matching .server command\n"
"(e.g. .server tcp:port=5056) and the port/pipe is reachable.")
.arg(target);
else if (target.startsWith("pid:", Qt::CaseInsensitive))
*errorMsg = QString("Failed to attach to process.\n\n"
"Target: %1\n\n"
"Make sure the process is running and you have "
"sufficient privileges (try Run as Administrator).")
.arg(target);
else
*errorMsg = QString("Failed to open dump file.\n\n"
"Target: %1\n\n"
"Make sure the file exists and is a valid dump.")
.arg(target);
}
return nullptr;
}
return provider;
}
uint64_t WinDbgMemoryPlugin::getInitialBaseAddress(const QString& target) const
{
Q_UNUSED(target);
return 0;
}
bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
{
QDialog dlg(parent);
dlg.setWindowTitle("WinDbg Settings");
dlg.resize(480, 360);
QPalette dlgPal = qApp->palette();
dlg.setPalette(dlgPal);
dlg.setAutoFillBackground(true);
auto* layout = new QVBoxLayout(&dlg);
QColor editBg = dlgPal.window().color().darker(115);
QString editSS = QStringLiteral(
"QLineEdit { background: %1; color: %2; border: 1px solid %3;"
" border-radius: 3px; padding: 4px 6px; }")
.arg(editBg.name(),
dlgPal.color(QPalette::Text).name(),
dlgPal.color(QPalette::Mid).name());
layout->addWidget(new QLabel(
"Connect to a running WinDbg debug server.\n"
"In WinDbg, run: .server tcp:port=5056\n\n"
"Non-invasive debug and dump files only.\n"
"Execution control (bp, g, t, p) is not supported.\n"
"WinDbg Classic is recommended."));
layout->addSpacing(8);
layout->addWidget(new QLabel("Connection string:"));
auto* connEdit = new QLineEdit;
connEdit->setPlaceholderText("tcp:Port=5056,Server=127.0.0.1");
connEdit->setText("tcp:Port=5056,Server=127.0.0.1");
connEdit->setStyleSheet(editSS);
layout->addWidget(connEdit);
layout->addSpacing(4);
layout->addWidget(new QLabel("Run one of these in WinDbg first:"));
auto addExample = [&](const QString& text) {
auto* row = new QHBoxLayout;
auto* label = new QLabel(text);
QPalette lp = dlgPal;
lp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
label->setPalette(lp);
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
row->addWidget(label, 1);
auto* copyBtn = new QPushButton("Copy");
copyBtn->setFixedWidth(50);
copyBtn->setToolTip("Copy to clipboard");
QObject::connect(copyBtn, &QPushButton::clicked, [text]() {
QGuiApplication::clipboard()->setText(text);
});
row->addWidget(copyBtn);
layout->addLayout(row);
};
addExample(".server tcp:port=5056");
addExample(".server npipe:pipe=reclass");
// ── Debugger Tools status ──
layout->addSpacing(8);
#ifdef _WIN32
bool found = dbgToolsFound();
auto* toolsRow = new QHBoxLayout;
auto* toolsLabel = new QLabel;
if (found) {
toolsLabel->setText(QStringLiteral("Debugging Tools: %1").arg(s_loadedDir));
QPalette tp = dlgPal;
tp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
toolsLabel->setPalette(tp);
} else {
toolsLabel->setText("Debugging Tools: not found");
QPalette tp = dlgPal;
tp.setColor(QPalette::WindowText, QColor(220, 120, 80));
toolsLabel->setPalette(tp);
}
toolsLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
toolsRow->addWidget(toolsLabel, 1);
auto* browseBtn = new QPushButton("Browse...");
browseBtn->setFixedWidth(70);
browseBtn->setToolTip("Locate Debugging Tools for Windows directory (contains dbgeng.dll)");
QObject::connect(browseBtn, &QPushButton::clicked, [&dlg, toolsLabel, &dlgPal]() {
QString dir = QFileDialog::getExistingDirectory(&dlg,
"Locate Debugging Tools for Windows",
"C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers");
if (dir.isEmpty()) return;
QString dllPath = dir + "/dbgeng.dll";
if (!QFileInfo::exists(dllPath)) {
QMessageBox::warning(&dlg, "Not Found",
"dbgeng.dll was not found in that directory.\n"
"Select the folder containing dbgeng.dll\n"
"(e.g. Debuggers\\x64).");
return;
}
QSettings settings;
settings.setValue(kSettingsKey, dir);
// Force reload on next use
s_hDbgEng = nullptr;
s_loadedDir.clear();
if (dbgToolsFound()) {
toolsLabel->setText(QStringLiteral("Debugging Tools: %1").arg(s_loadedDir));
QPalette tp = dlgPal;
tp.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
toolsLabel->setPalette(tp);
}
});
toolsRow->addWidget(browseBtn);
layout->addLayout(toolsRow);
if (!found) {
auto* note = new QLabel(
"The system dbgeng.dll does not support remote connections.\n"
"Install Debugging Tools for Windows or use Browse to locate them.");
QPalette np = dlgPal;
np.setColor(QPalette::WindowText, dlgPal.color(QPalette::Disabled, QPalette::WindowText));
note->setPalette(np);
note->setWordWrap(true);
layout->addWidget(note);
}
#endif
layout->addStretch();
auto* btnLayout = new QHBoxLayout;
btnLayout->addStretch();
auto* okBtn = new QPushButton("OK");
auto* cancelBtn = new QPushButton("Cancel");
btnLayout->addWidget(okBtn);
btnLayout->addWidget(cancelBtn);
layout->addLayout(btnLayout);
QObject::connect(okBtn, &QPushButton::clicked, &dlg, &QDialog::accept);
QObject::connect(cancelBtn, &QPushButton::clicked, &dlg, &QDialog::reject);
if (dlg.exec() != QDialog::Accepted)
return false;
QString conn = connEdit->text().trimmed();
if (conn.isEmpty()) return false;
*target = conn;
return true;
}
// ──────────────────────────────────────────────────────────────────────────
// Plugin factory
// ──────────────────────────────────────────────────────────────────────────
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{
return new WinDbgMemoryPlugin();
}

View File

@@ -0,0 +1,127 @@
#pragma once
#include "../../src/iplugin.h"
#include "../../src/core.h"
#include <cstdint>
#include <QObject>
#include <QThread>
// Forward declarations for DbgEng COM interfaces
struct IDebugClient;
struct IDebugDataSpaces;
struct IDebugDataSpaces2;
struct IDebugControl;
struct IDebugSymbols;
/**
* WinDbg memory provider
*
* Uses DbgEng to read memory from:
* - An existing WinDbg debug server via DebugConnect (tcp/npipe)
* - A live process by PID via DebugCreate (non-invasive attach)
* - A crash dump (.dmp) file via DebugCreate
*
* Target string format:
* "tcp:Port=5055,Server=localhost" - connect to WinDbg debug server (TCP)
* "npipe:Pipe=name,Server=localhost" - connect to WinDbg debug server (named pipe)
* "pid:1234" - attach to process 1234
* "dump:C:/path/to/file.dmp" - open dump file
*
* Threading: All DbgEng COM calls are dispatched to the thread that created
* the connection (DebugConnect/DebugCreate). This is required because the
* remote transport (TCP/named-pipe) binds to the creating thread. The
* controller's background refresh threads call read() which transparently
* marshals to the owning thread via BlockingQueuedConnection.
*/
// Helper QObject that lives on the DbgEng-owning thread.
// Used as a target for QMetaObject::invokeMethod to marshal calls.
class DbgEngDispatcher : public QObject {
Q_OBJECT
public:
using QObject::QObject;
};
class WinDbgMemoryProvider : public rcx::Provider
{
public:
/// Create a provider from a target string
WinDbgMemoryProvider(const QString& target);
~WinDbgMemoryProvider() override;
// Required overrides
bool read(uint64_t addr, void* buf, int len) const override;
int size() const override;
// Optional overrides
bool isReadable(uint64_t addr, int len) const override;
bool write(uint64_t addr, const void* buf, int len) override;
bool isWritable() const override { return m_writable; }
QString name() const override { return m_name; }
QString kind() const override { return QStringLiteral("WinDbg"); }
QString getSymbol(uint64_t addr) const override;
QVector<rcx::MemoryRegion> enumerateRegions() const override;
bool isLive() const override { return m_isLive; }
uint64_t base() const override { return m_base; }
int pointerSize() const override { return m_pointerSize; }
private:
void initInterfaces(); // get IDebugDataSpaces/Control/Symbols from client
void querySessionInfo(); // determine live/dump, writable, name, base
void cleanup();
// Marshal a lambda to the DbgEng-owning thread. If already on that
// thread, calls directly. Otherwise blocks via QueuedConnection.
template<typename Fn>
void dispatchToOwner(Fn&& fn) const;
IDebugClient* m_client = nullptr;
IDebugDataSpaces* m_dataSpaces = nullptr;
IDebugDataSpaces2* m_dataSpaces2 = nullptr;
IDebugControl* m_control = nullptr;
IDebugSymbols* m_symbols = nullptr;
QString m_name;
uint64_t m_base = 0;
bool m_isLive = false;
bool m_writable = false;
int m_pointerSize = 8;
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
mutable int m_readFailCount = 0;
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe
// transport is thread-affine — all calls must happen on the thread
// that called DebugConnect. A private thread with its own event loop
// ensures dispatchToOwner() works from any calling thread (including
// QtConcurrent workers and the main/GUI thread) without deadlock.
QThread* m_dbgThread = nullptr;
DbgEngDispatcher* m_dispatcher = nullptr;
};
/**
* Plugin that provides WinDbgMemoryProvider
*
* Uses DbgEng to read memory via:
* - Remote connection to an existing WinDbg debug server (tcp/npipe)
* - Local non-invasive attach to a live process (pid)
* - Local crash dump file (dump)
*/
class WinDbgMemoryPlugin : public IProviderPlugin
{
public:
std::string Name() const override { return "WinDbg Memory"; }
std::string Version() const override { return "2.0.0"; }
std::string Author() const override { return "Reclass"; }
std::string Description() const override { return "Read memory via DbgEng (live process attach or crash dump)"; }
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
QIcon Icon() const override;
bool canHandle(const QString& target) const override;
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
uint64_t getInitialBaseAddress(const QString& target) const override;
bool selectTarget(QWidget* parent, QString* target) override;
};
// Plugin export
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -1,4 +1,4 @@
# PowerShell script to build ReclassX
# PowerShell script to build Reclass
# Automatically detects Qt installation and configures build environment
#Requires -Version 5.1
@@ -283,9 +283,10 @@ function Find-MinGWDirectory {
$toolsDir = Join-Path $qtRoot "Tools"
if (Test-Path $toolsDir) {
# Prefer GCC-based MinGW (has g++.exe); exclude llvm-mingw. Prefer 64-bit, then newest.
$mingwToolDirs = Get-ChildItem -Path $toolsDir -Directory -ErrorAction SilentlyContinue | Where-Object {
$_.Name -match 'mingw'
}
$_.Name -match '^mingw\d+_\d+$'
} | Sort-Object -Property @{ Expression = { if ($_.Name -match '_64$') { 1 } else { 0 } }; Descending = $true }, Name -Descending
foreach ($dir in $mingwToolDirs) {
$testBin = Join-Path $dir.FullName "bin\g++.exe"
@@ -303,7 +304,7 @@ function Find-MinGWDirectory {
# ──────────────────────────────────────────────────────────────────────────────
Write-ColorOutput "`n========================================" Cyan
Write-ColorOutput "ReclassX Build Script" Cyan
Write-ColorOutput "Reclass Build Script" Cyan
Write-ColorOutput "========================================`n" Cyan
# Get script directory and project root
@@ -426,7 +427,7 @@ try {
Write-ColorOutput "`nCMake configuration completed successfully.`n" Green
# Build
Write-ColorOutput "Building ReclassX..." Cyan
Write-ColorOutput "Building Reclass..." Cyan
$cores = (Get-CimInstance -ClassName Win32_Processor).NumberOfLogicalProcessors
if (-not $cores -or $cores -lt 1) {
@@ -445,8 +446,8 @@ try {
# Find executable
Write-ColorOutput "Locating executable..." Cyan
$exePaths = @(
(Join-Path $buildDir "ReclassX.exe"),
(Join-Path $buildDir "$BuildType\ReclassX.exe")
(Join-Path $buildDir "Reclass.exe"),
(Join-Path $buildDir "$BuildType\Reclass.exe")
)
$exePath = $null
@@ -477,7 +478,7 @@ try {
# Count deployed files
$deployedFiles = Get-ChildItem -Path $exeDir -Recurse -File | Where-Object {
$_.Name -ne "ReclassX.exe" -and $_.Extension -match '\.(dll|qm)$'
$_.Name -ne "Reclass.exe" -and $_.Extension -match '\.(dll|qm)$'
}
if ($deployedFiles) {
Write-ColorOutput "Deployed $($deployedFiles.Count) Qt dependency files." Gray
@@ -491,7 +492,7 @@ try {
Write-ColorOutput "Application may not run without Qt DLLs in PATH" Yellow
}
} else {
Write-ColorOutput "WARNING: Could not locate ReclassX.exe" Yellow
Write-ColorOutput "WARNING: Could not locate Reclass.exe" Yellow
}
} catch {
@@ -507,5 +508,5 @@ Write-ColorOutput "========================================`n" Cyan
if ($exePath) {
Write-ColorOutput "Run the application with:" White
Write-ColorOutput " .\build\ReclassX.exe`n" Cyan
Write-ColorOutput " .\build\Reclass.exe`n" Cyan
}

168
scripts/build_macos.sh Executable file
View File

@@ -0,0 +1,168 @@
#!/usr/bin/env bash
set -euo pipefail
print_help() {
cat <<'EOF'
Reclass macOS Build Script
Usage:
./scripts/build_macos.sh [options]
Options:
--qt-dir <path> Qt installation prefix (e.g. /opt/homebrew/opt/qt)
--build-type <type> Release | Debug | RelWithDebInfo | MinSizeRel (default: Release)
--build-dir <path> Build directory (default: <repo>/build)
--generator <name> CMake generator (default: Ninja if available)
--clean Remove build directory before configuring
--rebuild Clean then build
--package Run macdeployqt and create a zip
--tests Run ctest after build
-h, --help Show this help
Notes:
- You can set QTDIR or Qt6_DIR in your environment instead of --qt-dir.
- If Qt is installed via Homebrew, the script will try to detect it.
EOF
}
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
project_root="$(cd "${script_dir}/.." && pwd)"
qt_dir=""
build_type="Release"
build_dir="${project_root}/build"
generator=""
do_clean="false"
do_package="false"
do_tests="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--qt-dir)
qt_dir="${2:-}"
shift 2
;;
--build-type)
build_type="${2:-}"
shift 2
;;
--build-dir)
build_dir="${2:-}"
shift 2
;;
--generator)
generator="${2:-}"
shift 2
;;
--clean)
do_clean="true"
shift
;;
--rebuild)
do_clean="true"
shift
;;
--package)
do_package="true"
shift
;;
--tests)
do_tests="true"
shift
;;
-h|--help)
print_help
exit 0
;;
*)
echo "Unknown argument: $1" >&2
print_help
exit 1
;;
esac
done
if [[ -z "${qt_dir}" ]]; then
if [[ -n "${QTDIR:-}" ]]; then
qt_dir="${QTDIR}"
elif [[ -n "${Qt6_DIR:-}" ]]; then
qt_dir="${Qt6_DIR}"
elif command -v brew >/dev/null 2>&1; then
if brew --prefix qt >/dev/null 2>&1; then
qt_dir="$(brew --prefix qt)"
fi
fi
fi
if ! command -v cmake >/dev/null 2>&1; then
echo "ERROR: cmake not found. Install CMake and try again." >&2
exit 1
fi
if [[ -z "${generator}" ]]; then
if command -v ninja >/dev/null 2>&1; then
generator="Ninja"
fi
fi
if [[ "${do_clean}" == "true" && -d "${build_dir}" ]]; then
echo "Cleaning build directory: ${build_dir}"
rm -rf "${build_dir}"
fi
mkdir -p "${build_dir}"
cmake_args=(
-S "${project_root}"
-B "${build_dir}"
-DCMAKE_BUILD_TYPE="${build_type}"
)
if [[ -n "${generator}" ]]; then
cmake_args+=(-G "${generator}")
fi
if [[ -n "${qt_dir}" ]]; then
export PATH="${qt_dir}/bin:${PATH}"
cmake_args+=(-DCMAKE_PREFIX_PATH="${qt_dir}")
fi
echo "Configuring..."
cmake "${cmake_args[@]}"
echo "Building..."
cmake --build "${build_dir}" --config "${build_type}"
if [[ "${do_tests}" == "true" ]]; then
echo "Running tests..."
ctest --test-dir "${build_dir}" --output-on-failure -C "${build_type}"
fi
if [[ "${do_package}" == "true" ]]; then
app_path="${build_dir}/Reclass.app"
if [[ ! -d "${app_path}" ]]; then
echo "ERROR: ${app_path} not found. Build may have failed." >&2
exit 1
fi
macdeployqt_bin=""
if [[ -n "${qt_dir}" && -x "${qt_dir}/bin/macdeployqt" ]]; then
macdeployqt_bin="${qt_dir}/bin/macdeployqt"
elif command -v macdeployqt >/dev/null 2>&1; then
macdeployqt_bin="$(command -v macdeployqt)"
fi
if [[ -z "${macdeployqt_bin}" ]]; then
echo "ERROR: macdeployqt not found. Ensure Qt is installed and in PATH." >&2
exit 1
fi
echo "Running macdeployqt..."
"${macdeployqt_bin}" "${app_path}" -always-overwrite
arch="$(uname -m)"
zip_name="Reclass-macos-${arch}-qt6.zip"
echo "Creating zip: ${zip_name}"
ditto -c -k --sequesterRsrc --keepParent "${app_path}" "${build_dir}/${zip_name}"
echo "Packaged: ${build_dir}/${zip_name}"
fi

View File

@@ -1,4 +1,4 @@
# PowerShell script to build QScintilla static library for ReclassX
# PowerShell script to build QScintilla static library for Reclass
# This script checks for Qt installation, prompts if missing, and builds QScintilla
#Requires -Version 5.1
@@ -272,7 +272,7 @@ function Find-MakeCommand {
# ──────────────────────────────────────────────────────────────────────────────
Write-ColorOutput "`n========================================" Cyan
Write-ColorOutput "QScintilla Build Script for ReclassX" Cyan
Write-ColorOutput "QScintilla Build Script for Reclass" Cyan
Write-ColorOutput "========================================`n" Cyan
# Get script directory and project root
@@ -318,10 +318,10 @@ $qtRoot = Split-Path (Split-Path $selectedQtDir -Parent) -Parent
$toolsDir = Join-Path $qtRoot "Tools"
if (Test-Path $toolsDir) {
# Look for MinGW tools directory
# Prefer GCC-based MinGW (has g++.exe); exclude llvm-mingw. Prefer 64-bit, then newest.
$mingwToolDirs = Get-ChildItem -Path $toolsDir -Directory -ErrorAction SilentlyContinue | Where-Object {
$_.Name -match 'mingw'
}
$_.Name -match '^mingw\d+_\d+$'
} | Sort-Object -Property @{ Expression = { if ($_.Name -match '_64$') { 1 } else { 0 } }; Descending = $true }, Name -Descending
foreach ($dir in $mingwToolDirs) {
$testBin = Join-Path $dir.FullName "bin\g++.exe"
@@ -423,7 +423,7 @@ try {
Write-Host " - $($lib.Name) ($sizeMB MB)" -ForegroundColor Green
Write-Host " Path: $($lib.Path)" -ForegroundColor Gray
}
Write-ColorOutput "`nYou can now build ReclassX with CMake." Green
Write-ColorOutput "`nYou can now build Reclass with CMake." Green
} else {
Write-ColorOutput "`nWARNING: Build completed but no library files found." Yellow
Write-ColorOutput "Expected files: qscintilla2_qt6.a or qscintilla2_qt6.lib" Yellow

540
src/addressparser.cpp Normal file
View File

@@ -0,0 +1,540 @@
#include "addressparser.h"
namespace rcx {
// ── Address Expression Parser ──────────────────────────────────────────
//
// Parses expressions like:
// "7FF66CCE0000" → plain hex address
// "0x100 + 0x200" → arithmetic on hex values
// "<Program.exe> + 0xDE" → module base + offset
// "[<Program.exe> + 0xDE] - AB" → dereference pointer, then subtract
// "7ff6`6cce0000" → WinDbg-style backtick separator (stripped before parsing)
// "base + e_lfanew" → C/C++ style identifier resolution
// "0xFF & 0x0F" → bitwise AND
// "1 << 4" → shift left
//
// Grammar (C operator precedence):
//
// bitwiseOr = bitwiseXor ('|' bitwiseXor)*
// bitwiseXor = bitwiseAnd ('^' bitwiseAnd)*
// bitwiseAnd = shift ('&' shift)*
// shift = expr (('<<' | '>>') expr)*
// expr = term (('+' | '-') term)*
// term = unary (('*' | '/') unary)*
// unary = '-' unary | '~' unary | atom
// atom = '[' bitwiseOr ']' -- read pointer at address (dereference)
// | '<' moduleName '>' -- resolve module base address
// | '(' bitwiseOr ')' -- grouping
// | identifier -- C/C++ name resolved via callback
// | hexLiteral -- hex number, optional 0x prefix
//
// All numeric literals are hexadecimal (base 16).
// Identifiers: [a-zA-Z_][a-zA-Z0-9_]* containing at least one non-hex char.
// Module names with extensions (e.g. "client.dll") are scanned as one token.
// Pure hex-digit words (e.g. "DEAD") are treated as hex literals.
class ExpressionParser {
public:
ExpressionParser(const QString& input, const AddressParserCallbacks* callbacks)
: m_input(input), m_callbacks(callbacks) {}
AddressParseResult parse() {
skipSpaces();
if (atEnd())
return error("empty expression");
uint64_t value = 0;
if (!parseBitwiseOr(value))
return error(m_error);
skipSpaces();
if (!atEnd())
return error(QStringLiteral("unexpected '%1'").arg(m_input[m_pos]));
return {true, value, {}, -1};
}
private:
const QString& m_input;
const AddressParserCallbacks* m_callbacks;
int m_pos = 0;
QString m_error;
int m_errorPos = 0;
// ── Helpers ──
bool atEnd() const { return m_pos >= m_input.size(); }
QChar peek() const { return atEnd() ? QChar('\0') : m_input[m_pos]; }
void advance() { m_pos++; }
void skipSpaces() {
while (!atEnd() && m_input[m_pos].isSpace())
m_pos++;
}
AddressParseResult error(const QString& msg) const {
return {false, 0, msg, m_errorPos};
}
bool fail(const QString& msg) {
m_error = msg;
m_errorPos = m_pos;
return false;
}
bool expect(QChar ch) {
skipSpaces();
if (peek() != ch)
return fail(QStringLiteral("expected '%1'").arg(ch));
advance();
return true;
}
static bool isHexDigit(QChar ch) {
return (ch >= '0' && ch <= '9')
|| (ch >= 'a' && ch <= 'f')
|| (ch >= 'A' && ch <= 'F');
}
static bool isIdentStart(QChar ch) {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_';
}
static bool isIdentChar(QChar ch) {
return isIdentStart(ch) || (ch >= '0' && ch <= '9');
}
// ── Recursive descent parsing ──
// bitwiseOr = bitwiseXor ('|' bitwiseXor)*
bool parseBitwiseOr(uint64_t& result) {
if (!parseBitwiseXor(result))
return false;
for (;;) {
skipSpaces();
if (peek() != '|')
break;
advance();
uint64_t rhs = 0;
if (!parseBitwiseXor(rhs))
return false;
result |= rhs;
}
return true;
}
// bitwiseXor = bitwiseAnd ('^' bitwiseAnd)*
bool parseBitwiseXor(uint64_t& result) {
if (!parseBitwiseAnd(result))
return false;
for (;;) {
skipSpaces();
if (peek() != '^')
break;
advance();
uint64_t rhs = 0;
if (!parseBitwiseAnd(rhs))
return false;
result ^= rhs;
}
return true;
}
// bitwiseAnd = shift ('&' shift)*
bool parseBitwiseAnd(uint64_t& result) {
if (!parseShift(result))
return false;
for (;;) {
skipSpaces();
if (peek() != '&')
break;
advance();
uint64_t rhs = 0;
if (!parseShift(rhs))
return false;
result &= rhs;
}
return true;
}
// shift = expr (('<<' | '>>') expr)*
bool parseShift(uint64_t& result) {
if (!parseExpression(result))
return false;
for (;;) {
skipSpaces();
QChar c = peek();
if (c != '<' && c != '>')
break;
// Must be << or >> (not < or > alone)
if (m_pos + 1 >= m_input.size() || m_input[m_pos + 1] != c)
break;
bool isLeft = (c == '<');
advance(); advance(); // skip << or >>
uint64_t rhs = 0;
if (!parseExpression(rhs))
return false;
result = isLeft ? (result << rhs) : (result >> rhs);
}
return true;
}
// expr = term (('+' | '-') term)*
bool parseExpression(uint64_t& result) {
if (!parseTerm(result))
return false;
for (;;) {
skipSpaces();
QChar op = peek();
if (op != '+' && op != '-')
break;
advance();
uint64_t rhs = 0;
if (!parseTerm(rhs))
return false;
result = (op == '+') ? result + rhs : result - rhs;
}
return true;
}
// term = unary (('*' | '/') unary)*
bool parseTerm(uint64_t& result) {
if (!parseUnary(result))
return false;
for (;;) {
skipSpaces();
QChar op = peek();
if (op != '*' && op != '/')
break;
advance();
uint64_t rhs = 0;
if (!parseUnary(rhs))
return false;
if (op == '*') {
result *= rhs;
} else {
if (rhs == 0)
return fail("division by zero");
result /= rhs;
}
}
return true;
}
// unary = '-' unary | '~' unary | atom
bool parseUnary(uint64_t& result) {
skipSpaces();
if (peek() == '-') {
advance();
uint64_t inner = 0;
if (!parseUnary(inner))
return false;
result = static_cast<uint64_t>(-static_cast<int64_t>(inner));
return true;
}
if (peek() == '~') {
advance();
uint64_t inner = 0;
if (!parseUnary(inner))
return false;
result = ~inner;
return true;
}
return parseAtom(result);
}
// atom = '[' bitwiseOr ']' | '<' name '>' | '(' bitwiseOr ')' | identifier | hexLiteral
bool parseAtom(uint64_t& result) {
skipSpaces();
if (atEnd())
return fail("unexpected end of expression");
QChar ch = peek();
if (ch == '[') return parseDereference(result);
if (ch == '<') return parseModuleName(result);
if (ch == '(') return parseGrouping(result);
// Try identifier before hex — identifiers start with [a-zA-Z_]
if (isIdentStart(ch))
return parseIdentifierOrHex(result);
return parseHexNumber(result);
}
// Identifier or hex literal disambiguation.
// Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier.
// Otherwise → backtrack and parse as hex number.
// WinDbg-style "module!symbol" is scanned as a single identifier token.
// If the identifier is followed by '(', try to parse as a built-in function call.
bool parseIdentifierOrHex(uint64_t& result) {
int start = m_pos;
bool hasNonHex = false;
// Scan full token, including "module!symbol" as one token
while (!atEnd() && isIdentChar(peek())) {
if (!isHexDigit(peek()))
hasNonHex = true;
advance();
}
// Handle module.dll / module.exe / module.sys extensions
// e.g. "client.dll + 0xFF" should parse "client.dll" as one token
if (!atEnd() && peek() == '.' && m_pos > start) {
int dotPos = m_pos;
advance(); // skip '.'
int extStart = m_pos;
while (!atEnd() && isIdentChar(peek()))
advance();
if (m_pos > extStart) {
hasNonHex = true; // '.' makes it definitively an identifier
} else {
m_pos = dotPos; // backtrack — '.' at end isn't an extension
}
}
// If we hit '!' and the next char is an identifier start, extend the token
// to include the second part (WinDbg module!symbol syntax)
if (!atEnd() && peek() == '!' && m_pos > start) {
int bangPos = m_pos;
advance(); // skip '!'
if (!atEnd() && isIdentStart(peek())) {
hasNonHex = true;
while (!atEnd() && isIdentChar(peek())) {
advance();
}
} else {
m_pos = bangPos; // backtrack — '!' at end isn't module!symbol
}
}
QString token = m_input.mid(start, m_pos - start);
if (!hasNonHex) {
// Pure hex digits (e.g. "DEAD") — backtrack, parse as hex
m_pos = start;
return parseHexNumber(result);
}
// Check for function call syntax: identifier '(' args ')'
skipSpaces();
if (peek() == '(')
return parseFunctionCall(token, result);
// It's an identifier — resolve via callback
if (!m_callbacks || !m_callbacks->resolveIdentifier) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->resolveIdentifier(token, &ok);
if (!ok)
return fail(QStringLiteral("unknown identifier '%1'").arg(token));
return true;
}
// Built-in function call: vtop(pid, va), cr3(pid), phys(addr)
bool parseFunctionCall(const QString& name, uint64_t& result) {
advance(); // skip '('
if (name == QStringLiteral("vtop")) {
// vtop(pid, virtualAddress) → physical address
uint64_t pid = 0;
if (!parseBitwiseOr(pid)) return false;
skipSpaces();
if (peek() != ',')
return fail("vtop() requires 2 arguments: vtop(pid, va)");
advance(); // skip ','
uint64_t va = 0;
if (!parseBitwiseOr(va)) return false;
if (!expect(')')) return false;
if (!m_callbacks || !m_callbacks->vtop) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->vtop((uint32_t)pid, va, &ok);
if (!ok)
return fail(QStringLiteral("vtop(0x%1, 0x%2) failed")
.arg(pid, 0, 16).arg(va, 0, 16));
return true;
}
if (name == QStringLiteral("cr3")) {
// cr3(pid) → CR3 value
uint64_t pid = 0;
if (!parseBitwiseOr(pid)) return false;
if (!expect(')')) return false;
if (!m_callbacks || !m_callbacks->cr3) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->cr3((uint32_t)pid, &ok);
if (!ok)
return fail(QStringLiteral("cr3(%1) failed").arg(pid));
return true;
}
if (name == QStringLiteral("phys")) {
// phys(addr) → read 8 bytes from physical address
uint64_t addr = 0;
if (!parseBitwiseOr(addr)) return false;
if (!expect(')')) return false;
if (!m_callbacks || !m_callbacks->physRead) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->physRead(addr, &ok);
if (!ok)
return fail(QStringLiteral("phys(0x%1) failed").arg(addr, 0, 16));
return true;
}
return fail(QStringLiteral("unknown function '%1'").arg(name));
}
// '[' bitwiseOr ']' — read the pointer value at the computed address
bool parseDereference(uint64_t& result) {
advance(); // skip '['
uint64_t address = 0;
if (!parseBitwiseOr(address))
return false;
if (!expect(']'))
return false;
// Without a callback, just return 0 (syntax-check mode)
if (!m_callbacks || !m_callbacks->readPointer) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->readPointer(address, &ok);
if (!ok)
return fail(QStringLiteral("failed to read memory at 0x%1").arg(address, 0, 16));
return true;
}
// '<' moduleName '>' — resolve a module's base address (e.g. <Program.exe>)
bool parseModuleName(uint64_t& result) {
advance(); // skip '<'
int nameStart = m_pos;
while (!atEnd() && peek() != '>')
advance();
if (atEnd())
return fail("expected '>'");
QString name = m_input.mid(nameStart, m_pos - nameStart).trimmed();
advance(); // skip '>'
if (name.isEmpty())
return fail("empty module name");
// Without a callback, just return 0 (syntax-check mode)
if (!m_callbacks || !m_callbacks->resolveModule) {
result = 0;
return true;
}
bool ok = false;
result = m_callbacks->resolveModule(name, &ok);
if (!ok)
return fail(QStringLiteral("module '%1' not found").arg(name));
return true;
}
// '(' bitwiseOr ')' — parenthesized sub-expression for grouping
bool parseGrouping(uint64_t& result) {
advance(); // skip '('
if (!parseBitwiseOr(result))
return false;
return expect(')');
}
// Hex number with optional "0x" prefix. All literals are base-16.
bool parseHexNumber(uint64_t& result) {
skipSpaces();
if (atEnd())
return fail("unexpected end of expression");
int start = m_pos;
// Skip optional 0x/0X prefix
if (m_pos + 1 < m_input.size()
&& m_input[m_pos] == '0'
&& (m_input[m_pos + 1] == 'x' || m_input[m_pos + 1] == 'X'))
m_pos += 2;
// Consume hex digits
int digitsStart = m_pos;
while (!atEnd() && isHexDigit(peek()))
advance();
if (m_pos == digitsStart) {
m_errorPos = start;
return fail("expected hex number");
}
QString digits = m_input.mid(digitsStart, m_pos - digitsStart);
bool ok = false;
result = digits.toULongLong(&ok, 16);
if (!ok) {
m_errorPos = start;
return fail("invalid hex number");
}
return true;
}
};
// ── Public API ─────────────────────────────────────────────────────────
AddressParseResult AddressParser::evaluate(const QString& formula, int ptrSize,
const AddressParserCallbacks* cb)
{
// ptrSize is used by the caller to configure the readPointer callback;
// the parser itself doesn't need it directly.
Q_UNUSED(ptrSize);
// WinDbg displays 64-bit addresses with backtick separators for readability,
// e.g. "00007ff6`1a2b3c4d". Strip them so users can paste directly.
// Also remove ' in case user uses it
QString cleaned = formula;
cleaned.remove('`');
cleaned.remove('\'');
ExpressionParser parser(cleaned, cb);
return parser.parse();
}
QString AddressParser::validate(const QString& formula)
{
QString cleaned = formula;
cleaned.remove('`');
cleaned.remove('\'');
cleaned = cleaned.trimmed();
if (cleaned.isEmpty())
return QStringLiteral("empty");
// Parse with no callbacks — modules, dereferences, identifiers succeed but return 0.
// This checks syntax only.
ExpressionParser parser(cleaned, nullptr);
auto result = parser.parse();
return result.ok ? QString() : result.error;
}
} // namespace rcx

33
src/addressparser.h Normal file
View File

@@ -0,0 +1,33 @@
#pragma once
#include <QString>
#include <cstdint>
#include <functional>
namespace rcx {
struct AddressParseResult {
bool ok;
uint64_t value;
QString error;
int errorPos;
};
struct AddressParserCallbacks {
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
std::function<uint64_t(const QString& name, bool* ok)> resolveIdentifier;
// Kernel paging functions (optional — only wired when kernel provider active)
std::function<uint64_t(uint32_t pid, uint64_t va, bool* ok)> vtop;
std::function<uint64_t(uint32_t pid, bool* ok)> cr3;
std::function<uint64_t(uint64_t physAddr, bool* ok)> physRead;
};
class AddressParser {
public:
static AddressParseResult evaluate(const QString& formula, int ptrSize = 8,
const AddressParserCallbacks* cb = nullptr);
static QString validate(const QString& formula);
};
} // namespace rcx

1
src/app.rc Normal file
View File

@@ -0,0 +1 @@
IDI_ICON1 ICON "icons/class.ico"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,13 +7,15 @@
#include <QUndoCommand>
#include <QTimer>
#include <QFutureWatcher>
#include <QPointer>
#include <memory>
class QSplitter;
namespace rcx {
class RcxController;
class TypeSelectorPopup;
struct TypeEntry;
enum class TypePopupMode;
// ── Document ──
@@ -38,7 +40,10 @@ public:
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
}
ComposeResult compose(uint64_t viewRootId = 0) const;
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
bool treeLines = false, bool braceWrap = false,
bool typeHints = false,
SymbolLookupFn symbolLookup = {}) const;
bool save(const QString& path);
bool load(const QString& path);
void loadData(const QString& binaryPath);
@@ -63,12 +68,12 @@ private:
// ── Saved source entry ──
struct SavedSourceEntry {
QString kind; // "File" or "Process"
QString kind; // "File" or provider identifier (e.g. "processmemory")
QString displayName; // filename or process name
QString filePath; // for File sources
uint32_t pid = 0; // for Process sources
QString processName; // for Process sources
QString providerTarget; // for plugin providers (e.g. "pid:name")
uint64_t baseAddress = 0;
QString baseAddressFormula;
};
// ── Controller ──
@@ -80,23 +85,36 @@ public:
~RcxController() override;
RcxEditor* primaryEditor() const;
RcxEditor* addSplitEditor(QSplitter* splitter);
RcxEditor* addSplitEditor(QWidget* parent = nullptr);
void removeSplitEditor(RcxEditor* editor);
QList<RcxEditor*> editors() const { return m_editors; }
void convertRootKeyword(const QString& newKeyword);
void changeNodeKind(int nodeIdx, NodeKind newKind);
void renameNode(int nodeIdx, const QString& newName);
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
void insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name);
void removeNode(int nodeIdx);
void toggleCollapse(int nodeIdx);
void setNodeValue(int nodeIdx, int subLine, const QString& text, bool isAscii = false);
void materializeRefChildren(int nodeIdx);
void setNodeValue(int nodeIdx, int subLine, const QString& text,
bool isAscii = false, uint64_t resolvedAddr = 0);
void duplicateNode(int nodeIdx);
void convertToTypedPointer(uint64_t nodeId);
void splitHexNode(uint64_t nodeId);
void toggleBitfieldBit(uint64_t nodeId, int memberIdx);
void editBitfieldValue(uint64_t nodeId, int memberIdx);
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
void batchRemoveNodes(const QVector<int>& nodeIndices);
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
void deleteRootStruct(uint64_t structId);
void groupIntoUnion(const QSet<uint64_t>& nodeIds);
void dissolveUnion(uint64_t unionId);
void applyCommand(const Command& cmd, bool isUndo);
void refresh();
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
uint64_t findOrCreateStructByName(const QString& typeName);
// Selection
void handleNodeClick(RcxEditor* source, int line, uint64_t nodeId,
@@ -111,10 +129,43 @@ public:
RcxDocument* document() const { return m_doc; }
void setEditorFont(const QString& fontName);
void setRefreshInterval(int ms);
void setCompactColumns(bool v);
void setTreeLines(bool v);
void setBraceWrap(bool v);
void setTypeHints(bool v);
bool typeHints() const { return m_typeHints; }
void resetProvider();
// MCP bridge accessors
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
void attachViaPlugin(const QString& providerIdentifier, const QString& target);
const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; }
int activeSourceIndex() const { return m_activeSourceIdx; }
void switchSource(int idx) { switchToSavedSource(idx); }
void clearSources();
void selectSource(const QString& text);
void copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx);
// Value tracking toggle (per-tab, off by default)
bool trackValues() const { return m_trackValues; }
void setTrackValues(bool on);
void resetChangeTracking();
// Cross-tab type visibility: point at the project's full document list
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
// Test accessors
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
const ComposeResult& lastResult() const { return m_lastResult; }
int dataExtent() const { return computeDataExtent(); }
signals:
void nodeSelected(int nodeIdx);
void selectionChanged(int count);
void contextMenuAboutToShow(QMenu* menu, int line);
void requestOpenProviderTab(const QString& pluginId, const QString& target,
const QString& title);
private:
RcxDocument* m_doc;
@@ -123,29 +174,43 @@ private:
QSet<uint64_t> m_selIds;
int m_anchorLine = -1;
bool m_suppressRefresh = false;
bool m_compactColumns = false;
bool m_treeLines = false;
bool m_braceWrap = false;
bool m_typeHints = false;
uint64_t m_viewRootId = 0;
// ── Saved sources for quick-switch ──
QVector<SavedSourceEntry> m_savedSources;
int m_activeSourceIdx = -1;
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
QPointer<TypeSelectorPopup> m_cachedPopup;
int m_typePopupGen = 0; // generation counter for deferred content loading
// ── Auto-refresh state ──
using PageMap = QHash<uint64_t, QByteArray>;
QTimer* m_refreshTimer = nullptr;
QFutureWatcher<QByteArray>* m_refreshWatcher = nullptr;
QFutureWatcher<PageMap>* m_refreshWatcher = nullptr;
std::unique_ptr<SnapshotProvider> m_snapshotProv;
QByteArray m_prevSnapshot;
PageMap m_prevPages;
QSet<int64_t> m_changedOffsets;
QHash<uint64_t, ValueHistory> m_valueHistory;
bool m_trackValues = true;
int m_valueTrackCooldown = 0; // suppress value recording for N refresh cycles after clear
uint64_t m_refreshGen = 0;
uint64_t m_readGen = 0;
bool m_readInFlight = false;
QVector<RcxDocument*>* m_projectDocs = nullptr;
void connectEditor(RcxEditor* editor);
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
void updateCommandRow();
void performRealignment(uint64_t structId, int targetAlign);
void attachToProcess(uint32_t pid, const QString& processName);
void switchToSavedSource(int idx);
void pushSavedSourcesToEditors();
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);
TypeSelectorPopup* ensurePopup(RcxEditor* editor);
// ── Auto-refresh methods ──
void setupAutoRefresh();
@@ -153,6 +218,10 @@ private:
void onReadComplete();
int computeDataExtent() const;
void resetSnapshot();
void collectPointerRanges(uint64_t structId, uint64_t memBase,
int depth, int maxDepth,
QSet<QPair<uint64_t,uint64_t>>& visited,
QVector<QPair<uint64_t,int>>& ranges) const;
};
} // namespace rcx

View File

@@ -8,8 +8,10 @@
#include <QHash>
#include <QSet>
#include <cstdint>
#include <array>
#include <memory>
#include <variant>
#include <QDateTime>
#include "providers/provider.h"
#include "providers/buffer_provider.h"
@@ -25,17 +27,23 @@ enum class NodeKind : uint8_t {
UInt8, UInt16, UInt32, UInt64,
Float, Double, Bool,
Pointer32, Pointer64,
FuncPtr32, FuncPtr64,
Vec2, Vec3, Vec4, Mat4x4,
UTF8, UTF16,
Padding,
Struct, Array
};
// ── Kind flags (replaces repeated Hex/Padding switches) ──
} // namespace rcx (temporarily close for qHash)
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
inline uint qHash(rcx::NodeKind key, uint seed = 0) { return qHash(static_cast<int>(key), seed); }
#endif
namespace rcx { // reopen
// ── Kind flags (replaces repeated Hex switches) ──
enum KindFlags : uint32_t {
KF_None = 0,
KF_HexPreview = 1 << 0, // Hex8..Hex64 + Padding (ASCII+hex layout)
KF_HexPreview = 1 << 0, // Hex8..Hex64 (ASCII+hex layout)
KF_Container = 1 << 1, // Struct/Array
KF_String = 1 << 2, // UTF8/UTF16
KF_Vector = 1 << 3, // Vec2/3/4
@@ -72,13 +80,14 @@ inline constexpr KindMeta kKindMeta[] = {
{NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None},
{NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None},
{NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None},
{NodeKind::FuncPtr32, "FuncPtr32", "fnptr32", 4, 1, 4, KF_None},
{NodeKind::FuncPtr64, "FuncPtr64", "fnptr64", 8, 1, 8, KF_None},
{NodeKind::Vec2, "Vec2", "vec2", 8, 1, 4, KF_Vector},
{NodeKind::Vec3, "Vec3", "vec3", 12, 1, 4, KF_Vector},
{NodeKind::Vec4, "Vec4", "vec4", 16, 1, 4, KF_Vector},
{NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None},
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String},
{NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String},
{NodeKind::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview},
{NodeKind::UTF8, "UTF8", "str", 1, 1, 1, KF_String},
{NodeKind::UTF16, "UTF16", "wstr", 2, 1, 2, KF_String},
{NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container},
{NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container},
};
@@ -128,15 +137,27 @@ inline constexpr bool isHexNode(NodeKind k) {
inline constexpr bool isVectorKind(NodeKind k) {
return k == NodeKind::Vec2 || k == NodeKind::Vec3 || k == NodeKind::Vec4;
}
inline constexpr bool isMatrixKind(NodeKind k) {
return k == NodeKind::Mat4x4;
}
inline constexpr bool isFuncPtr(NodeKind k) {
return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64;
}
// Hex types, pointer types, function pointers, and containers are not meaningful
// primitive-pointer targets — dereferencing them produces the same output as void*.
inline constexpr bool isValidPrimitivePtrTarget(NodeKind k) {
if (isHexNode(k)) return false;
if (k == NodeKind::Pointer32 || k == NodeKind::Pointer64) return false;
if (isFuncPtr(k)) return false;
if (k == NodeKind::Struct || k == NodeKind::Array) return false;
return true;
}
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
inline QStringList allTypeNamesForUI(bool /*stripBrackets*/ = false) {
QStringList out;
out.reserve(std::size(kKindMeta));
for (const auto& m : kKindMeta) {
QString t = QString::fromLatin1(m.typeName);
if (stripBrackets) t.remove(QStringLiteral("[]"));
out << t;
}
for (const auto& m : kKindMeta)
out << QString::fromLatin1(m.typeName);
out.sort(Qt::CaseInsensitive);
out.removeDuplicates();
return out;
@@ -146,7 +167,6 @@ inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
enum Marker : int {
M_CONT = 0,
M_PAD = 1,
M_PTR0 = 2,
M_CYCLE = 3,
M_ERR = 4,
@@ -154,6 +174,15 @@ enum Marker : int {
M_HOVER = 6,
M_SELECTED = 7,
M_CMD_ROW = 8,
M_ACCENT = 9,
};
// ── Bitfield member (name + bit position + width within a container) ──
struct BitfieldMember {
QString name;
uint8_t bitOffset = 0; // position from LSB within the container
uint8_t bitWidth = 1; // number of bits (1..64)
};
// ── Node ──
@@ -166,20 +195,35 @@ struct Node {
QString classKeyword; // "struct", "class", or "enum" (empty = "struct")
uint64_t parentId = 0; // 0 = root (no parent)
int offset = 0;
bool isStatic = false; // static field — excluded from struct layout
QString offsetExpr; // C/C++ expression → absolute address (static fields only)
bool isRelative = false; // Pointer: target = base + value (RVA) instead of absolute
int arrayLen = 1; // Array: element count
int strLen = 64;
bool collapsed = false;
bool collapsed = true;
uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr
NodeKind elementKind = NodeKind::UInt8; // Array: element type
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
int viewIndex = 0; // Array: current view offset (transient)
QVector<QPair<QString, int64_t>> enumMembers; // Enum: name→value pairs
QVector<BitfieldMember> bitfieldMembers; // Bitfield: per-bit member definitions
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
int byteSize() const {
switch (kind) {
case NodeKind::UTF8: return strLen;
case NodeKind::UTF16: return strLen * 2;
case NodeKind::Padding: return qMax(1, arrayLen);
case NodeKind::Array: return arrayLen * sizeForKind(elementKind);
case NodeKind::UTF16: return qMin(strLen, INT_MAX / 2) * 2;
case NodeKind::Array: {
int elemSz = sizeForKind(elementKind);
if (elemSz <= 0) return 0;
return qMin(arrayLen, INT_MAX / elemSz) * elemSz;
}
case NodeKind::Struct:
if (classKeyword == QStringLiteral("bitfield")) {
int sz = sizeForKind(elementKind);
return sz > 0 ? sz : 4;
}
return 0;
default: return sizeForKind(kind);
}
}
@@ -195,11 +239,40 @@ struct Node {
o["classKeyword"] = classKeyword;
o["parentId"] = QString::number(parentId);
o["offset"] = offset;
if (isStatic)
o["isStatic"] = true;
if (!offsetExpr.isEmpty())
o["offsetExpr"] = offsetExpr;
if (isRelative)
o["isRelative"] = true;
o["arrayLen"] = arrayLen;
o["strLen"] = strLen;
o["collapsed"] = collapsed;
o["refId"] = QString::number(refId);
o["elementKind"] = kindToString(elementKind);
if (ptrDepth > 0)
o["ptrDepth"] = ptrDepth;
if (!enumMembers.isEmpty()) {
QJsonArray arr;
for (const auto& m : enumMembers) {
QJsonObject em;
em["name"] = m.first;
em["value"] = QString::number(m.second);
arr.append(em);
}
o["enumMembers"] = arr;
}
if (!bitfieldMembers.isEmpty()) {
QJsonArray arr;
for (const auto& m : bitfieldMembers) {
QJsonObject bm;
bm["name"] = m.name;
bm["bitOffset"] = m.bitOffset;
bm["bitWidth"] = m.bitWidth;
arr.append(bm);
}
o["bitfieldMembers"] = arr;
}
return o;
}
static Node fromJson(const QJsonObject& o) {
@@ -211,11 +284,34 @@ struct Node {
n.classKeyword = o["classKeyword"].toString();
n.parentId = o["parentId"].toString("0").toULongLong();
n.offset = o["offset"].toInt(0);
n.arrayLen = o["arrayLen"].toInt(1);
n.strLen = o["strLen"].toInt(64);
n.collapsed = o["collapsed"].toBool(false);
n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false));
n.offsetExpr = o["offsetExpr"].toString();
n.isRelative = o["isRelative"].toBool(false);
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
n.collapsed = true; // Always load collapsed; user expands as needed
n.refId = o["refId"].toString("0").toULongLong();
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
if (o.contains("enumMembers")) {
QJsonArray arr = o["enumMembers"].toArray();
for (const auto& v : arr) {
QJsonObject em = v.toObject();
n.enumMembers.emplaceBack(em["name"].toString(),
em["value"].toString("0").toLongLong());
}
}
if (o.contains("bitfieldMembers")) {
QJsonArray arr = o["bitfieldMembers"].toArray();
for (const auto& v : arr) {
QJsonObject bm = v.toObject();
BitfieldMember m;
m.name = bm["name"].toString();
m.bitOffset = (uint8_t)qBound(0, bm["bitOffset"].toInt(0), 255);
m.bitWidth = (uint8_t)qBound(1, bm["bitWidth"].toInt(1), 64);
n.bitfieldMembers.append(m);
}
}
return n;
}
@@ -224,11 +320,12 @@ struct Node {
return classKeyword.isEmpty() ? QStringLiteral("struct") : classKeyword;
}
// Helper: is this a string-like array (char[] or wchar_t[])?
bool isStringArray() const {
return kind == NodeKind::Array &&
(elementKind == NodeKind::UInt8 || elementKind == NodeKind::UInt16);
}
// NOTE: isStringArray() was checking UInt8/UInt16 instead of UTF8/UTF16.
// Currently unused — commented out until a caller needs it.
// bool isStringArray() const {
// return kind == NodeKind::Array &&
// (elementKind == NodeKind::UTF8 || elementKind == NodeKind::UTF16);
// }
};
// ── NodeTree ──
@@ -236,8 +333,11 @@ struct Node {
struct NodeTree {
QVector<Node> nodes;
uint64_t baseAddress = 0x00400000;
QString baseAddressFormula; // e.g. "<ReClass.exe> + 0x100"
int pointerSize = 8; // 4 for 32-bit targets, 8 for 64-bit
uint64_t m_nextId = 1;
mutable QHash<uint64_t, int> m_idCache;
mutable QHash<uint64_t, QVector<int>> m_childCache;
int addNode(const Node& n) {
Node copy = n;
@@ -247,13 +347,15 @@ struct NodeTree {
nodes.append(copy);
if (!m_idCache.isEmpty())
m_idCache[copy.id] = idx;
if (!m_childCache.isEmpty())
m_childCache[copy.parentId].append(idx);
return idx;
}
// Reserve a unique ID atomically (for use before pushing undo commands)
uint64_t reserveId() { return m_nextId++; }
void invalidateIdCache() const { m_idCache.clear(); }
void invalidateIdCache() const { m_idCache.clear(); m_childCache.clear(); }
int indexOfId(uint64_t id) const {
if (m_idCache.isEmpty() && !nodes.isEmpty()) {
@@ -264,11 +366,11 @@ struct NodeTree {
}
QVector<int> childrenOf(uint64_t parentId) const {
QVector<int> result;
for (int i = 0; i < nodes.size(); i++) {
if (nodes[i].parentId == parentId) result.append(i);
if (m_childCache.isEmpty() && !nodes.isEmpty()) {
for (int i = 0; i < nodes.size(); i++)
m_childCache[nodes[i].parentId].append(i);
}
return result;
return m_childCache.value(parentId);
}
// Collect node + all descendants (iterative, cycle-safe)
@@ -349,18 +451,20 @@ struct NodeTree {
QVector<int> kids = childMap ? childMap->value(structId) : childrenOf(structId);
for (int ci : kids) {
const Node& c = nodes[ci];
if (c.isStatic) continue; // static fields don't affect struct size
int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array)
? structSpan(c.id, childMap, visited) : c.byteSize();
int end = c.offset + sz;
if (end > maxEnd) maxEnd = end;
int64_t end = (int64_t)c.offset + sz;
if (end > maxEnd) maxEnd = (int)qMin(end, (int64_t)INT_MAX);
}
// Embedded struct reference: no own children but refId points to a struct definition
if (kids.isEmpty() && node.kind == NodeKind::Struct && node.refId != 0)
maxEnd = qMax(maxEnd, structSpan(node.refId, childMap, visited));
return qMax(declaredSize, maxEnd);
}
// Compute natural alignment of a struct (max alignment of direct children)
int computeStructAlignment(uint64_t structId) const;
// Batch selection normalizers
QSet<uint64_t> normalizePreferAncestors(const QSet<uint64_t>& ids) const;
QSet<uint64_t> normalizePreferDescendants(const QSet<uint64_t>& ids) const;
@@ -368,6 +472,10 @@ struct NodeTree {
QJsonObject toJson() const {
QJsonObject o;
o["baseAddress"] = QString::number(baseAddress, 16);
if (!baseAddressFormula.isEmpty())
o["baseAddressFormula"] = baseAddressFormula;
if (pointerSize != 8)
o["pointerSize"] = pointerSize;
o["nextId"] = QString::number(m_nextId);
QJsonArray arr;
for (const auto& n : nodes) arr.append(n.toJson());
@@ -378,8 +486,11 @@ struct NodeTree {
static NodeTree fromJson(const QJsonObject& o) {
NodeTree t;
t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16);
t.baseAddressFormula = o["baseAddressFormula"].toString();
t.pointerSize = o["pointerSize"].toInt(8);
t.m_nextId = o["nextId"].toString("1").toULongLong();
QJsonArray arr = o["nodes"].toArray();
t.nodes.reserve(arr.size());
for (const auto& v : arr) {
Node n = Node::fromJson(v.toObject());
t.nodes.append(n);
@@ -390,6 +501,66 @@ struct NodeTree {
};
// ── Value History (ring buffer for heatmap) ──
struct ValueHistory {
static constexpr int kCapacity = 10;
std::array<QString, kCapacity> values;
std::array<qint64, kCapacity> timestamps{}; // msec since epoch
int count = 0; // total unique values recorded
int head = 0; // next write position in ring
void record(const QString& v) {
if (count > 0) {
int last = (head + kCapacity - 1) % kCapacity;
if (values[last] == v) return; // no change
}
values[head] = v;
timestamps[head] = QDateTime::currentMSecsSinceEpoch();
head = (head + 1) % kCapacity;
if (count < INT_MAX) count++;
}
void clear() {
count = 0;
head = 0;
}
int uniqueCount() const { return qMin(count, kCapacity); }
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
int heatLevel() const {
if (count <= 1) return 0;
if (count == 2) return 1;
if (count <= 4) return 2;
return 3;
}
QString last() const {
if (count == 0) return {};
return values[(head + kCapacity - 1) % kCapacity];
}
// Iterate from oldest to newest (up to uniqueCount entries)
template<typename Fn>
void forEach(Fn&& fn) const {
int n = uniqueCount();
int start = (head + kCapacity - n) % kCapacity;
for (int i = 0; i < n; i++)
fn(values[(start + i) % kCapacity]);
}
// Iterate with timestamps from newest to oldest
template<typename Fn>
void forEachWithTime(Fn&& fn) const {
int n = uniqueCount();
for (int i = 0; i < n; i++) {
int idx = (head + kCapacity - 1 - i) % kCapacity;
fn(values[idx], timestamps[idx]);
}
}
};
// ── LineMeta ──
enum class LineKind : uint8_t {
@@ -402,6 +573,30 @@ static constexpr uint64_t kCommandRowId = UINT64_MAX;
static constexpr int kCommandRowLine = 0;
static constexpr int kFirstDataLine = 1;
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
static constexpr uint64_t kArrayElemShift = 42; // bits 42-61 hold element index
static constexpr uint64_t kArrayElemMask = 0x3FFFFC0000000000ULL; // 20 bits → max 1048575 elements
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 42)
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
Q_ASSERT(elemIdx >= 0);
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0xFFFFF) << kArrayElemShift);
}
inline int arrayElemIdxFromSelId(uint64_t selId) {
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
}
// Member selection encoding (enum/bitfield members) — mirrors array element pattern
static constexpr uint64_t kMemberBit = 0x2000000000000000ULL;
static constexpr uint64_t kMemberSubShift = 42;
static constexpr uint64_t kMemberSubMask = 0x3FFFFC0000000000ULL;
inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) {
return nodeId | kMemberBit | ((uint64_t)(subLine & 0xFFFFF) << kMemberSubShift);
}
inline int memberSubFromSelId(uint64_t selId) {
return (int)((selId & kMemberSubMask) >> kMemberSubShift);
}
struct LineMeta {
int nodeIdx = -1;
@@ -421,13 +616,22 @@ struct LineMeta {
int arrayCount = 0; // Array: total element count
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
QString offsetText;
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
uint64_t ptrBase = 0; // Pointer expansion base (non-zero = use for RVA)
uint32_t markerMask = 0;
bool dataChanged = false; // true if any byte in this node changed since last refresh
int heatLevel = 0; // 0=static, 1=cold, 2=warm, 3=hot (from ValueHistory)
QVector<int> changedByteIndices; // Hex preview: which byte indices (0-based) changed on this line
int lineByteCount = 0; // Hex preview: actual data byte count on this line
int effectiveTypeW = 14; // Per-line type column width used for rendering
int effectiveNameW = 22; // Per-line name column width used for rendering
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
bool isArrayElement = false; // true for synthesized primitive array element lines
bool isMemberLine = false; // true for enum member / bitfield member lines
bool isStaticLine = false; // true for static field node lines
QString typeHint; // Type inference hint text (e.g. "Float×2") — only set for hex nodes when hints enabled
int typeHintStart = -1; // Character offset where hint text starts in line text (-1 = none)
QVector<NodeKind> typeHintKinds; // Suggested kinds from inference (empty = no hint)
};
inline bool isSyntheticLine(const LineMeta& lm) {
@@ -439,6 +643,9 @@ inline bool isSyntheticLine(const LineMeta& lm) {
struct LayoutInfo {
int typeW = 14; // Effective type column width (default = kColType)
int nameW = 22; // Effective name column width (default = kColName)
int offsetHexDigits = 8; // Hex digits for offset margin (4/8/12/16)
uint64_t baseAddress = 0; // Base address for relative offset computation
bool treeLines = false; // Whether tree line connectors are embedded in the text
};
// ── ComposeResult ──
@@ -460,7 +667,7 @@ namespace cmd {
struct Insert { Node node; QVector<OffsetAdj> offAdjs; };
struct Remove { uint64_t nodeId; QVector<Node> subtree;
QVector<OffsetAdj> offAdjs; };
struct ChangeBase { uint64_t oldBase, newBase; };
struct ChangeBase { uint64_t oldBase, newBase; QString oldFormula, newFormula; };
struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; };
struct ChangeArrayMeta { uint64_t nodeId;
NodeKind oldElementKind, newElementKind;
@@ -470,13 +677,19 @@ namespace cmd {
struct ChangeStructTypeName { uint64_t nodeId; QString oldName, newName; };
struct ChangeClassKeyword { uint64_t nodeId; QString oldKeyword, newKeyword; };
struct ChangeOffset { uint64_t nodeId; int oldOffset, newOffset; };
struct ChangeEnumMembers { uint64_t nodeId;
QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; };
struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; };
struct ToggleRelative { uint64_t nodeId; bool oldVal, newVal; };
}
using Command = std::variant<
cmd::ChangeKind, cmd::Rename, cmd::Collapse,
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
cmd::ChangeClassKeyword, cmd::ChangeOffset
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers,
cmd::ChangeOffsetExpr, cmd::ToggleStatic, cmd::ToggleRelative
>;
// ── Column spans (for inline editing) ──
@@ -489,36 +702,37 @@ struct ColumnSpan {
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount,
ArrayElementType, ArrayElementCount, PointerTarget,
RootClassType, RootClassName };
RootClassType, RootClassName, TypeSelector, StaticExpr };
// Column layout constants (shared with format.cpp span computation)
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
inline constexpr int kColType = 14; // Max type column width (fits "uint64_t[999]")
inline constexpr int kColName = 22;
inline constexpr int kColValue = 32;
inline constexpr int kColValue = 96;
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address)
inline constexpr int kSepWidth = 1;
inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t")
inline constexpr int kMinTypeW = 7; // Minimum type column width (fits "uint8_t")
inline constexpr int kMaxTypeW = 128; // Maximum type column width
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
inline constexpr int kMaxNameW = 128; // Maximum name column width
inline constexpr int kCompactTypeW = 20; // Type column cap for compact column mode
inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) {
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
if (lm.lineKind != LineKind::Field || lm.isContinuation || lm.isMemberLine) return {};
int ind = kFoldCol + lm.depth * 3;
return {ind, ind + typeW, true};
}
inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int nameW = kColName) {
if (lm.isContinuation || lm.lineKind != LineKind::Field) return {};
if (lm.isContinuation || lm.lineKind != LineKind::Field || lm.isMemberLine) return {};
int ind = kFoldCol + lm.depth * 3;
int start = ind + typeW + kSepWidth;
// Hex/Padding: ASCII preview takes the name column position (8 chars)
// Hex: ASCII preview occupies the name column (padded to nameW)
if (isHexPreview(lm.nodeKind))
return {start, start + 8, true};
return {start, start + nameW, true};
return {start, start + nameW, true};
}
@@ -526,44 +740,77 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) {
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer ||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
if (lm.isMemberLine) return {};
int ind = kFoldCol + lm.depth * 3;
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
bool isHexPad = isHexPreview(lm.nodeKind);
int valWidth = isHexPad ? 23 : kColValue;
// Hex uses nameW for ASCII column (same as regular name column)
bool isHex = isHexPreview(lm.nodeKind);
int valWidth = isHex ? 23 : kColValue;
int prefixW = typeW + nameW + 2 * kSepWidth;
if (lm.isContinuation) {
int prefixW = isHexPad
? (typeW + kSepWidth + 8 + kSepWidth)
: (typeW + nameW + 2 * kSepWidth);
int start = ind + prefixW;
return {start, start + valWidth, true};
}
if (lm.lineKind != LineKind::Field) return {};
int start = isHexPad
? (ind + typeW + kSepWidth + 8 + kSepWidth)
: (ind + typeW + kSepWidth + nameW + kSepWidth);
int start = ind + prefixW;
return {start, start + valWidth, true};
}
// Member line spans (enum "name = value", bitfield "name : N = value")
inline ColumnSpan memberNameSpanFor(const LineMeta& lm, const QString& lineText) {
if (!lm.isMemberLine) return {};
int ind = kFoldCol + lm.depth * 3;
int eq = lineText.indexOf(QLatin1String(" = "), ind);
if (eq < 0) return {};
int nameEnd = eq;
while (nameEnd > ind && lineText[nameEnd - 1] == ' ') nameEnd--;
return {ind, nameEnd, true};
}
inline ColumnSpan memberValueSpanFor(const LineMeta& lm, const QString& lineText) {
if (!lm.isMemberLine) return {};
int eq = lineText.indexOf(QLatin1String(" = "));
if (eq < 0) return {};
int valStart = eq + 3;
int valEnd = lineText.size();
while (valEnd > valStart && lineText[valEnd - 1] == ' ') valEnd--;
return {valStart, valEnd, true};
}
// Static field expression span: locates text between "return " and "→" / "(error)" / end
inline ColumnSpan staticExprSpanFor(const LineMeta& /*lm*/, const QString& lineText) {
int ret = lineText.indexOf(QLatin1String("return "));
if (ret < 0) return {};
int exprStart = ret + 7;
// End: before arrow, before "(error)", or line end
int exprEnd = lineText.size();
int arrow = lineText.indexOf(QChar(0x2192), exprStart);
if (arrow > exprStart) exprEnd = arrow;
int err = lineText.indexOf(QLatin1String("(error)"), exprStart);
if (err > exprStart && err < exprEnd) exprEnd = err;
// Also stop at " }" for collapsed format
int brace = lineText.indexOf(QLatin1String(" }"), exprStart);
if (brace > exprStart && brace < exprEnd) exprEnd = brace;
while (exprEnd > exprStart && lineText[exprEnd - 1] == ' ') exprEnd--;
return {exprStart, exprEnd, true};
}
inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName) {
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
int ind = kFoldCol + lm.depth * 3;
bool isHexPad = isHexPreview(lm.nodeKind);
int valWidth = isHexPad ? 23 : kColValue;
bool isHex = isHexPreview(lm.nodeKind);
int valWidth = isHex ? 23 : kColValue;
int prefixW = typeW + nameW + 2 * kSepWidth;
int start;
if (lm.isContinuation) {
int prefixW = isHexPad
? (typeW + kSepWidth + 8 + kSepWidth)
: (typeW + nameW + 2 * kSepWidth);
start = ind + prefixW + valWidth;
} else {
start = isHexPad
? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth)
: (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth);
start = ind + prefixW + valWidth;
}
return {start, lineLength, start < lineLength};
}
@@ -572,50 +819,56 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
// Line format: "source▾ · 0x140000000"
inline ColumnSpan commandRowSrcSpan(const QString& lineText) {
int idx = lineText.indexOf(QStringLiteral(" \u00B7"));
if (idx < 0) return {};
// Source label ends at the ▾ dropdown arrow
int arrow = lineText.indexOf(QChar(0x25BE));
if (arrow < 0) return {};
int start = 0;
while (start < idx && !lineText[start].isLetterOrNumber()
while (start < arrow && !lineText[start].isLetterOrNumber()
&& lineText[start] != '<' && lineText[start] != '\'') start++;
if (start >= idx) return {};
// Exclude trailing ▾ from the editable span
int end = idx;
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
if (end <= start) return {};
return {start, end, true};
}
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
int tag = lineText.indexOf(QStringLiteral(" \u00B7"));
if (tag < 0) return {};
int start = tag + 3; // after " · "
int end = start;
while (end < lineText.size() && !lineText[end].isSpace()) end++;
if (end <= start) return {};
return {start, end, true};
if (start >= arrow) return {};
return {start, arrow, true};
}
// ── CommandRow root-class spans ──
// Combined CommandRow format ends with: " struct ClassName {"
// Combined CommandRow format ends with: " struct ClassName {"
inline int commandRowRootStart(const QString& lineText) {
int best = -1;
int i;
i = lineText.lastIndexOf(QStringLiteral("struct\u25BE"));
// Match "struct " / "class " / "enum " as whole words before the class name
i = lineText.lastIndexOf(QStringLiteral("struct "));
if (i > best) best = i;
i = lineText.lastIndexOf(QStringLiteral("class\u25BE"));
i = lineText.lastIndexOf(QStringLiteral("class "));
if (i > best) best = i;
i = lineText.lastIndexOf(QStringLiteral("enum\u25BE"));
i = lineText.lastIndexOf(QStringLiteral("enum "));
if (i > best) best = i;
return best;
}
inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
// Address starts at "0x" after the source dropdown arrow
int arrow = lineText.indexOf(QChar(0x25BE));
if (arrow < 0) return {};
int start = lineText.indexOf(QStringLiteral("0x"), arrow);
if (start < 0) {
// Formula mode: address is between arrow and root keyword
start = arrow + 1;
while (start < lineText.size() && lineText[start].isSpace()) start++;
}
// End at root keyword (struct/class/enum) or end of line
int rootStart = commandRowRootStart(lineText);
int end = (rootStart > start) ? rootStart : lineText.size();
// Trim trailing whitespace
while (end > start && lineText[end - 1].isSpace()) end--;
if (end <= start) return {};
return {start, end, true};
}
inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) {
int start = commandRowRootStart(lineText);
if (start < 0) return {};
int end = start;
while (end < lineText.size() && lineText[end] != QChar(' ')
&& lineText[end] != QChar(0x25BE)) end++;
while (end < lineText.size() && lineText[end] != QChar(' ')) end++;
if (end <= start) return {};
return {start, end, true};
}
@@ -635,6 +888,16 @@ inline ColumnSpan commandRowRootNameSpan(const QString& lineText) {
return {nameStart, nameEnd, true};
}
// ── CommandRow type-selector chevron span ──
// Detects "[▸]" at the start of the command row text
inline ColumnSpan commandRowChevronSpan(const QString& lineText) {
if (lineText.size() < 3) return {};
if (lineText[0] == '[' && lineText[1] == QChar(0x25B8) && lineText[2] == ']')
return {0, qMin(4, (int)lineText.size()), true}; // include trailing space for easier clicking
return {};
}
// ── Array element type/count spans (within type column of array headers) ──
// Line format: " int32_t[10] name {"
// arrayElemTypeSpan covers "int32_t", arrayElemCountSpan covers "10"
@@ -657,6 +920,16 @@ inline ColumnSpan arrayElemCountSpanFor(const LineMeta& lm, const QString& lineT
return {openBracket + 1, closeBracket, true};
}
// Click-area version: includes brackets [N] for hit testing
inline ColumnSpan arrayElemCountClickSpanFor(const LineMeta& lm, const QString& lineText) {
if (lm.lineKind != LineKind::Header || !lm.isArrayHeader) return {};
int ind = kFoldCol + lm.depth * 3;
int openBracket = lineText.indexOf('[', ind);
int closeBracket = lineText.indexOf(']', openBracket);
if (openBracket < 0 || closeBracket < 0 || closeBracket <= openBracket + 1) return {};
return {openBracket, closeBracket + 1, true};
}
// ── Pointer kind/target spans (within type column of pointer fields) ──
// Line format: " void* name -> 0x..."
// pointerTargetSpan covers the target name before '*'
@@ -739,17 +1012,18 @@ namespace fmt {
QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine = 0,
const QString& comment = {}, int colType = kColType, int colName = kColName,
const QString& typeOverride = {});
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation);
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName);
const QString& typeOverride = {}, bool compact = false);
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits = 8);
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName, bool compact = false);
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName);
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {}, bool compact = false);
QString structTypeName(const Node& node); // Full type string for struct headers
QString arrayTypeName(NodeKind elemKind, int count);
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName = {});
QString pointerTypeName(NodeKind kind, const QString& targetName);
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
const Provider& prov, uint64_t addr,
const QString& ptrTypeName, int colType = kColType, int colName = kColName);
const QString& ptrTypeName, int colType = kColType, int colName = kColName,
bool compact = false);
QString validateBaseAddress(const QString& text);
QString indent(int depth);
QString readValue(const Node& node, const Provider& prov,
@@ -759,10 +1033,23 @@ namespace fmt {
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
QString validateValue(NodeKind kind, const QString& text);
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW);
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
uint64_t value, int depth, int nameW);
uint64_t extractBits(const Provider& prov, uint64_t addr,
NodeKind containerKind,
uint8_t bitOffset, uint8_t bitWidth);
} // namespace fmt
// ── Compose function forward declaration ──
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0);
// Optional callback: given an absolute address, return a symbol name (e.g. "nt!PsActiveProcessHead")
// or empty string if no symbol matches. Used for PDB symbol annotations on rows.
using SymbolLookupFn = std::function<QString(uint64_t addr)>;
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
bool compactColumns = false, bool treeLines = false,
bool braceWrap = false, bool typeHints = false,
SymbolLookupFn symbolLookup = {});
} // namespace rcx

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

36
src/dock_tab_buttons.h Normal file
View File

@@ -0,0 +1,36 @@
#pragma once
#include <QWidget>
#include <QToolButton>
#include <QHBoxLayout>
#include <QIcon>
// Dock tab button widget (close button)
// Placed on the right side of each dock tab via QTabBar::setTabButton.
class DockTabButtons : public QWidget {
Q_OBJECT
public:
QToolButton* closeBtn;
explicit DockTabButtons(QWidget* parent = nullptr) : QWidget(parent) {
auto* hl = new QHBoxLayout(this);
hl->setContentsMargins(0, 0, 0, 0);
hl->setSpacing(0);
closeBtn = new QToolButton(this);
closeBtn->setAutoRaise(true);
closeBtn->setCursor(Qt::PointingHandCursor);
closeBtn->setFixedSize(16, 16);
closeBtn->setToolTip("Close tab");
closeBtn->setIcon(QIcon(":/vsicons/close.svg"));
closeBtn->setIconSize(QSize(12, 12));
hl->addWidget(closeBtn);
}
void applyTheme(const QColor& hover) {
QString style = QStringLiteral(
"QToolButton { border: none; padding: 1px; border-radius: 0px; }"
"QToolButton:hover { background: %1; }").arg(hover.name());
closeBtn->setStyleSheet(style);
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,18 @@
#pragma once
#include "core.h"
#include "providerregistry.h"
#include "themes/theme.h"
#include <QWidget>
#include <QSet>
#include <QPoint>
#include <QHash>
class QLineEdit;
class QsciScintilla;
class QsciLexerCPP;
namespace rcx {
struct SavedSourceDisplay {
QString text;
bool active = false;
};
class RcxEditor : public QWidget {
Q_OBJECT
public:
@@ -26,9 +25,15 @@ public:
void restoreViewState(const ViewState& vs);
QsciScintilla* scintilla() const { return m_sci; }
QWidget* historyPopup() const { return m_historyPopup; }
QWidget* disasmPopup() const { return m_disasmPopup; }
QWidget* structPreviewPopup() const { return m_structPreviewPopup; }
const LineMeta* metaForLine(int line) const;
int currentNodeIndex() const;
void scrollToNodeId(uint64_t nodeId);
void showFindBar();
void dismissHistoryPopup();
void dismissAllPopups();
// ── Column span computation ──
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
@@ -40,16 +45,27 @@ public:
// ── Inline editing ──
bool isEditing() const { return m_editState.active; }
bool beginInlineEdit(EditTarget target, int line = -1);
bool beginInlineEdit(EditTarget target, int line = -1, int col = -1);
void cancelInlineEdit();
void setHexEditPending(bool v) { m_hexEditPending = v; }
void setStaticCompletions(const QStringList& words) { m_staticCompletions = words; }
void applySelectionOverlay(const QSet<uint64_t>& selIds);
void setCommandRowText(const QString& line);
void setEditorFont(const QString& fontName);
static void setGlobalFontName(const QString& fontName);
static QString globalFontName();
void applyTheme(const Theme& theme);
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
QString textWithMargins() const;
void setCustomTypeNames(const QStringList& names);
void setValueHistoryRef(const QHash<uint64_t, ValueHistory>* ref) { m_valueHistory = ref; }
void setProviderRef(const Provider* prov, const Provider* realProv, const NodeTree* tree) {
m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree;
}
void setRelativeOffsets(bool rel) { m_relativeOffsets = rel; reformatMargins(); }
// Saved sources for quick-switch in source picker
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
@@ -57,10 +73,19 @@ public:
signals:
void marginClicked(int margin, int line, Qt::KeyboardModifiers mods);
void contextMenuRequested(int line, int nodeIdx, int subLine, QPoint globalPos);
void keywordConvertRequested(const QString& newKeyword);
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
void inlineEditCommitted(int nodeIdx, int subLine,
EditTarget target, const QString& text);
EditTarget target, const QString& text,
uint64_t resolvedAddr = 0);
void inlineEditCancelled();
void typeSelectorRequested();
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
void insertAboveRequested(int nodeIdx, NodeKind kind);
void relativeOffsetsChanged(bool relative);
void appendBytesRequested(uint64_t structId, int byteCount);
void trimHexRequested(uint64_t structId);
void appendEnumMembersRequested(uint64_t enumId, int count);
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
@@ -71,6 +96,9 @@ private:
QVector<LineMeta> m_meta;
LayoutInfo m_layout; // cached from ComposeResult
// ── Toggle: absolute vs relative offset margin
bool m_relativeOffsets = true;
int m_marginStyleBase = -1;
int m_hintLine = -1;
@@ -79,8 +107,12 @@ private:
bool m_hoverInside = false;
uint64_t m_hoveredNodeId = 0;
int m_hoveredLine = -1;
uint64_t m_prevHoveredNodeId = 0; // for incremental marker update
int m_prevHoveredLine = -1; // for incremental marker update
QSet<uint64_t> m_currentSelIds;
QVector<int> m_hoverSpanLines; // Lines with hover span indicators
// ── nodeId → display-line index (built in applyDocument) ──
QHash<uint64_t, QVector<int>> m_nodeLineIndex;
// ── Drag selection ──
bool m_dragging = false;
bool m_dragStarted = false; // true once drag threshold exceeded
@@ -108,8 +140,10 @@ private:
NodeKind editKind = NodeKind::Int32;
int commentCol = -1; // fixed comment column (stored at edit start)
bool lastValidationOk = true; // track state to avoid redundant updates
bool hexOverwrite = false; // true for hex-byte / ASCII-preview fixed-length editing
};
InlineEditState m_editState;
QStringList m_staticCompletions; // autocomplete words for StaticExpr editing
// ── Tab cycling state ──
EditTarget m_lastTabTarget = EditTarget::Value;
@@ -120,7 +154,27 @@ private:
// ── Saved sources for quick-switch ──
QVector<SavedSourceDisplay> m_savedSourceDisplay;
// ── Value history ref (owned by controller) ──
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
QWidget* m_disasmPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
QWidget* m_structPreviewPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
QWidget* m_arrowTooltip = nullptr; // RcxTooltip (arrow callout)
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
const NodeTree* m_disasmTree = nullptr;
// ── Find bar ──
QWidget* m_findBarContainer = nullptr;
QLineEdit* m_findBar = nullptr;
long m_findPos = 0;
void hideFindBar();
// ── Hex inline edit ──
bool m_hexEditPending = false; // set by context menu before calling beginInlineEdit
// ── Reentrancy guards ──
bool m_applyingDocument = false;
bool m_clampingSelection = false;
bool m_updatingComment = false;
@@ -131,11 +185,11 @@ private:
void setupMarkers();
void allocateMarginStyles();
void applyMarginText(const QVector<LineMeta>& meta);
void applyMarkers(const QVector<LineMeta>& meta);
void applyFoldLevels(const QVector<LineMeta>& meta);
void applyLineAttributes(const QVector<LineMeta>& meta);
void reformatMargins();
void applyHexDimming(const QVector<LineMeta>& meta);
void applyDataChangedHighlight(const QVector<LineMeta>& meta);
void applyHeatmapHighlight(const QVector<LineMeta>& meta, const QVector<QString>& lineTexts);
void applySymbolColoring(const QVector<LineMeta>& meta, const QVector<QString>& lineTexts);
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
void applyCommandRowPills();
@@ -143,6 +197,7 @@ private:
int editEndCol() const;
bool handleNormalKey(QKeyEvent* ke);
bool handleEditKey(QKeyEvent* ke);
bool handleHexEditKey(QKeyEvent* ke);
void showTypeAutocomplete();
void showSourcePicker();
void showTypeListFiltered(const QString& filter);

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

616
src/examples/MMPFN.rcx Normal file
View File

@@ -0,0 +1,616 @@
{
"baseAddress": "FFFFCA8010000000",
"nextId": "3000",
"nodes": [
{
"id": "100",
"kind": "Struct",
"name": "list_entry",
"structTypeName": "_LIST_ENTRY",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "101",
"kind": "Pointer64",
"name": "Flink",
"offset": 0,
"parentId": "100",
"refId": "100",
"collapsed": true
},
{
"id": "102",
"kind": "Pointer64",
"name": "Blink",
"offset": 8,
"parentId": "100",
"refId": "100",
"collapsed": true
},
{
"id": "200",
"kind": "Struct",
"name": "balanced_node",
"structTypeName": "_RTL_BALANCED_NODE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "210",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 0,
"parentId": "200",
"refId": "0",
"collapsed": false
},
{
"id": "211",
"kind": "Pointer64",
"name": "Left",
"offset": 0,
"parentId": "210",
"refId": "200",
"collapsed": true
},
{
"id": "212",
"kind": "Pointer64",
"name": "Right",
"offset": 8,
"parentId": "210",
"refId": "200",
"collapsed": true
},
{
"id": "220",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 16,
"parentId": "200",
"refId": "0",
"collapsed": false
},
{
"id": "221",
"kind": "UInt64",
"name": "ParentValue",
"offset": 0,
"parentId": "220"
},
{
"id": "300",
"kind": "Struct",
"name": "mmpte",
"structTypeName": "_MMPTE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "301",
"kind": "Struct",
"name": "u",
"classKeyword": "union",
"offset": 0,
"parentId": "300",
"refId": "0",
"collapsed": false
},
{
"id": "302",
"kind": "UInt64",
"name": "Long",
"offset": 0,
"parentId": "301"
},
{
"id": "303",
"kind": "Struct",
"name": "Hard",
"structTypeName": "_MMPTE_HARDWARE",
"offset": 0,
"parentId": "301",
"refId": "400",
"collapsed": true
},
{
"id": "304",
"kind": "Struct",
"name": "Proto",
"structTypeName": "_MMPTE_PROTOTYPE",
"offset": 0,
"parentId": "301",
"refId": "600",
"collapsed": true
},
{
"id": "305",
"kind": "Struct",
"name": "Soft",
"structTypeName": "_MMPTE_SOFTWARE",
"offset": 0,
"parentId": "301",
"refId": "500",
"collapsed": true
},
{
"id": "306",
"kind": "Struct",
"name": "Trans",
"structTypeName": "_MMPTE_TRANSITION",
"offset": 0,
"parentId": "301",
"refId": "700",
"collapsed": true
},
{
"id": "307",
"kind": "Struct",
"name": "Subsect",
"structTypeName": "_MMPTE_SUBSECTION",
"offset": 0,
"parentId": "301",
"refId": "800",
"collapsed": true
},
{
"id": "308",
"kind": "Struct",
"name": "TimeStamp",
"structTypeName": "_MMPTE_TIMESTAMP",
"offset": 0,
"parentId": "301",
"refId": "900",
"collapsed": true
},
{
"id": "309",
"kind": "Struct",
"name": "List",
"structTypeName": "_MMPTE_LIST",
"offset": 0,
"parentId": "301",
"refId": "1000",
"collapsed": true
},
{
"id": "400",
"kind": "Struct",
"name": "mmpte_hardware",
"structTypeName": "_MMPTE_HARDWARE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "401",
"kind": "Hex64",
"name": "Valid:1 Dirty1:1 Owner:1 WriteThrough:1 CacheDisable:1 Accessed:1 Dirty:1 LargePage:1 Global:1 CopyOnWrite:1 Unused:1 Write:1 PageFrameNumber:40 ReservedForSoftware:4 WsleAge:4 WsleProtection:3 NoExecute:1",
"offset": 0,
"parentId": "400"
},
{
"id": "500",
"kind": "Struct",
"name": "mmpte_software",
"structTypeName": "_MMPTE_SOFTWARE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "501",
"kind": "Hex64",
"name": "Valid:1 PageFileReserved:1 PageFileAllocated:1 ColdPage:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 UsedPageTableEntries:10 ShadowStack:1 OnStandbyLookaside:1 Unused:4 PageFileHigh:32",
"offset": 0,
"parentId": "500"
},
{
"id": "600",
"kind": "Struct",
"name": "mmpte_prototype",
"structTypeName": "_MMPTE_PROTOTYPE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "601",
"kind": "Hex64",
"name": "Valid:1 DemandFillProto:1 HiberVerifyConverted:1 ReadOnly:1 SwizzleBit:1 Protection:5 Prototype:1 Combined:1 Unused1:4 ProtoAddress:48",
"offset": 0,
"parentId": "600"
},
{
"id": "700",
"kind": "Struct",
"name": "mmpte_transition",
"structTypeName": "_MMPTE_TRANSITION",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "701",
"kind": "Hex64",
"name": "Valid:1 Write:1 OnStandbyLookaside:1 IoTracker:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFrameNumber:40 Unused:12",
"offset": 0,
"parentId": "700"
},
{
"id": "800",
"kind": "Struct",
"name": "mmpte_subsection",
"structTypeName": "_MMPTE_SUBSECTION",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "801",
"kind": "Hex64",
"name": "Valid:1 Unused0:2 OnStandbyLookaside:1 SwizzleBit:1 Protection:5 Prototype:1 ColdPage:1 Unused2:3 ExecutePrivilege:1 SubsectionAddress:48",
"offset": 0,
"parentId": "800"
},
{
"id": "900",
"kind": "Struct",
"name": "mmpte_timestamp",
"structTypeName": "_MMPTE_TIMESTAMP",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "901",
"kind": "Hex64",
"name": "MustBeZero:1 Unused:3 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 Reserved:16 GlobalTimeStamp:32",
"offset": 0,
"parentId": "900"
},
{
"id": "1000",
"kind": "Struct",
"name": "mmpte_list",
"structTypeName": "_MMPTE_LIST",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1001",
"kind": "Hex64",
"name": "Valid:1 OneEntry:1 filler0:2 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 filler1:13 NextEntry:39",
"offset": 0,
"parentId": "1000"
},
{
"id": "1100",
"kind": "Struct",
"name": "mipfnflink",
"structTypeName": "_MIPFNFLINK",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1101",
"kind": "Hex64",
"name": "Flink",
"offset": 0,
"parentId": "1100"
},
{
"id": "1200",
"kind": "Struct",
"name": "mipfnblink",
"structTypeName": "_MIPFNBLINK",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1201",
"kind": "Hex64",
"name": "Blink",
"offset": 0,
"parentId": "1200"
},
{
"id": "1300",
"kind": "Struct",
"name": "mmpfnentry1",
"structTypeName": "_MMPFNENTRY1",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1301",
"kind": "Hex8",
"name": "Flags",
"offset": 0,
"parentId": "1300"
},
{
"id": "1400",
"kind": "Struct",
"name": "mmpfnentry3",
"structTypeName": "_MMPFNENTRY3",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1401",
"kind": "Hex8",
"name": "Flags",
"offset": 0,
"parentId": "1400"
},
{
"id": "1500",
"kind": "Struct",
"name": "mi_pfn_flags",
"structTypeName": "_MI_PFN_FLAGS",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1501",
"kind": "Hex32",
"name": "Flags",
"offset": 0,
"parentId": "1500"
},
{
"id": "1600",
"kind": "Struct",
"name": "mi_pfn_flags4",
"structTypeName": "_MI_PFN_FLAGS4",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1601",
"kind": "Hex64",
"name": "Flags",
"offset": 0,
"parentId": "1600"
},
{
"id": "1700",
"kind": "Struct",
"name": "mi_pfn_flags5",
"structTypeName": "_MI_PFN_FLAGS5",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "1701",
"kind": "Hex32",
"name": "Flags",
"offset": 0,
"parentId": "1700"
},
{
"id": "2000",
"kind": "Struct",
"name": "mmpfn",
"structTypeName": "_MMPFN",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": false
},
{
"id": "2001",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 0,
"parentId": "2000",
"refId": "0",
"collapsed": false
},
{
"id": "2010",
"kind": "Struct",
"name": "ListEntry",
"structTypeName": "_LIST_ENTRY",
"offset": 0,
"parentId": "2001",
"refId": "100",
"collapsed": true
},
{
"id": "2011",
"kind": "Struct",
"name": "TreeNode",
"structTypeName": "_RTL_BALANCED_NODE",
"offset": 0,
"parentId": "2001",
"refId": "200",
"collapsed": true
},
{
"id": "2012",
"kind": "Struct",
"name": "",
"offset": 0,
"parentId": "2001",
"refId": "0",
"collapsed": false
},
{
"id": "2013",
"kind": "Struct",
"name": "u1",
"structTypeName": "_MIPFNFLINK",
"offset": 0,
"parentId": "2012",
"refId": "1100",
"collapsed": true
},
{
"id": "2014",
"kind": "Struct",
"name": "",
"classKeyword": "union",
"offset": 8,
"parentId": "2012",
"refId": "0",
"collapsed": false
},
{
"id": "2015",
"kind": "Pointer64",
"name": "PteAddress",
"offset": 0,
"parentId": "2014",
"refId": "300",
"collapsed": true
},
{
"id": "2016",
"kind": "UInt64",
"name": "PteLong",
"offset": 0,
"parentId": "2014"
},
{
"id": "2017",
"kind": "Struct",
"name": "OriginalPte",
"structTypeName": "_MMPTE",
"offset": 16,
"parentId": "2012",
"refId": "300",
"collapsed": true
},
{
"id": "2020",
"kind": "Struct",
"name": "u2",
"structTypeName": "_MIPFNBLINK",
"offset": 24,
"parentId": "2000",
"refId": "1200",
"collapsed": true
},
{
"id": "2030",
"kind": "Struct",
"name": "u3",
"classKeyword": "union",
"offset": 32,
"parentId": "2000",
"refId": "0",
"collapsed": false
},
{
"id": "2031",
"kind": "Struct",
"name": "",
"offset": 0,
"parentId": "2030",
"refId": "0",
"collapsed": false
},
{
"id": "2032",
"kind": "UInt16",
"name": "ReferenceCount",
"offset": 0,
"parentId": "2031"
},
{
"id": "2033",
"kind": "Struct",
"name": "e1",
"structTypeName": "_MMPFNENTRY1",
"offset": 2,
"parentId": "2031",
"refId": "1300",
"collapsed": true
},
{
"id": "2034",
"kind": "Struct",
"name": "e4",
"structTypeName": "_MI_PFN_FLAGS",
"offset": 0,
"parentId": "2030",
"refId": "1500",
"collapsed": true
},
{
"id": "2040",
"kind": "Struct",
"name": "u5",
"structTypeName": "_MI_PFN_FLAGS5",
"offset": 36,
"parentId": "2000",
"refId": "1700",
"collapsed": true
},
{
"id": "2050",
"kind": "Struct",
"name": "u4",
"structTypeName": "_MI_PFN_FLAGS4",
"offset": 40,
"parentId": "2000",
"refId": "1600",
"collapsed": true
}
]
}

126
src/examples/PageTables.rcx Normal file
View File

@@ -0,0 +1,126 @@
{
"baseAddress": "0",
"nextId": "2000",
"nodes": [
{
"id": "100",
"kind": "Struct",
"name": "pte",
"structTypeName": "X64_PTE",
"classKeyword": "bitfield",
"elementKind": "UInt64",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true,
"arrayLen": 1,
"strLen": 64,
"bitfieldMembers": [
{"name": "Present", "bitOffset": 0, "bitWidth": 1},
{"name": "ReadWrite", "bitOffset": 1, "bitWidth": 1},
{"name": "UserSuper", "bitOffset": 2, "bitWidth": 1},
{"name": "WriteThrough", "bitOffset": 3, "bitWidth": 1},
{"name": "CacheDisable", "bitOffset": 4, "bitWidth": 1},
{"name": "Accessed", "bitOffset": 5, "bitWidth": 1},
{"name": "Dirty", "bitOffset": 6, "bitWidth": 1},
{"name": "PageSize", "bitOffset": 7, "bitWidth": 1},
{"name": "Global", "bitOffset": 8, "bitWidth": 1},
{"name": "AVL", "bitOffset": 9, "bitWidth": 3},
{"name": "PhysAddr", "bitOffset": 12, "bitWidth": 40},
{"name": "Available", "bitOffset": 52, "bitWidth": 7},
{"name": "ProtKey", "bitOffset": 59, "bitWidth": 4},
{"name": "NX", "bitOffset": 63, "bitWidth": 1}
]
},
{
"id": "200",
"kind": "Struct",
"name": "page_table",
"structTypeName": "X64_PAGE_TABLE",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true
},
{
"id": "201",
"kind": "Array",
"name": "entries",
"offset": 0,
"parentId": "200",
"refId": "100",
"elementKind": "Struct",
"arrayLen": 512,
"strLen": 64,
"collapsed": true
},
{
"id": "300",
"kind": "Struct",
"name": "pde_2mb",
"structTypeName": "X64_PDE_LARGE",
"classKeyword": "bitfield",
"elementKind": "UInt64",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true,
"arrayLen": 1,
"strLen": 64,
"bitfieldMembers": [
{"name": "Present", "bitOffset": 0, "bitWidth": 1},
{"name": "ReadWrite", "bitOffset": 1, "bitWidth": 1},
{"name": "UserSuper", "bitOffset": 2, "bitWidth": 1},
{"name": "WriteThrough", "bitOffset": 3, "bitWidth": 1},
{"name": "CacheDisable", "bitOffset": 4, "bitWidth": 1},
{"name": "Accessed", "bitOffset": 5, "bitWidth": 1},
{"name": "Dirty", "bitOffset": 6, "bitWidth": 1},
{"name": "PageSize", "bitOffset": 7, "bitWidth": 1},
{"name": "Global", "bitOffset": 8, "bitWidth": 1},
{"name": "AVL", "bitOffset": 9, "bitWidth": 3},
{"name": "PAT", "bitOffset": 12, "bitWidth": 1},
{"name": "Reserved", "bitOffset": 13, "bitWidth": 8},
{"name": "PhysAddr", "bitOffset": 21, "bitWidth": 31},
{"name": "Available", "bitOffset": 52, "bitWidth": 7},
{"name": "ProtKey", "bitOffset": 59, "bitWidth": 4},
{"name": "NX", "bitOffset": 63, "bitWidth": 1}
]
},
{
"id": "400",
"kind": "Struct",
"name": "pdpte_1gb",
"structTypeName": "X64_PDPTE_HUGE",
"classKeyword": "bitfield",
"elementKind": "UInt64",
"offset": 0,
"parentId": "0",
"refId": "0",
"collapsed": true,
"arrayLen": 1,
"strLen": 64,
"bitfieldMembers": [
{"name": "Present", "bitOffset": 0, "bitWidth": 1},
{"name": "ReadWrite", "bitOffset": 1, "bitWidth": 1},
{"name": "UserSuper", "bitOffset": 2, "bitWidth": 1},
{"name": "WriteThrough", "bitOffset": 3, "bitWidth": 1},
{"name": "CacheDisable", "bitOffset": 4, "bitWidth": 1},
{"name": "Accessed", "bitOffset": 5, "bitWidth": 1},
{"name": "Dirty", "bitOffset": 6, "bitWidth": 1},
{"name": "PageSize", "bitOffset": 7, "bitWidth": 1},
{"name": "Global", "bitOffset": 8, "bitWidth": 1},
{"name": "AVL", "bitOffset": 9, "bitWidth": 3},
{"name": "PAT", "bitOffset": 12, "bitWidth": 1},
{"name": "Reserved", "bitOffset": 13, "bitWidth": 17},
{"name": "PhysAddr", "bitOffset": 30, "bitWidth": 22},
{"name": "Available", "bitOffset": 52, "bitWidth": 7},
{"name": "ProtKey", "bitOffset": 59, "bitWidth": 4},
{"name": "NX", "bitOffset": 63, "bitWidth": 1}
]
}
],
"rootIds": ["200"]
}

File diff suppressed because one or more lines are too long

1
src/examples/WinSDK.rcx Normal file

File diff suppressed because one or more lines are too long

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": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "27",
"kind": "Hex64",
"name": "field_70",
"offset": 112,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "28",
"kind": "Hex64",
"name": "field_78",
"offset": 120,
"parentId": "1",
"refId": "0",
"strLen": 64
}
]
}

10755
src/examples/t6zm.rcx Normal file

File diff suppressed because it is too large Load Diff

42817
src/examples/windows-x86_64.h Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

BIN
src/fonts/JetBrainsMono.ttf Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +1,6 @@
#include "core.h"
#include "addressparser.h"
#include <cmath>
#include <cstring>
#include <limits>
@@ -21,6 +23,14 @@ static QString fit(QString s, int w) {
return s.leftJustified(w, ' ');
}
// Like fit() but overflows instead of truncating: if s exceeds w, return full string
static QString fitOverflow(const QString& s, int w) {
if (w <= 0) return {};
if (s.size() <= w)
return s.leftJustified(w, ' ');
return s;
}
// ── Type name ──
// Override seam: injectable type-name provider
@@ -41,13 +51,15 @@ QString typeName(NodeKind kind, int colType) {
return fit(m ? QString::fromLatin1(m->typeName) : QStringLiteral("???"), colType);
}
// Array type string: "uint32_t[16]" or "char[64]"
QString arrayTypeName(NodeKind elemKind, int count) {
auto* m = kindMeta(elemKind);
QString elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
// char[] for UInt8, wchar_t[] for UInt16
if (elemKind == NodeKind::UInt8) elem = QStringLiteral("char");
else if (elemKind == NodeKind::UInt16) elem = QStringLiteral("wchar_t");
// Array type string: "uint32_t[16]" or "Material[2]"
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName) {
QString elem;
if (elemKind == NodeKind::Struct && !structName.isEmpty())
elem = structName;
else {
auto* m = kindMeta(elemKind);
elem = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
}
return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]");
}
@@ -61,29 +73,45 @@ QString pointerTypeName(NodeKind kind, const QString& targetName) {
// ── Value formatting ──
static QString hexVal(uint64_t v) {
return QStringLiteral("0x") + QString::number(v, 16);
return QString::asprintf("0x%llx", (unsigned long long)v);
}
static QString rawHex(uint64_t v, int digits) {
return QString::number(v, 16).rightJustified(digits, '0');
}
QString fmtInt8(int8_t v) { return hexVal((uint8_t)v); }
QString fmtInt16(int16_t v) { return hexVal((uint16_t)v); }
QString fmtInt32(int32_t v) { return hexVal((uint32_t)v); }
QString fmtInt64(int64_t v) { return hexVal((uint64_t)v); }
QString fmtInt8(int8_t v) { return QString::number(v); }
QString fmtInt16(int16_t v) { return QString::number(v); }
QString fmtInt32(int32_t v) { return QString::number(v); }
QString fmtInt64(int64_t v) { return QString::number((qlonglong)v); }
QString fmtUInt8(uint8_t v) { return hexVal(v); }
QString fmtUInt16(uint16_t v) { return hexVal(v); }
QString fmtUInt32(uint32_t v) { return hexVal(v); }
QString fmtUInt64(uint64_t v) { return hexVal(v); }
QString fmtFloat(float v) {
QString s = QString::number(v, 'g', 4);
if (!s.contains('.') && !s.contains('e') && !s.contains('E'))
s += QStringLiteral(".f");
else
s += QLatin1Char('f');
return s;
// Fixed 7-char body: digits + "." + decimals + "f"
// Negative values get a '-' prefix (8 chars total), positive stay 7.
if (std::isnan(v)) return QStringLiteral("NaN");
if (std::isinf(v)) return v > 0 ? QStringLiteral("inff") : QStringLiteral("-inff");
float av = std::fabs(v);
if (av >= 100000.f)
return v < 0 ? QStringLiteral("-99999+f") : QStringLiteral("99999+f");
// body = digits + "." + decimals + "f", target exactly 7 chars.
// Start with max decimals, reduce if integer part is wide or rounding overflows.
for (int dec = 4; dec >= 0; dec--) {
QString body = QString::number(av, 'f', dec);
body += (dec == 0) ? QStringLiteral(".f") : QStringLiteral("f");
if (body.size() == 7) {
if (v < 0.f) body.prepend('-');
return body;
}
}
// Rounding pushed past 99999 — use overflow cap
return v < 0 ? QStringLiteral("-99999+f") : QStringLiteral("99999+f");
}
QString fmtDouble(double v) {
QString s = QString::number(v, 'g', 6);
@@ -93,15 +121,8 @@ QString fmtDouble(double v) {
}
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
QString fmtPointer32(uint32_t v) {
if (v == 0) return QStringLiteral("-> NULL");
return QStringLiteral("-> ") + hexVal(v);
}
QString fmtPointer64(uint64_t v) {
if (v == 0) return QStringLiteral("-> NULL");
return QStringLiteral("-> ") + hexVal(v);
}
QString fmtPointer32(uint32_t v) { return hexVal(v); }
QString fmtPointer64(uint64_t v) { return hexVal(v); }
// ── Indentation ──
@@ -111,40 +132,52 @@ QString indent(int depth) {
// ── Offset margin ──
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation) {
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits) {
if (isContinuation) return QStringLiteral(" \u00B7 ");
return QString::number(absoluteOffset, 16).toUpper() + QChar(' ');
return QString::number(absoluteOffset, 16).toUpper()
.rightJustified(hexDigits, '0') + QChar(' ');
}
// ── Struct type name (for width calculation) ──
QString structTypeName(const Node& node) {
// Full type string: "struct TypeName" or just "struct" if no typename
QString base = typeName(node.kind).trimmed(); // "struct"
// Named types: just the type name (e.g. "_LIST_ENTRY")
// Anonymous: just the keyword (e.g. "union", "struct")
if (!node.structTypeName.isEmpty())
return base + QStringLiteral(" ") + node.structTypeName;
return base;
return node.structTypeName;
return node.resolvedClassKeyword();
}
// ── Struct header / footer ──
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName) {
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName, bool compact) {
// Columnar format: <type> <name> { (or no brace when collapsed)
QString ind = indent(depth);
QString type = fit(structTypeName(node), colType);
QString rawType = structTypeName(node);
QString suffix = collapsed ? QString() : QStringLiteral("{");
if (node.name.isEmpty()) {
// Anonymous struct/union: "union {" with no column padding
return ind + rawType + SEP + suffix;
}
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
return ind + type + SEP + node.name + SEP + suffix;
}
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
return indent(depth) + QStringLiteral("};");
QString fmtStructFooter(const Node& node, int depth, int /*totalSize*/) {
QString footer = indent(depth) + QStringLiteral("};");
if (node.resolvedClassKeyword() == QStringLiteral("enum"))
footer += QStringLiteral(" +10");
else
footer += QStringLiteral(" +10h +100h +1000h Trim");
return footer;
}
// ── Array header ──
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName) {
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName, bool compact) {
QString ind = indent(depth);
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen), colType);
QString rawType = arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
QString suffix = collapsed ? QString() : QStringLiteral("{");
return ind + type + SEP + node.name + SEP + suffix;
}
@@ -153,10 +186,16 @@ QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collap
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
const Provider& prov, uint64_t addr,
const QString& ptrTypeName, int colType, int colName) {
const QString& ptrTypeName, int colType, int colName,
bool compact) {
QString ind = indent(depth);
QString type = fit(ptrTypeName, colType);
bool overflow = compact && ptrTypeName.size() > colType;
QString type = compact ? fitOverflow(ptrTypeName, colType) : fit(ptrTypeName, colType);
if (collapsed) {
if (overflow) {
// Overflow: no column padding
return ind + type + SEP + node.name + SEP + readValue(node, prov, addr, 0);
}
// Collapsed: show pointer value instead of brace (name padded for value alignment)
QString name = fit(node.name, colName);
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
@@ -194,15 +233,18 @@ static QString bytesToAscii(const QByteArray& b, int slot) {
return out;
}
static const char kHexDigits[] = "0123456789ABCDEF";
static QString bytesToHex(const QByteArray& b, int slot) {
QString out;
out.reserve(slot * 3);
QChar buf[64]; // max slot=8 → 8*3-1=23 chars; 64 is plenty
int pos = 0;
for (int i = 0; i < slot; ++i) {
uint8_t c = (i < b.size()) ? (uint8_t)b[i] : 0;
out += QString::asprintf("%02X", (unsigned)c);
if (i + 1 < slot) out += ' ';
buf[pos++] = QLatin1Char(kHexDigits[c >> 4]);
buf[pos++] = QLatin1Char(kHexDigits[c & 0xF]);
if (i + 1 < slot) buf[pos++] = QLatin1Char(' ');
}
return out;
return QString(buf, pos);
}
static QString fmtAsciiAndBytes(const Provider& prov, uint64_t addr,
@@ -242,15 +284,55 @@ static QString readValueImpl(const Node& node, const Provider& prov,
if (!display) return rawHex(val, 8);
QString s = fmtPointer32(val);
QString sym = prov.getSymbol((uint64_t)val);
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::Pointer64: {
uint64_t val = prov.readU64(addr);
// Primitive pointer: dereference and show target value
// (hex/ptr/fnptr targets fall through to plain void* display)
if (node.ptrDepth > 0 && isValidPrimitivePtrTarget(node.elementKind) && val != 0) {
uint64_t target = val;
for (int d = 1; d < node.ptrDepth && target != 0; d++)
target = prov.isReadable(target, 8) ? prov.readU64(target) : 0;
if (target != 0 && prov.isReadable(target, sizeForKind(node.elementKind))) {
// Create a temporary node of the target kind to format the value
Node tmp;
tmp.kind = node.elementKind;
tmp.strLen = node.strLen;
QString derefVal = readValueImpl(tmp, prov, target, 0, mode);
if (display) {
QString arrow = QStringLiteral("-> ");
QString sym = prov.getSymbol(val);
if (!sym.isEmpty())
return arrow + derefVal + QStringLiteral(" // ") + sym;
return arrow + derefVal;
}
return derefVal;
}
if (!display) return rawHex(val, 16);
return fmtPointer64(val);
}
if (!display) return rawHex(val, 16);
QString s = fmtPointer64(val);
QString sym = prov.getSymbol(val);
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::FuncPtr32: {
uint32_t val = prov.readU32(addr);
if (!display) return rawHex(val, 8);
QString s = fmtPointer32(val);
QString sym = prov.getSymbol((uint64_t)val);
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::FuncPtr64: {
uint64_t val = prov.readU64(addr);
if (!display) return rawHex(val, 16);
QString s = fmtPointer64(val);
QString sym = prov.getSymbol(val);
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::Vec2:
@@ -259,21 +341,20 @@ static QString readValueImpl(const Node& node, const Provider& prov,
int count = sizeForKind(node.kind) / 4;
QStringList parts;
for (int i = 0; i < count; i++)
parts << fmtFloat(prov.readF32(addr + i * 4)).trimmed();
parts << fmtFloat(prov.readF32(addr + i * 4));
return parts.join(QStringLiteral(", "));
}
case NodeKind::Mat4x4: {
if (!display) return {}; // not editable as single value
if (subLine < 0 || subLine >= 4) return QStringLiteral("?");
QString line = QStringLiteral("[");
QString line = QStringLiteral("row%1 [").arg(subLine);
for (int c = 0; c < 4; c++) {
if (c > 0) line += QStringLiteral(", ");
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4)).trimmed();
line += fmtFloat(prov.readF32(addr + (subLine * 4 + c) * 4));
}
line += QStringLiteral("]");
return line;
}
case NodeKind::Padding: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2);
case NodeKind::UTF8: {
QByteArray bytes = prov.readBytes(addr, node.strLen);
int end = bytes.indexOf('\0');
@@ -306,48 +387,46 @@ QString readValue(const Node& node, const Provider& prov,
QString fmtNodeLine(const Node& node, const Provider& prov,
uint64_t addr, int depth, int subLine,
const QString& comment, int colType, int colName,
const QString& typeOverride) {
const QString& typeOverride, bool compact) {
QString ind = indent(depth);
QString type = typeOverride.isEmpty() ? typeName(node.kind, colType) : fit(typeOverride, colType);
QString name = fit(node.name, colName);
// Blank prefix for continuation lines (same width as type+sep+name+sep)
const int prefixW = colType + colName + 2 * kSepWidth;
// Comment suffix (padded or empty)
QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ')
// Compute raw type string for overflow detection
QString rawType = typeOverride.isEmpty() ? typeNameRaw(node.kind) : typeOverride;
bool overflow = compact && rawType.size() > colType;
QString type = overflow ? fitOverflow(rawType, colType)
: (typeOverride.isEmpty() ? typeName(node.kind, colType)
: fit(typeOverride, colType));
QString name = overflow ? node.name : fit(node.name, colName);
// Effective column width for this line (accounts for overflow, clamped to hard max)
int effectiveColType = overflow ? rawType.size() : colType;
// Blank prefix for continuation lines (same width as type+sep+name+sep)
const int prefixW = effectiveColType + (overflow ? name.size() : colName) + 2 * kSepWidth;
// Comment suffix (only present when a comment is provided; no trailing padding)
QString cmtSuffix = comment.isEmpty() ? QString()
: fit(comment, COL_COMMENT);
// Mat4x4: subLine 0..3 = rows
// Mat4x4: subLine 0..3 = rows — no truncation so large floats always display fully
if (node.kind == NodeKind::Mat4x4) {
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
QString val = readValue(node, prov, addr, subLine);
if (subLine == 0) return ind + type + SEP + name + SEP + val + cmtSuffix;
return ind + QString(prefixW, ' ') + val + cmtSuffix;
}
// Hex nodes and Padding: hex byte preview
// Hex nodes: hex byte preview (ASCII padded to colName to align with value column)
if (isHexPreview(node.kind)) {
if (node.kind == NodeKind::Padding) {
const int totalSz = qMax(1, node.arrayLen);
const int lineOff = subLine * 8;
const int lineBytes = qMin(8, totalSz - lineOff);
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
QString ascii = bytesToAscii(b, lineBytes);
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
if (subLine == 0)
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
}
// Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns
const int sz = sizeForKind(node.kind);
QByteArray b = prov.isReadable(addr, sz)
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
QString ascii = bytesToAscii(b, sz).leftJustified(8, ' ');
QString ascii = bytesToAscii(b, sz).leftJustified(colName, ' ');
QString hex = bytesToHex(b, sz).leftJustified(23, ' ');
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
}
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
QString val = overflow ? readValue(node, prov, addr, subLine)
: fit(readValue(node, prov, addr, subLine), COL_VALUE);
return ind + type + SEP + name + SEP + val + cmtSuffix;
}
@@ -537,6 +616,14 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
qulonglong val = stripHex(s).toULongLong(ok, 16);
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
}
case NodeKind::FuncPtr32: {
uint val = stripHex(s).toUInt(ok, 16);
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
}
case NodeKind::FuncPtr64: {
qulonglong val = stripHex(s).toULongLong(ok, 16);
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
}
case NodeKind::UTF8: {
*ok = true;
if (s.startsWith('"') && s.endsWith('"'))
@@ -565,7 +652,8 @@ QString validateValue(NodeKind kind, const QString& text) {
// For integer/hex types, validate character set first
bool isHexKind = (kind >= NodeKind::Hex8 && kind <= NodeKind::Hex64)
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64;
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64
|| kind == NodeKind::FuncPtr32 || kind == NodeKind::FuncPtr64;
bool isIntKind = (kind >= NodeKind::Int8 && kind <= NodeKind::UInt64);
if (isHexKind || isIntKind) {
@@ -573,8 +661,10 @@ QString validateValue(NodeKind kind, const QString& text) {
QString digits = hasHexPrefix ? s.mid(2) : s;
if (hasHexPrefix || isHexKind) {
// Hex mode: only 0-9, a-f, A-F
// Hex mode: only 0-9, a-f, A-F (spaces allowed for multi-byte hex kinds)
bool isMultiByteHex = (kind >= NodeKind::Hex16 && kind <= NodeKind::Hex64);
for (QChar c : digits) {
if (c == ' ' && isMultiByteHex) continue;
if (!c.isDigit() && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
return QStringLiteral("invalid hex '%1'").arg(c);
}
@@ -609,43 +699,42 @@ QString validateValue(NodeKind kind, const QString& text) {
return QStringLiteral("invalid");
}
// ── Base address validation (supports simple +/- equations) ──
// ── Base address validation (delegates to AddressParser) ──
QString validateBaseAddress(const QString& text) {
QString s = text.trimmed();
if (s.isEmpty()) return QStringLiteral("empty");
//s.remove('`');
return AddressParser::validate(s);
}
int pos = 0;
bool firstTerm = true;
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW) {
QString ind = indent(depth);
return ind + name.leftJustified(nameW) + QStringLiteral(" = ") + QString::number(value);
}
while (pos < s.size()) {
// Skip whitespace
while (pos < s.size() && s[pos].isSpace()) pos++;
if (pos >= s.size()) break;
// ── Bitfield member formatting ──
// Check for +/- operator (except first term)
if (!firstTerm) {
if (s[pos] == '+' || s[pos] == '-') pos++;
else return QStringLiteral("invalid '%1'").arg(s[pos]);
while (pos < s.size() && s[pos].isSpace()) pos++;
}
// Skip 0x prefix if present
if (pos + 1 < s.size() && s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X'))
pos += 2;
// Must have at least one hex digit
int numStart = pos;
while (pos < s.size() && (s[pos].isDigit() ||
(s[pos] >= 'a' && s[pos] <= 'f') ||
(s[pos] >= 'A' && s[pos] <= 'F'))) pos++;
if (pos == numStart) return QStringLiteral("invalid");
firstTerm = false;
uint64_t extractBits(const Provider& prov, uint64_t addr,
NodeKind containerKind,
uint8_t bitOffset, uint8_t bitWidth) {
uint64_t container = 0;
switch (containerKind) {
case NodeKind::Hex8: container = prov.readU8(addr); break;
case NodeKind::Hex16: container = prov.readU16(addr); break;
case NodeKind::Hex32: container = prov.readU32(addr); break;
default: container = prov.readU64(addr); break;
}
Q_ASSERT(bitOffset + bitWidth <= 64);
if (bitWidth >= 64) return container >> bitOffset;
return (container >> bitOffset) & ((1ULL << bitWidth) - 1);
}
return {};
QString fmtBitfieldMember(const QString& name, uint8_t bitWidth,
uint64_t value, int depth, int nameW) {
QString ind = indent(depth);
return ind + name.leftJustified(nameW)
+ QStringLiteral(" : %1 = %2").arg(bitWidth).arg(value);
}
} // namespace rcx::fmt

File diff suppressed because it is too large Load Diff

View File

@@ -6,14 +6,82 @@
namespace rcx {
// Generate C++ struct definitions for a single root struct and all
// nested/referenced types reachable from it.
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr);
// ── Code output format ──
// Generate C++ struct definitions for every root-level struct (full SDK).
enum class CodeFormat : int {
CppHeader = 0, // C/C++ struct definitions
RustStruct, // Rust #[repr(C)] struct definitions
DefineOffsets, // #define ClassName_FieldName 0xNN
CSharpStruct, // C# [StructLayout] with [FieldOffset]
PythonCtypes, // Python ctypes.Structure
_Count
};
enum class CodeScope : int {
Current = 0, // Just the selected struct
WithChildren, // Selected struct + all referenced types
FullSdk, // All root-level structs
_Count
};
const char* codeFormatName(CodeFormat fmt);
const char* codeFormatFileFilter(CodeFormat fmt);
const char* codeScopeName(CodeScope scope);
// ── Format-aware dispatch (calls the appropriate backend) ──
QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
// Render rootStructId + all struct types reachable from it
QString renderCodeTree(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCodeAll(CodeFormat fmt, const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
// ── Individual backends ──
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCppTree(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCppAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr);
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderRust(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderRustTree(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderRustAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderDefines(const NodeTree& tree, uint64_t rootStructId);
QString renderDefinesTree(const NodeTree& tree, uint64_t rootStructId);
QString renderDefinesAll(const NodeTree& tree);
QString renderCSharp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCSharpTree(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderCSharpAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases = nullptr,
bool emitAsserts = false);
QString renderPython(const NodeTree& tree, uint64_t rootStructId);
QString renderPythonTree(const NodeTree& tree, uint64_t rootStructId);
QString renderPythonAll(const NodeTree& tree);
// Null generator placeholder (returns empty string).
QString renderNull(const NodeTree& tree, uint64_t rootStructId);

BIN
src/icons/class.icns Normal file

Binary file not shown.

BIN
src/icons/class.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

BIN
src/icons/class.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

View File

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

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

1387
src/imports/import_pdb.cpp Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,63 @@
#pragma once
#include "core.h"
#include <QVector>
#include <functional>
namespace rcx {
// ── PDB Symbol Extraction ──
struct PdbSymbol {
QString name;
uint32_t rva;
uint32_t typeIndex = 0; // TPI type index (0 = unknown / public symbol)
};
struct PdbSymbolResult {
QString moduleName; // derived from PDB filename (e.g. "ntoskrnl")
QVector<PdbSymbol> symbols;
};
// Extract public/global symbols (name → RVA) from a PDB file.
// This reads the DBI stream's public and global symbol sub-streams.
PdbSymbolResult extractPdbSymbols(const QString& pdbPath,
QString* errorMsg = nullptr);
// ── PDB Type Import ──
struct PdbTypeInfo {
uint32_t typeIndex; // TPI type index
QString name; // struct/class/union/enum name
uint64_t size; // sizeof in bytes
int childCount; // direct member count
bool isUnion; // union vs struct/class
bool isEnum = false; // enum type
};
// Phase 1: Enumerate all UDT types in the PDB (fast scan, no recursive import).
QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath,
QString* errorMsg = nullptr);
// Phase 2: Import selected types with full recursive child types.
// progressCb is called with (current, total) for each top-level type;
// return false from the callback to cancel the import.
using ProgressCb = std::function<bool(int current, int total)>;
NodeTree importPdbSelected(const QString& pdbPath,
const QVector<uint32_t>& typeIndices,
QString* errorMsg = nullptr,
ProgressCb progressCb = {});
// Legacy single-call API: import one struct by name (or all if filter empty).
NodeTree importPdb(const QString& pdbPath,
const QString& structFilter = {},
QString* errorMsg = nullptr);
// Import the type associated with a global symbol's typeIndex.
// Opens the PDB, resolves the typeIndex to a UDT/enum, and returns the imported tree.
// Returns empty tree if the symbol has no associated type or the type is a simple primitive.
NodeTree importTypeForSymbol(const QString& pdbPath,
uint32_t typeIndex,
QString* typeName = nullptr,
QString* errorMsg = nullptr);
} // namespace rcx

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

View File

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

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