mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
16 Commits
snapshot-2
...
snapshot-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4706f7b782 | ||
|
|
fe9bfafa3b | ||
|
|
ff928df685 | ||
|
|
d6e3c182fc | ||
|
|
078a6028f0 | ||
|
|
d7a6e1862e | ||
|
|
1ddf47a754 | ||
|
|
1a885a8b1d | ||
|
|
67218d3e48 | ||
|
|
f651edd740 | ||
|
|
25aaace382 | ||
|
|
b5ddb042b8 | ||
|
|
e900dea836 | ||
|
|
b647a334bc | ||
|
|
fc390bc1f7 | ||
|
|
7efe740ec1 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -49,6 +49,7 @@ jobs:
|
||||
- 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/
|
||||
@@ -57,6 +58,7 @@ jobs:
|
||||
cp -r build/styles release/ 2>/dev/null || true
|
||||
cp -r build/imageformats release/ 2>/dev/null || true
|
||||
cp -r build/iconengines release/ 2>/dev/null || true
|
||||
windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw release/Reclass.exe
|
||||
mkdir -p release/Plugins
|
||||
cp build/Plugins/*.dll release/Plugins/ 2>/dev/null || true
|
||||
cp -r build/themes release/ 2>/dev/null || true
|
||||
|
||||
@@ -372,6 +372,21 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||
|
||||
add_executable(test_source_provider tests/test_source_provider.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
src/typeselectorpopup.cpp
|
||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
|
||||
src/resources.qrc)
|
||||
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
|
||||
target_link_libraries(test_source_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
|
||||
QScintilla::QScintilla)
|
||||
if(WIN32)
|
||||
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||
endif()
|
||||
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||
@@ -381,6 +396,19 @@ if(BUILD_TESTING)
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_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)
|
||||
|
||||
# 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)
|
||||
|
||||
18
README.md
18
README.md
@@ -1,6 +1,9 @@
|
||||
<div align="center">
|
||||
|
||||
# Reclass
|
||||
<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>
|
||||
|
||||
**A structured binary editor for reverse engineering — inspect raw bytes as typed structs, arrays, and pointers.<p>A complete overhaul of the popular "reclassing" tools**
|
||||
|
||||
@@ -38,10 +41,23 @@ Built with C++17, Qt 6, and QScintilla. The entire editor surface is rendered as
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Process memory section enumeration
|
||||
- [ ] Address parser auto-complete
|
||||
- [ ] Safe mode
|
||||
- [ ] File import for other Reclass instances
|
||||
- [ ] Expose UI functionality to plugins
|
||||
- [ ] iOS/macOS support
|
||||
- [ ] Display RTTI information
|
||||
|
||||
---
|
||||
|
||||
## Data Sources
|
||||
|
||||
- **File** — open any binary file and inspect its contents as structured data
|
||||
- **Process** — attach to a live process and read its memory in real time
|
||||
- **Remote Process** — read another process's memory via shared memory
|
||||
- **WinDbg** — load `.dmp` crash dump files or connect to live debugging sessions
|
||||
|
||||
---
|
||||
|
||||
160
docs/RECLASS_DARKMODE.svg
Normal file
160
docs/RECLASS_DARKMODE.svg
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 186.01 52.79">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#Unbenannter_Verlauf_130-2);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#Unbenannter_Verlauf_236-2);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#Unbenannter_Verlauf_225-2);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #1f2939;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #5d9bd4;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #1e3e88;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: #6e809a;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: url(#Unbenannter_Verlauf_225);
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
fill: url(#Unbenannter_Verlauf_236);
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
fill: url(#Unbenannter_Verlauf_130);
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
fill: url(#Unbenannter_Verlauf_170);
|
||||
}
|
||||
|
||||
.cls-12 {
|
||||
fill: url(#Unbenannter_Verlauf_161);
|
||||
}
|
||||
|
||||
.cls-13 {
|
||||
fill: url(#Unbenannter_Verlauf_183);
|
||||
}
|
||||
|
||||
.cls-14 {
|
||||
fill: #b06ba9;
|
||||
}
|
||||
|
||||
.cls-15 {
|
||||
fill: #826415;
|
||||
}
|
||||
|
||||
.cls-16 {
|
||||
fill: #e2aa11;
|
||||
}
|
||||
|
||||
.cls-17 {
|
||||
fill: #893089;
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.33" y1="8.33" x2="18.11" y2="18.11" gradientTransform="translate(13.22 -5.47) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f3db78"/>
|
||||
<stop offset=".19" stop-color="#f4e188"/>
|
||||
<stop offset=".34" stop-color="#f4e38d"/>
|
||||
<stop offset=".38" stop-color="#f4df81"/>
|
||||
<stop offset=".47" stop-color="#f5d86f"/>
|
||||
<stop offset=".57" stop-color="#f5d463"/>
|
||||
<stop offset=".67" stop-color="#f6d360"/>
|
||||
<stop offset=".89" stop-color="#f1cc53"/>
|
||||
<stop offset="1" stop-color="#efbe33"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.46" x2="10.98" y2="26.03" gradientTransform="translate(-4.95 39.45) rotate(-135)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".18" stop-color="#e2aa11"/>
|
||||
<stop offset=".91" stop-color="#826415"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.46" y1=".41" x2="26.03" y2="10.98" gradientTransform="translate(31.39 24.39) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.97" y1="15.65" x2="42.34" y2="23.02" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#deb0d3"/>
|
||||
<stop offset=".15" stop-color="#e1b5d6"/>
|
||||
<stop offset=".3" stop-color="#e3b8d7"/>
|
||||
<stop offset=".4" stop-color="#d7a8cd"/>
|
||||
<stop offset=".53" stop-color="#cf9cc7"/>
|
||||
<stop offset=".67" stop-color="#cd99c5"/>
|
||||
<stop offset=".89" stop-color="#c68abc"/>
|
||||
<stop offset="1" stop-color="#bb7db4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.78" y1="20.14" x2="36.87" y2="28.24" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#b06ba9"/>
|
||||
<stop offset=".87" stop-color="#893089"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.45" y1="9.43" x2="47.55" y2="17.53" xlink:href="#Unbenannter_Verlauf_225"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.88" y1="39.45" x2="42.29" y2="46.86" gradientTransform="translate(41.82 -14.64) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#91c4eb"/>
|
||||
<stop offset=".2" stop-color="#9dc9ed"/>
|
||||
<stop offset=".33" stop-color="#96c6ec"/>
|
||||
<stop offset=".35" stop-color="#91c3ea"/>
|
||||
<stop offset=".45" stop-color="#7fb8e5"/>
|
||||
<stop offset=".56" stop-color="#73b2e2"/>
|
||||
<stop offset=".67" stop-color="#70b0e1"/>
|
||||
<stop offset=".89" stop-color="#60a7dc"/>
|
||||
<stop offset="1" stop-color="#4d9bd5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.83" y1="43.9" x2="36.92" y2="51.99" gradientTransform="translate(22.68 105.31) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#5d9bd4"/>
|
||||
<stop offset=".87" stop-color="#1e3e88"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.51" y1="33.23" x2="47.59" y2="41.32" gradientTransform="translate(48.45 94.63) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
|
||||
</defs>
|
||||
<g>
|
||||
<rect class="cls-7" x="22.48" y="16.85" width="1.76" height="25.1"/>
|
||||
<rect class="cls-7" x="17.08" y="16.85" width="19.7" height="1.82"/>
|
||||
<rect class="cls-7" x="22.48" y="40.19" width="11.9" height="1.76"/>
|
||||
<g>
|
||||
<rect class="cls-12" x="2.56" y="6.31" width="21.31" height="13.82" transform="translate(-5.48 13.22) rotate(-45)"/>
|
||||
<rect class="cls-15" x="17.52" y="6.88" width="1.15" height="22.44" transform="translate(18.1 -7.49) rotate(45)"/>
|
||||
<g>
|
||||
<rect class="cls-16" x="7.76" y="-2.88" width="1.15" height="22.44" transform="translate(8.34 -3.45) rotate(45)"/>
|
||||
<rect class="cls-10" x="5.12" y="13.27" width="1.15" height="14.95" transform="translate(24.39 31.39) rotate(135)"/>
|
||||
<rect class="cls-1" x="20.17" y="-1.78" width="1.15" height="14.95" transform="translate(39.45 -4.95) rotate(135)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="cls-11" points="40.33 10.29 29.64 21.02 36.98 28.38 47.67 17.66 40.33 10.29"/>
|
||||
<polygon class="cls-17" points="37.01 29.1 36.29 28.38 47.68 16.96 48.39 17.68 37.01 29.1"/>
|
||||
<polygon class="cls-14" points="29.67 21.74 28.95 21.02 40.34 9.6 41.05 10.31 29.67 21.74"/>
|
||||
<polygon class="cls-8" points="28.95 21.02 29.67 20.3 37.72 28.38 37 29.1 28.95 21.02"/>
|
||||
<polygon class="cls-3" points="39.63 10.31 40.34 9.6 48.39 17.67 47.68 18.39 39.63 10.31"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect class="cls-13" x="30.96" y="37.92" width="15.26" height="10.48" transform="translate(-19.21 39.92) rotate(-45)"/>
|
||||
<g>
|
||||
<rect class="cls-9" x="32.83" y="42.71" width="1.01" height="11.38" transform="translate(91.13 59.06) rotate(135)"/>
|
||||
<rect class="cls-2" x="43.5" y="32.04" width="1.01" height="11.38" transform="translate(101.81 33.29) rotate(135)"/>
|
||||
<rect class="cls-6" x="41.84" y="38.69" width="1.01" height="16.1" transform="translate(45.45 -16.25) rotate(45)"/>
|
||||
<rect class="cls-5" x="34.5" y="31.35" width="1.01" height="16.1" transform="translate(38.11 -13.21) rotate(45)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-4" d="M53.66,30.51v11.46h-2.57v-25.13h9.36c5.04,0,7.72,2.72,7.72,6.65,0,3.22-1.89,5.26-4.51,5.89,2.34.58,4.09,2.2,4.09,6.5v1.02c0,1.74-.11,4.07.33,5.07h-2.54c-.46-1.08-.39-3.1-.39-5.34v-.6c0-3.87-1.12-5.52-5.79-5.52h-5.7ZM53.66,28.26h5.79c4.15,0,6.03-1.56,6.03-4.64,0-2.9-1.89-4.54-5.57-4.54h-6.25v9.18Z"/>
|
||||
<path class="cls-4" d="M86.55,29.87h-12.65v9.79h13.88l-.35,2.3h-16.06v-25.12h15.81v2.27h-13.28v8.49h12.65v2.27Z"/>
|
||||
<path class="cls-4" d="M109.12,35.04c-1.15,4.11-4.2,7.19-9.68,7.19-7.34,0-11.13-5.72-11.13-12.79s3.76-12.96,11.21-12.96c5.64,0,8.84,3.18,9.63,7.37h-2.56c-1.04-3.02-3.01-5.13-7.18-5.13-5.92,0-8.38,5.4-8.38,10.66s2.39,10.62,8.52,10.62c3.99,0,5.89-2.16,7.01-4.95h2.57Z"/>
|
||||
<path class="cls-4" d="M111.62,16.84h2.56v22.82h13.3l-.41,2.27h-15.46v-25.09Z"/>
|
||||
<path class="cls-4" d="M133.03,33.77l-2.97,8.16h-2.58l9.09-25.09h3.11l9.48,25.09h-2.76l-3.05-8.16h-10.32ZM142.61,31.5c-2.61-7.07-3.99-10.62-4.51-12.4h-.04c-.61,2-2.16,6.36-4.27,12.4h8.82Z"/>
|
||||
<path class="cls-4" d="M151.68,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.24,4.56,8.24,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
|
||||
<path class="cls-4" d="M170.59,35.08c.72,3.19,2.87,5,6.77,5,4.28,0,5.95-2.09,5.95-4.65,0-2.69-1.25-4.29-6.55-5.59-5.58-1.38-7.76-3.23-7.76-6.81s2.56-6.55,8.04-6.55,8.11,3.41,8.44,6.57h-2.63c-.52-2.48-2.11-4.37-5.93-4.37-3.37,0-5.22,1.55-5.22,4.16s1.54,3.59,6.07,4.7c7.1,1.75,8.25,4.56,8.25,7.67,0,3.85-2.83,7.03-8.78,7.03-6.29,0-8.78-3.56-9.27-7.15h2.63Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
160
docs/RECLASS_LIGHTMODE.svg
Normal file
160
docs/RECLASS_LIGHTMODE.svg
Normal file
@@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 185.55 52.66">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: url(#Unbenannter_Verlauf_130-2);
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: url(#Unbenannter_Verlauf_236-2);
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#Unbenannter_Verlauf_225-2);
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #5d9bd4;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: #e3e8f0;
|
||||
}
|
||||
|
||||
.cls-6 {
|
||||
fill: #1e3e88;
|
||||
}
|
||||
|
||||
.cls-7 {
|
||||
fill: #6e809a;
|
||||
}
|
||||
|
||||
.cls-8 {
|
||||
fill: url(#Unbenannter_Verlauf_225);
|
||||
}
|
||||
|
||||
.cls-9 {
|
||||
fill: url(#Unbenannter_Verlauf_236);
|
||||
}
|
||||
|
||||
.cls-10 {
|
||||
fill: url(#Unbenannter_Verlauf_130);
|
||||
}
|
||||
|
||||
.cls-11 {
|
||||
fill: url(#Unbenannter_Verlauf_170);
|
||||
}
|
||||
|
||||
.cls-12 {
|
||||
fill: url(#Unbenannter_Verlauf_161);
|
||||
}
|
||||
|
||||
.cls-13 {
|
||||
fill: url(#Unbenannter_Verlauf_183);
|
||||
}
|
||||
|
||||
.cls-14 {
|
||||
fill: #b06ba9;
|
||||
}
|
||||
|
||||
.cls-15 {
|
||||
fill: #826415;
|
||||
}
|
||||
|
||||
.cls-16 {
|
||||
fill: #e2aa11;
|
||||
}
|
||||
|
||||
.cls-17 {
|
||||
fill: #893089;
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="Unbenannter_Verlauf_161" data-name="Unbenannter Verlauf 161" x1="8.31" y1="8.31" x2="18.06" y2="18.06" gradientTransform="translate(13.19 -5.46) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f3db78"/>
|
||||
<stop offset=".19" stop-color="#f4e188"/>
|
||||
<stop offset=".34" stop-color="#f4e38d"/>
|
||||
<stop offset=".38" stop-color="#f4df81"/>
|
||||
<stop offset=".47" stop-color="#f5d86f"/>
|
||||
<stop offset=".57" stop-color="#f5d463"/>
|
||||
<stop offset=".67" stop-color="#f6d360"/>
|
||||
<stop offset=".89" stop-color="#f1cc53"/>
|
||||
<stop offset="1" stop-color="#efbe33"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130" data-name="Unbenannter Verlauf 130" x1=".41" y1="15.42" x2="10.95" y2="25.97" gradientTransform="translate(-4.94 39.35) rotate(-135)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".18" stop-color="#e2aa11"/>
|
||||
<stop offset=".91" stop-color="#826415"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_130-2" data-name="Unbenannter Verlauf 130" x1="15.42" y1=".41" x2="25.97" y2="10.95" gradientTransform="translate(31.32 24.33) rotate(-135)" xlink:href="#Unbenannter_Verlauf_130"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_170" data-name="Unbenannter Verlauf 170" x1="34.88" y1="15.61" x2="42.24" y2="22.97" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#deb0d3"/>
|
||||
<stop offset=".15" stop-color="#e1b5d6"/>
|
||||
<stop offset=".3" stop-color="#e3b8d7"/>
|
||||
<stop offset=".4" stop-color="#d7a8cd"/>
|
||||
<stop offset=".53" stop-color="#cf9cc7"/>
|
||||
<stop offset=".67" stop-color="#cd99c5"/>
|
||||
<stop offset=".89" stop-color="#c68abc"/>
|
||||
<stop offset="1" stop-color="#bb7db4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225" data-name="Unbenannter Verlauf 225" x1="28.7" y1="20.09" x2="36.78" y2="28.17" gradientTransform="translate(.63 .63) rotate(-.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#b06ba9"/>
|
||||
<stop offset=".87" stop-color="#893089"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_225-2" data-name="Unbenannter Verlauf 225" x1="39.35" y1="9.41" x2="47.43" y2="17.49" xlink:href="#Unbenannter_Verlauf_225"/>
|
||||
<linearGradient id="Unbenannter_Verlauf_183" data-name="Unbenannter Verlauf 183" x1="34.79" y1="39.35" x2="42.18" y2="46.74" gradientTransform="translate(41.71 -14.61) rotate(45)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#91c4eb"/>
|
||||
<stop offset=".2" stop-color="#9dc9ed"/>
|
||||
<stop offset=".33" stop-color="#96c6ec"/>
|
||||
<stop offset=".35" stop-color="#91c3ea"/>
|
||||
<stop offset=".45" stop-color="#7fb8e5"/>
|
||||
<stop offset=".56" stop-color="#73b2e2"/>
|
||||
<stop offset=".67" stop-color="#70b0e1"/>
|
||||
<stop offset=".89" stop-color="#60a7dc"/>
|
||||
<stop offset="1" stop-color="#4d9bd5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236" data-name="Unbenannter Verlauf 236" x1="28.76" y1="43.79" x2="36.83" y2="51.86" gradientTransform="translate(22.62 105.05) rotate(-135.12) skewX(-.25)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset=".19" stop-color="#5d9bd4"/>
|
||||
<stop offset=".87" stop-color="#1e3e88"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="Unbenannter_Verlauf_236-2" data-name="Unbenannter Verlauf 236" x1="39.41" y1="33.15" x2="47.47" y2="41.21" gradientTransform="translate(48.33 94.4) rotate(-135.12) skewX(-.25)" xlink:href="#Unbenannter_Verlauf_236"/>
|
||||
</defs>
|
||||
<g>
|
||||
<rect class="cls-7" x="22.43" y="16.81" width="1.75" height="25.04"/>
|
||||
<rect class="cls-7" x="17.04" y="16.81" width="19.66" height="1.82"/>
|
||||
<rect class="cls-7" x="22.43" y="40.09" width="11.87" height="1.75"/>
|
||||
<g>
|
||||
<rect class="cls-12" x="2.56" y="6.29" width="21.26" height="13.79" transform="translate(-5.46 13.19) rotate(-45)"/>
|
||||
<rect class="cls-15" x="17.48" y="6.87" width="1.15" height="22.38" transform="translate(18.06 -7.47) rotate(45)"/>
|
||||
<g>
|
||||
<rect class="cls-16" x="7.74" y="-2.87" width="1.15" height="22.38" transform="translate(8.32 -3.45) rotate(45)"/>
|
||||
<rect class="cls-10" x="5.1" y="13.24" width="1.15" height="14.92" transform="translate(24.33 31.32) rotate(135)"/>
|
||||
<rect class="cls-1" x="20.12" y="-1.78" width="1.15" height="14.92" transform="translate(39.35 -4.94) rotate(135)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="cls-11" points="40.23 10.26 29.56 20.96 36.89 28.31 47.56 17.61 40.23 10.26"/>
|
||||
<polygon class="cls-17" points="36.91 29.03 36.2 28.31 47.56 16.92 48.27 17.63 36.91 29.03"/>
|
||||
<polygon class="cls-14" points="29.59 21.68 28.88 20.97 40.24 9.57 40.95 10.29 29.59 21.68"/>
|
||||
<polygon class="cls-8" points="28.88 20.97 29.59 20.25 37.62 28.31 36.91 29.02 28.88 20.97"/>
|
||||
<polygon class="cls-3" points="39.53 10.29 40.24 9.57 48.27 17.63 47.56 18.34 39.53 10.29"/>
|
||||
</g>
|
||||
<g>
|
||||
<rect class="cls-13" x="30.88" y="37.82" width="15.22" height="10.45" transform="translate(-19.17 39.82) rotate(-45)"/>
|
||||
<g>
|
||||
<rect class="cls-9" x="32.75" y="42.61" width="1.01" height="11.36" transform="translate(90.91 58.91) rotate(135)"/>
|
||||
<rect class="cls-2" x="43.4" y="31.96" width="1.01" height="11.36" transform="translate(101.56 33.21) rotate(135)"/>
|
||||
<rect class="cls-6" x="41.73" y="38.59" width="1.01" height="16.06" transform="translate(45.34 -16.21) rotate(45)"/>
|
||||
<rect class="cls-4" x="34.41" y="31.27" width="1.01" height="16.06" transform="translate(38.02 -13.18) rotate(45)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-5" d="M53.53,30.44v11.43h-2.56v-25.07h9.34c5.03,0,7.7,2.71,7.7,6.63,0,3.21-1.88,5.24-4.5,5.87,2.33.58,4.08,2.19,4.08,6.49v1.01c0,1.74-.11,4.06.33,5.06h-2.54c-.46-1.08-.39-3.09-.39-5.33v-.59c0-3.87-1.12-5.51-5.78-5.51h-5.68ZM53.53,28.19h5.77c4.14,0,6.02-1.55,6.02-4.63,0-2.89-1.88-4.53-5.55-4.53h-6.24v9.16Z"/>
|
||||
<path class="cls-5" d="M86.34,29.8h-12.62v9.77h13.84l-.35,2.3h-16.02v-25.06h15.77v2.26h-13.25v8.47h12.62v2.26Z"/>
|
||||
<path class="cls-5" d="M108.85,34.96c-1.15,4.09-4.19,7.17-9.65,7.17-7.32,0-11.11-5.7-11.11-12.76s3.75-12.93,11.18-12.93c5.63,0,8.82,3.17,9.6,7.35h-2.56c-1.03-3.02-3-5.12-7.16-5.12-5.91,0-8.36,5.39-8.36,10.63s2.38,10.6,8.5,10.6c3.98,0,5.88-2.16,6.99-4.94h2.56Z"/>
|
||||
<path class="cls-5" d="M111.34,16.8h2.56v22.77h13.27l-.4,2.26h-15.42v-25.03Z"/>
|
||||
<path class="cls-5" d="M132.69,33.69l-2.96,8.14h-2.57l9.07-25.03h3.1l9.45,25.03h-2.75l-3.04-8.14h-10.3ZM142.25,31.43c-2.61-7.05-3.98-10.59-4.5-12.37h-.04c-.61,2-2.15,6.34-4.26,12.37h8.8Z"/>
|
||||
<path class="cls-5" d="M151.31,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
|
||||
<path class="cls-5" d="M170.17,34.99c.72,3.18,2.86,4.99,6.75,4.99,4.27,0,5.93-2.08,5.93-4.64,0-2.68-1.24-4.28-6.53-5.57-5.57-1.37-7.75-3.23-7.75-6.8s2.55-6.54,8.02-6.54,8.09,3.4,8.42,6.55h-2.62c-.52-2.48-2.11-4.36-5.91-4.36-3.36,0-5.21,1.54-5.21,4.15s1.54,3.58,6.05,4.69c7.09,1.75,8.22,4.55,8.22,7.65,0,3.84-2.82,7.02-8.76,7.02-6.27,0-8.75-3.55-9.24-7.13h2.62Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
@@ -5,9 +5,7 @@
|
||||
#include <QStyle>
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QInputDialog>
|
||||
#include <QPushButton>
|
||||
#include <QUuid>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QPixmap>
|
||||
@@ -65,12 +63,12 @@ struct IpcClient {
|
||||
|
||||
/* ── connect / disconnect ──────────────────────────────────────── */
|
||||
|
||||
bool connect(uint32_t pid, const QByteArray& nonce, int timeoutMs = 5000)
|
||||
bool connect(uint32_t pid, int timeoutMs = 5000)
|
||||
{
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce.constData());
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce.constData());
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce.constData());
|
||||
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) */
|
||||
@@ -373,51 +371,6 @@ static QString payloadPath()
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Create bootstrap shared memory with the nonce */
|
||||
static bool createBootstrapShm(uint32_t pid, const QByteArray& nonce)
|
||||
{
|
||||
char bootName[128];
|
||||
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE hBoot = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
|
||||
PAGE_READWRITE, 0, RCX_RPC_BOOT_SIZE,
|
||||
bootName);
|
||||
if (!hBoot) return false;
|
||||
|
||||
auto* view = static_cast<RcxRpcBootHeader*>(
|
||||
MapViewOfFile(hBoot, FILE_MAP_WRITE, 0, 0, RCX_RPC_BOOT_SIZE));
|
||||
if (!view) { CloseHandle(hBoot); return false; }
|
||||
|
||||
memset(view, 0, RCX_RPC_BOOT_SIZE);
|
||||
view->nonceLength = (uint32_t)nonce.size();
|
||||
memcpy(view->nonce, nonce.constData(), qMin(nonce.size(), 59));
|
||||
|
||||
UnmapViewOfFile(view);
|
||||
/* keep hBoot open until payload reads it (payload unlinks after reading) */
|
||||
/* leak intentional: closed when process exits or payload consumes it */
|
||||
return true;
|
||||
#else
|
||||
int fd = shm_open(bootName, O_CREAT | O_RDWR, 0600);
|
||||
if (fd < 0) return false;
|
||||
if (ftruncate(fd, RCX_RPC_BOOT_SIZE) != 0) { close(fd); return false; }
|
||||
|
||||
void* view = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, fd, 0);
|
||||
close(fd);
|
||||
if (view == MAP_FAILED) return false;
|
||||
|
||||
auto* boot = static_cast<RcxRpcBootHeader*>(view);
|
||||
memset(boot, 0, RCX_RPC_BOOT_SIZE);
|
||||
boot->nonceLength = (uint32_t)nonce.size();
|
||||
memcpy(boot->nonce, nonce.constData(), qMin(nonce.size(), 59));
|
||||
|
||||
munmap(view, RCX_RPC_BOOT_SIZE);
|
||||
/* payload unlinks after consuming */
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* ── Windows injection: CreateRemoteThread + LoadLibraryA ─────────── */
|
||||
|
||||
@@ -447,7 +400,7 @@ static bool injectPayload(uint32_t pid, QString* errorMsg)
|
||||
|
||||
WriteProcessMemory(hProc, remotePath, pathUtf8.constData(), pathLen, nullptr);
|
||||
|
||||
/* create remote thread calling LoadLibraryA(path) */
|
||||
/* Step 1: LoadLibraryA — loads the DLL (DllMain is minimal) */
|
||||
HMODULE hK32 = GetModuleHandleA("kernel32.dll");
|
||||
auto pLoadLib = reinterpret_cast<LPTHREAD_START_ROUTINE>(
|
||||
GetProcAddress(hK32, "LoadLibraryA"));
|
||||
@@ -464,19 +417,81 @@ static bool injectPayload(uint32_t pid, QString* errorMsg)
|
||||
|
||||
WaitForSingleObject(hThread, 10000);
|
||||
|
||||
/* check if LoadLibrary returned non-null */
|
||||
DWORD exitCode = 0;
|
||||
GetExitCodeThread(hThread, &exitCode);
|
||||
CloseHandle(hThread);
|
||||
|
||||
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
|
||||
CloseHandle(hProc);
|
||||
|
||||
if (exitCode == 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -717,24 +732,23 @@ bool RemoteProcessMemoryPlugin::canHandle(const QString& target) const
|
||||
std::unique_ptr<rcx::Provider>
|
||||
RemoteProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
||||
{
|
||||
/* target = "rpm:{pid}:{nonce}:{name}" */
|
||||
/* target = "rpm:{pid}:{name}" */
|
||||
QStringList parts = target.split(':');
|
||||
if (parts.size() < 4 || parts[0] != QStringLiteral("rpm")) {
|
||||
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 nonce = parts[2];
|
||||
QString name = parts.mid(3).join(':'); /* name may contain colons */
|
||||
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, nonce, errorMsg);
|
||||
auto ipc = getOrCreateConnection(pid, errorMsg);
|
||||
if (!ipc) return nullptr;
|
||||
|
||||
return std::make_unique<RemoteProcessProvider>(pid, name, ipc);
|
||||
@@ -745,7 +759,7 @@ uint64_t RemoteProcessMemoryPlugin::getInitialBaseAddress(const QString& target)
|
||||
/* Read imageBase directly from the shared-memory header -- zero IPC cost.
|
||||
The payload filled it at init from PEB->Ldr (Win) / /proc/self/maps (Linux). */
|
||||
QStringList parts = target.split(':');
|
||||
if (parts.size() < 3 || parts[0] != QStringLiteral("rpm"))
|
||||
if (parts.size() < 2 || parts[0] != QStringLiteral("rpm"))
|
||||
return 0;
|
||||
|
||||
bool ok;
|
||||
@@ -793,35 +807,17 @@ bool RemoteProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
|
||||
QAbstractButton* clicked = box.clickedButton();
|
||||
if (clicked == injectBtn) {
|
||||
/* generate nonce */
|
||||
QString nonce = QUuid::createUuid().toString(QUuid::Id128).left(16);
|
||||
QByteArray nonceUtf8 = nonce.toUtf8();
|
||||
|
||||
/* create bootstrap, inject */
|
||||
if (!createBootstrapShm(pid, nonceUtf8)) {
|
||||
QMessageBox::critical(parent, QStringLiteral("Error"),
|
||||
QStringLiteral("Failed to create bootstrap shared memory."));
|
||||
return false;
|
||||
}
|
||||
|
||||
QString injectErr;
|
||||
if (!injectPayload(pid, &injectErr)) {
|
||||
QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr);
|
||||
return false;
|
||||
}
|
||||
|
||||
*target = QStringLiteral("rpm:%1:%2:%3").arg(pid).arg(nonce, name);
|
||||
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
else if (clicked == connectBtn) {
|
||||
bool ok;
|
||||
QString nonce = QInputDialog::getText(parent,
|
||||
QStringLiteral("Connect to Payload"),
|
||||
QStringLiteral("Enter the payload nonce:"),
|
||||
QLineEdit::Normal, QString(), &ok);
|
||||
if (!ok || nonce.isEmpty()) return false;
|
||||
|
||||
*target = QStringLiteral("rpm:%1:%2:%3").arg(pid).arg(nonce, name);
|
||||
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -903,7 +899,7 @@ QVector<PluginProcessInfo> RemoteProcessMemoryPlugin::enumerateProcesses()
|
||||
|
||||
std::shared_ptr<IpcClient>
|
||||
RemoteProcessMemoryPlugin::getOrCreateConnection(
|
||||
uint32_t pid, const QString& nonce, QString* errorMsg)
|
||||
uint32_t pid, QString* errorMsg)
|
||||
{
|
||||
QMutexLocker lock(&m_connectionsMutex);
|
||||
|
||||
@@ -912,7 +908,7 @@ RemoteProcessMemoryPlugin::getOrCreateConnection(
|
||||
return *it;
|
||||
|
||||
auto ipc = std::make_shared<IpcClient>();
|
||||
if (!ipc->connect(pid, nonce.toUtf8())) {
|
||||
if (!ipc->connect(pid)) {
|
||||
if (errorMsg)
|
||||
*errorMsg = QStringLiteral("Failed to connect IPC to PID %1.\n"
|
||||
"Is the payload running?").arg(pid);
|
||||
|
||||
@@ -77,7 +77,7 @@ public:
|
||||
|
||||
private:
|
||||
std::shared_ptr<IpcClient> getOrCreateConnection(
|
||||
uint32_t pid, const QString& nonce, QString* errorMsg);
|
||||
uint32_t pid, QString* errorMsg);
|
||||
|
||||
mutable QMutex m_connectionsMutex;
|
||||
QHash<uint32_t, std::shared_ptr<IpcClient>> m_connections;
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
* rcx_payload -- injected into target process.
|
||||
*
|
||||
* Pure Win32 / POSIX, NO Qt, minimal footprint.
|
||||
* Reads a nonce from bootstrap shared memory, creates the main IPC
|
||||
* channel (shared memory + events/semaphores), and runs a server
|
||||
* thread that handles RPC commands from the editor plugin.
|
||||
* 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"
|
||||
@@ -18,12 +17,13 @@
|
||||
#include <psapi.h>
|
||||
|
||||
/* ── globals ──────────────────────────────────────────────────────── */
|
||||
static HANDLE g_hShm = nullptr;
|
||||
static void* g_mappedView = nullptr;
|
||||
static HANDLE g_hReqEvent = nullptr;
|
||||
static HANDLE g_hRspEvent = nullptr;
|
||||
static HANDLE g_hThread = nullptr;
|
||||
static volatile LONG g_shutdown = 0;
|
||||
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 ────────────────────────────────── */
|
||||
|
||||
@@ -167,135 +167,147 @@ static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data)
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
}
|
||||
|
||||
/* ── server thread ────────────────────────────────────────────────── */
|
||||
/* forward declaration */
|
||||
void RcxPayloadCleanup();
|
||||
|
||||
static DWORD WINAPI ServerThread(LPVOID)
|
||||
/* ── timer callback (non-blocking poll) ───────────────────────────── */
|
||||
|
||||
static VOID CALLBACK RcxPollTimerCallback(PVOID, BOOLEAN)
|
||||
{
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
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;
|
||||
|
||||
/* signal readiness */
|
||||
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 1);
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
while (!InterlockedCompareExchange(&g_shutdown, 0, 0)) {
|
||||
DWORD rc = WaitForSingleObject(g_hReqEvent, 250);
|
||||
if (rc == WAIT_TIMEOUT)
|
||||
continue;
|
||||
if (rc != WAIT_OBJECT_0)
|
||||
break;
|
||||
|
||||
hdr->status = RCX_RPC_STATUS_OK;
|
||||
|
||||
switch (static_cast<RcxRpcCommand>(hdr->command)) {
|
||||
case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break;
|
||||
case RPC_CMD_WRITE: handle_write(hdr, data); break;
|
||||
case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break;
|
||||
case RPC_CMD_PING: break;
|
||||
case RPC_CMD_SHUTDOWN:
|
||||
InterlockedExchange(&g_shutdown, 1);
|
||||
break;
|
||||
default:
|
||||
hdr->status = RCX_RPC_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
SetEvent(g_hRspEvent);
|
||||
|
||||
if (static_cast<RcxRpcCommand>(hdr->command) == RPC_CMD_SHUTDOWN)
|
||||
break;
|
||||
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;
|
||||
}
|
||||
|
||||
/* mark not-ready so the host process can detect shutdown */
|
||||
InterlockedExchange(reinterpret_cast<volatile LONG*>(&hdr->payloadReady), 0);
|
||||
return 0;
|
||||
SetEvent(g_hRspEvent);
|
||||
}
|
||||
|
||||
/* ── cleanup ──────────────────────────────────────────────────────── */
|
||||
|
||||
static void Cleanup(bool waitThread)
|
||||
void RcxPayloadCleanup()
|
||||
{
|
||||
InterlockedExchange(&g_shutdown, 1);
|
||||
if (!InterlockedCompareExchange(&g_initialized, 0, 0))
|
||||
return;
|
||||
|
||||
/* wake the thread if it's blocked on REQ */
|
||||
if (g_hReqEvent) SetEvent(g_hReqEvent);
|
||||
|
||||
if (waitThread && g_hThread) {
|
||||
WaitForSingleObject(g_hThread, 2000);
|
||||
/* stop the poll timer first */
|
||||
if (g_hTimerQueue) {
|
||||
DeleteTimerQueueEx(g_hTimerQueue, INVALID_HANDLE_VALUE); /* waits for callbacks */
|
||||
g_hTimerQueue = nullptr;
|
||||
g_hPollTimer = nullptr;
|
||||
}
|
||||
if (g_hThread) { CloseHandle(g_hThread); g_hThread = nullptr; }
|
||||
if (g_mappedView){ UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; }
|
||||
if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; }
|
||||
if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; }
|
||||
if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; }
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* ── DllMain ──────────────────────────────────────────────────────── */
|
||||
/* ── init (called AFTER DllMain returns — safe for timer queues) ── */
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID reserved)
|
||||
extern "C" __declspec(dllexport)
|
||||
bool RcxPayloadInit()
|
||||
{
|
||||
if (reason == DLL_PROCESS_ATTACH) {
|
||||
uint32_t pid = GetCurrentProcessId();
|
||||
if (InterlockedCompareExchange(&g_initialized, 1, 0) != 0)
|
||||
return true; /* already initialized */
|
||||
|
||||
/* ── read nonce from bootstrap shm ── */
|
||||
char bootName[128];
|
||||
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
|
||||
uint32_t pid = GetCurrentProcessId();
|
||||
|
||||
HANDLE hBoot = OpenFileMappingA(FILE_MAP_READ, FALSE, bootName);
|
||||
if (!hBoot) return TRUE; /* no bootstrap = nothing to do */
|
||||
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);
|
||||
|
||||
auto* bootView = static_cast<const RcxRpcBootHeader*>(
|
||||
MapViewOfFile(hBoot, FILE_MAP_READ, 0, 0, RCX_RPC_BOOT_SIZE));
|
||||
if (!bootView) { CloseHandle(hBoot); return TRUE; }
|
||||
|
||||
char nonce[64] = {};
|
||||
uint32_t nLen = bootView->nonceLength;
|
||||
if (nLen > 59) nLen = 59;
|
||||
memcpy(nonce, bootView->nonce, nLen);
|
||||
nonce[nLen] = '\0';
|
||||
|
||||
UnmapViewOfFile(bootView);
|
||||
CloseHandle(hBoot);
|
||||
|
||||
/* ── create main shared memory ── */
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce);
|
||||
|
||||
g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
|
||||
PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName);
|
||||
if (!g_hShm) return TRUE;
|
||||
|
||||
g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE);
|
||||
if (!g_mappedView) { CloseHandle(g_hShm); g_hShm = nullptr; return TRUE; }
|
||||
|
||||
memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE);
|
||||
auto* hdr = static_cast<RcxRpcHeader*>(g_mappedView);
|
||||
hdr->version = RCX_RPC_VERSION;
|
||||
|
||||
/* image base from PEB: gs:[0x60] → PEB, +0x18 → Ldr, Flink → first entry, +0x30 → DllBase */
|
||||
{
|
||||
uint64_t peb;
|
||||
asm volatile("mov %%gs:0x60, %0" : "=r"(peb));
|
||||
uint64_t ldr = *reinterpret_cast<uint64_t*>(peb + 0x18);
|
||||
uint64_t firstLink = *reinterpret_cast<uint64_t*>(ldr + 0x10); /* InLoadOrderModuleList.Flink */
|
||||
hdr->imageBase = *reinterpret_cast<uint64_t*>(firstLink + 0x30); /* DllBase */
|
||||
}
|
||||
|
||||
/* ── create events ── */
|
||||
g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName);
|
||||
g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName);
|
||||
if (!g_hReqEvent || !g_hRspEvent) { Cleanup(false); return TRUE; }
|
||||
|
||||
/* ── start server thread (payloadReady set by the thread) ── */
|
||||
g_hThread = CreateThread(nullptr, 0, ServerThread, nullptr, 0, nullptr);
|
||||
if (!g_hThread) { Cleanup(false); return TRUE; }
|
||||
}
|
||||
else if (reason == DLL_PROCESS_DETACH) {
|
||||
/* reserved != NULL → process is terminating (threads already dead) */
|
||||
Cleanup(reserved == nullptr);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -529,37 +541,14 @@ static void payload_init()
|
||||
{
|
||||
uint32_t pid = (uint32_t)getpid();
|
||||
|
||||
/* ── read nonce from bootstrap shm ── */
|
||||
char bootName[128];
|
||||
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
|
||||
|
||||
int bootFd = shm_open(bootName, O_RDONLY, 0);
|
||||
if (bootFd < 0) return;
|
||||
|
||||
void* bootView = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ,
|
||||
MAP_SHARED, bootFd, 0);
|
||||
close(bootFd);
|
||||
if (bootView == MAP_FAILED) return;
|
||||
|
||||
auto* boot = static_cast<const RcxRpcBootHeader*>(bootView);
|
||||
char nonce[64] = {};
|
||||
uint32_t nLen = boot->nonceLength;
|
||||
if (nLen > 59) nLen = 59;
|
||||
memcpy(nonce, boot->nonce, nLen);
|
||||
nonce[nLen] = '\0';
|
||||
munmap(bootView, RCX_RPC_BOOT_SIZE);
|
||||
|
||||
/* one-shot, unlink bootstrap */
|
||||
shm_unlink(bootName);
|
||||
|
||||
/* ── open /proc/self/mem for safe access ── */
|
||||
g_memFd = open("/proc/self/mem", O_RDWR);
|
||||
if (g_memFd < 0) return;
|
||||
|
||||
/* ── create main shared memory ── */
|
||||
rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid, nonce);
|
||||
rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid, nonce);
|
||||
rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid, nonce);
|
||||
/* ── 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;
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#define RCX_RPC_HEADER_SIZE 4096
|
||||
#define RCX_RPC_DATA_OFFSET RCX_RPC_HEADER_SIZE
|
||||
#define RCX_RPC_DATA_SIZE (RCX_RPC_SHM_SIZE - RCX_RPC_DATA_OFFSET)
|
||||
#define RCX_RPC_BOOT_SIZE 64
|
||||
|
||||
/* status codes */
|
||||
#define RCX_RPC_STATUS_OK 0
|
||||
@@ -83,47 +82,32 @@ struct RcxRpcHeader {
|
||||
uint8_t _pad[RCX_RPC_HEADER_SIZE - 48];
|
||||
};
|
||||
|
||||
/* Bootstrap shm -- 64 bytes, carries the nonce from plugin to payload */
|
||||
struct RcxRpcBootHeader {
|
||||
uint32_t nonceLength;
|
||||
char nonce[60];
|
||||
};
|
||||
/* ── name formatting helpers (PID-only, no nonce) ─────────────────── */
|
||||
|
||||
/* ── name formatting helpers ───────────────────────────────────────── */
|
||||
|
||||
static inline void rcx_rpc_boot_name(char* buf, int n, uint32_t pid) {
|
||||
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_BOOT_%u", pid);
|
||||
snprintf(buf, n, "Local\\RCX_SHM_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_boot_%u", pid);
|
||||
snprintf(buf, n, "/rcx_shm_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid, const char* nonce) {
|
||||
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_SHM_%u_%s", pid, nonce);
|
||||
snprintf(buf, n, "Local\\RCX_REQ_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_shm_%u_%s", pid, nonce);
|
||||
snprintf(buf, n, "/rcx_req_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid, const char* nonce) {
|
||||
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_REQ_%u_%s", pid, nonce);
|
||||
snprintf(buf, n, "Local\\RCX_RSP_%u", pid);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_req_%u_%s", pid, nonce);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid, const char* nonce) {
|
||||
#ifdef _WIN32
|
||||
snprintf(buf, n, "Local\\RCX_RSP_%u_%s", pid, nonce);
|
||||
#else
|
||||
snprintf(buf, n, "/rcx_rsp_%u_%s", pid, nonce);
|
||||
snprintf(buf, n, "/rcx_rsp_%u", pid);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
|
||||
static_assert(sizeof(RcxRpcBootHeader) <= RCX_RPC_BOOT_SIZE, "Boot header must fit 64 bytes");
|
||||
static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes");
|
||||
#endif
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* Usage:
|
||||
* test_rpc_client (auto-spawn host)
|
||||
* test_rpc_client <pid> <nonce> [testbuf_hex testlen]
|
||||
* test_rpc_client <pid> [testbuf_hex testlen]
|
||||
*/
|
||||
|
||||
#include "../rcx_rpc_protocol.h"
|
||||
@@ -45,12 +45,12 @@ struct TestIpcClient {
|
||||
void* view = nullptr;
|
||||
bool ok = false;
|
||||
|
||||
bool connect(uint32_t pid, const char* nonce, int timeoutMs = 5000)
|
||||
bool connect(uint32_t pid, int timeoutMs = 5000)
|
||||
{
|
||||
char shmName[128], reqName[128], rspName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce);
|
||||
rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce);
|
||||
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce);
|
||||
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;
|
||||
@@ -268,7 +268,7 @@ static pid_t g_hostPid = 0;
|
||||
#endif
|
||||
static FILE* g_hostPipe = nullptr;
|
||||
|
||||
static bool spawn_host(uint32_t* outPid, char* outNonce,
|
||||
static bool spawn_host(uint32_t* outPid,
|
||||
uint64_t* outTestBuf, uint32_t* outTestLen)
|
||||
{
|
||||
/* resolve path to test_rpc_host next to ourselves */
|
||||
@@ -302,11 +302,11 @@ static bool spawn_host(uint32_t* outPid, char* outNonce,
|
||||
return false;
|
||||
}
|
||||
|
||||
/* parse: READY pid=X nonce=Y testbuf=0xZ testlen=N */
|
||||
/* parse: READY pid=X testbuf=0xZ testlen=N */
|
||||
unsigned long long tbuf = 0;
|
||||
unsigned tlen = 0;
|
||||
if (sscanf(line, "READY pid=%u nonce=%63s testbuf=0x%llx testlen=%u",
|
||||
outPid, outNonce, &tbuf, &tlen) < 2) {
|
||||
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;
|
||||
}
|
||||
@@ -341,30 +341,28 @@ static void print_fail(const char* name) { printf(" [FAIL] %s\n", name); exit(1
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
uint32_t pid = 0;
|
||||
char nonce[64] = {};
|
||||
uint64_t testBuf = 0;
|
||||
uint32_t testLen = 0;
|
||||
bool autoMode = false;
|
||||
|
||||
if (argc >= 3) {
|
||||
if (argc >= 2) {
|
||||
pid = (uint32_t)atoi(argv[1]);
|
||||
strncpy(nonce, argv[2], 63);
|
||||
if (argc >= 5) {
|
||||
testBuf = (uint64_t)strtoull(argv[3], nullptr, 0);
|
||||
testLen = (uint32_t)atoi(argv[4]);
|
||||
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, nonce, &testBuf, &testLen)) return 1;
|
||||
if (!spawn_host(&pid, &testBuf, &testLen)) return 1;
|
||||
}
|
||||
|
||||
printf("Connecting to PID=%u nonce=%s testbuf=0x%llx testlen=%u\n\n",
|
||||
pid, nonce, (unsigned long long)testBuf, testLen);
|
||||
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, nonce)) {
|
||||
if (!ipc.connect(pid)) {
|
||||
fprintf(stderr, "ERROR: IPC connect failed\n");
|
||||
if (autoMode) cleanup_host();
|
||||
return 1;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* test_rpc_host -- loads rcx_payload in-process, acts as the "target".
|
||||
*
|
||||
* Usage: test_rpc_host [nonce]
|
||||
* Usage: test_rpc_host
|
||||
*
|
||||
* Prints a READY line (machine-parseable), then waits for the payload
|
||||
* to shut down (RPC_CMD_SHUTDOWN from the client).
|
||||
@@ -68,50 +68,11 @@ static int payload_path(char* out, int outLen)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Create bootstrap shared memory with the nonce */
|
||||
static int create_bootstrap(uint32_t pid, const char* nonce)
|
||||
{
|
||||
char bootName[128];
|
||||
rcx_rpc_boot_name(bootName, sizeof(bootName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE h = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
|
||||
PAGE_READWRITE, 0, RCX_RPC_BOOT_SIZE, bootName);
|
||||
if (!h) return -1;
|
||||
void* v = MapViewOfFile(h, FILE_MAP_WRITE, 0, 0, RCX_RPC_BOOT_SIZE);
|
||||
if (!v) { CloseHandle(h); return -1; }
|
||||
|
||||
RcxRpcBootHeader* boot = (RcxRpcBootHeader*)v;
|
||||
memset(boot, 0, RCX_RPC_BOOT_SIZE);
|
||||
boot->nonceLength = (uint32_t)strlen(nonce);
|
||||
strncpy(boot->nonce, nonce, 59);
|
||||
|
||||
UnmapViewOfFile(v);
|
||||
/* keep h open for payload to read */
|
||||
return 0;
|
||||
#else
|
||||
int fd = shm_open(bootName, O_CREAT | O_RDWR, 0600);
|
||||
if (fd < 0) return -1;
|
||||
if (ftruncate(fd, RCX_RPC_BOOT_SIZE) != 0) { close(fd); return -1; }
|
||||
void* v = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, fd, 0);
|
||||
close(fd);
|
||||
if (v == MAP_FAILED) return -1;
|
||||
|
||||
RcxRpcBootHeader* boot = (RcxRpcBootHeader*)v;
|
||||
memset(boot, 0, RCX_RPC_BOOT_SIZE);
|
||||
boot->nonceLength = (uint32_t)strlen(nonce);
|
||||
strncpy(boot->nonce, nonce, 59);
|
||||
munmap(v, RCX_RPC_BOOT_SIZE);
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Open the main shared memory (read-only, just to monitor payloadReady) */
|
||||
static void* open_main_shm(uint32_t pid, const char* nonce)
|
||||
static void* open_main_shm(uint32_t pid)
|
||||
{
|
||||
char shmName[128];
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce);
|
||||
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE h = nullptr;
|
||||
@@ -142,21 +103,14 @@ static uint8_t g_testBuf[65536];
|
||||
|
||||
/* ── main ─────────────────────────────────────────────────────────── */
|
||||
|
||||
int main(int argc, char** argv)
|
||||
int main(int, char**)
|
||||
{
|
||||
const char* nonce = (argc > 1) ? argv[1] : "test0001";
|
||||
uint32_t pid = current_pid();
|
||||
|
||||
/* fill test buffer with known pattern */
|
||||
for (int i = 0; i < (int)sizeof(g_testBuf); ++i)
|
||||
g_testBuf[i] = (uint8_t)(i & 0xFF);
|
||||
|
||||
/* create bootstrap shm */
|
||||
if (create_bootstrap(pid, nonce) != 0) {
|
||||
fprintf(stderr, "ERROR: failed to create bootstrap shm\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* load payload */
|
||||
char plPath[1024];
|
||||
if (payload_path(plPath, sizeof(plPath)) != 0) {
|
||||
@@ -171,6 +125,15 @@ int main(int argc, char** argv)
|
||||
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) {
|
||||
@@ -180,7 +143,7 @@ int main(int argc, char** argv)
|
||||
#endif
|
||||
|
||||
/* open main shm and wait for payloadReady */
|
||||
void* shmView = open_main_shm(pid, nonce);
|
||||
void* shmView = open_main_shm(pid);
|
||||
if (!shmView) {
|
||||
fprintf(stderr, "ERROR: failed to open main shared memory\n");
|
||||
return 1;
|
||||
@@ -197,8 +160,8 @@ int main(int argc, char** argv)
|
||||
}
|
||||
|
||||
/* print READY line for the client to parse */
|
||||
printf("READY pid=%u nonce=%s testbuf=0x%llx testlen=%u\n",
|
||||
pid, nonce,
|
||||
printf("READY pid=%u testbuf=0x%llx testlen=%u\n",
|
||||
pid,
|
||||
(unsigned long long)(uintptr_t)g_testBuf,
|
||||
(unsigned)sizeof(g_testBuf));
|
||||
fflush(stdout);
|
||||
@@ -210,7 +173,7 @@ int main(int argc, char** argv)
|
||||
printf("Payload shut down, exiting.\n");
|
||||
|
||||
#ifdef _WIN32
|
||||
/* give the server thread a moment to exit */
|
||||
/* give the timer queue a moment to drain */
|
||||
Sleep(200);
|
||||
FreeLibrary(hPayload);
|
||||
if (shmView) UnmapViewOfFile(shmView);
|
||||
|
||||
@@ -197,53 +197,15 @@ void WinDbgMemoryProvider::querySessionInfo()
|
||||
}
|
||||
}
|
||||
|
||||
if (m_symbols) {
|
||||
ULONG numModules = 0, numUnloaded = 0;
|
||||
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
|
||||
qDebug() << "[WinDbg] GetNumberModules hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "loaded=" << numModules << "unloaded=" << numUnloaded;
|
||||
if (SUCCEEDED(hr) && numModules > 0) {
|
||||
char modName[256] = {};
|
||||
ULONG modSize = 0;
|
||||
hr = m_symbols->GetModuleNames(0, 0, nullptr, 0, nullptr,
|
||||
modName, sizeof(modName), &modSize,
|
||||
nullptr, 0, nullptr);
|
||||
if (SUCCEEDED(hr) && modSize > 0)
|
||||
m_name = QString::fromUtf8(modName);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_name.isEmpty())
|
||||
m_name = m_isLive ? QStringLiteral("DbgEng (Live)") : QStringLiteral("DbgEng (Dump)");
|
||||
|
||||
if (m_symbols) {
|
||||
ULONG numModules = 0, numUnloaded = 0;
|
||||
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
|
||||
if (SUCCEEDED(hr) && numModules > 0) {
|
||||
ULONG64 moduleBase = 0;
|
||||
hr = m_symbols->GetModuleByIndex(0, &moduleBase);
|
||||
qDebug() << "[WinDbg] Module 0 base=" << Qt::hex << moduleBase;
|
||||
if (SUCCEEDED(hr))
|
||||
m_base = moduleBase;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_base && m_dataSpaces) {
|
||||
uint8_t probe[2] = {};
|
||||
ULONG got = 0;
|
||||
hr = m_dataSpaces->ReadVirtual(m_base, probe, 2, &got);
|
||||
qDebug() << "[WinDbg] Probe read at" << Qt::hex << m_base
|
||||
<< "hr=" << (unsigned long)hr << "got=" << got
|
||||
<< "bytes:" << (int)probe[0] << (int)probe[1];
|
||||
if (FAILED(hr) || got == 0) {
|
||||
qWarning() << "[WinDbg] Probe read FAILED — cleaning up";
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// WinDbg provides access to the entire virtual address space.
|
||||
// Do NOT auto-select a module as base — let the user set their
|
||||
// own base address. m_base stays 0 so the controller won't
|
||||
// override tree.baseAddress.
|
||||
m_name = m_isLive ? QStringLiteral("WinDbg (Live)")
|
||||
: QStringLiteral("WinDbg (Dump)");
|
||||
|
||||
qDebug() << "[WinDbg] Ready. name=" << m_name
|
||||
<< "base=" << Qt::hex << m_base << "isLive=" << m_isLive;
|
||||
<< "isLive=" << m_isLive;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -305,8 +267,18 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
dispatchToOwner([&]() {
|
||||
ULONG bytesRead = 0;
|
||||
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
|
||||
if (FAILED(hr) || (int)bytesRead < len)
|
||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||
if (SUCCEEDED(hr) && (int)bytesRead >= len) {
|
||||
result = true;
|
||||
return;
|
||||
}
|
||||
// Partial or failed read — zero-fill remainder and log
|
||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||
++m_readFailCount;
|
||||
if (m_readFailCount <= 5 || (m_readFailCount % 100) == 0)
|
||||
qDebug() << "[WinDbg] ReadVirtual FAILED addr=0x" << Qt::hex << addr
|
||||
<< "len=" << Qt::dec << len
|
||||
<< "hr=0x" << Qt::hex << (unsigned long)hr
|
||||
<< "got=" << Qt::dec << bytesRead;
|
||||
result = bytesRead > 0;
|
||||
});
|
||||
return result;
|
||||
|
||||
@@ -83,6 +83,7 @@ private:
|
||||
bool m_isLive = false;
|
||||
bool m_writable = false;
|
||||
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
|
||||
mutable int m_readFailCount = 0;
|
||||
|
||||
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe
|
||||
// transport is thread-affine — all calls must happen on the thread
|
||||
|
||||
133
src/compose.cpp
133
src/compose.cpp
@@ -22,6 +22,7 @@ struct ComposeState {
|
||||
int nameW = kColName; // global name column width (fallback)
|
||||
int offsetHexDigits = 8; // hex digit tier for offset margin
|
||||
bool baseEmitted = false; // only first root struct shows base address
|
||||
bool compactColumns = false; // compact column mode: cap type width, overflow long types
|
||||
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
||||
|
||||
// Precomputed for O(1) lookups
|
||||
@@ -104,6 +105,13 @@ static inline uint64_t resolveAddr(const ComposeState& state,
|
||||
return state.absOffsets[nodeIdx];
|
||||
}
|
||||
|
||||
|
||||
static const QVector<int>& childIndices(const ComposeState& state, uint64_t parentId) {
|
||||
static const QVector<int> kEmpty;
|
||||
auto it = state.childMap.constFind(parentId);
|
||||
return it == state.childMap.constEnd() ? kEmpty : it.value();
|
||||
}
|
||||
|
||||
void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
const Provider& prov, int nodeIdx,
|
||||
int depth, uint64_t absAddr, uint64_t scopeId) {
|
||||
@@ -132,6 +140,11 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
}
|
||||
}
|
||||
|
||||
// Detect type overflow in compact mode (for effectiveTypeW)
|
||||
QString rawType = ptrTypeOverride.isEmpty() ? fmt::typeNameRaw(node.kind) : ptrTypeOverride;
|
||||
bool typeOverflow = state.compactColumns && rawType.size() > typeW;
|
||||
int lineTypeW = typeOverflow ? rawType.size() : typeW;
|
||||
|
||||
for (int sub = 0; sub < numLines; sub++) {
|
||||
bool isCont = (sub > 0);
|
||||
|
||||
@@ -148,7 +161,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.effectiveTypeW = typeW;
|
||||
lm.effectiveTypeW = lineTypeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
lm.pointerTargetName = ptrTargetName;
|
||||
|
||||
@@ -158,7 +171,8 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
}
|
||||
|
||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride);
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
|
||||
state.compactColumns);
|
||||
state.emitLine(lineText, lm);
|
||||
}
|
||||
}
|
||||
@@ -248,8 +262,6 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.foldCollapsed = node.collapsed;
|
||||
lm.foldLevel = computeFoldLevel(depth, true);
|
||||
lm.markerMask = (1u << M_STRUCT_BG);
|
||||
lm.effectiveTypeW = typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
|
||||
QString headerText;
|
||||
if (node.kind == NodeKind::Array) {
|
||||
@@ -260,19 +272,69 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.arrayCount = node.arrayLen;
|
||||
QString elemStructName = (node.elementKind == NodeKind::Struct)
|
||||
? resolvePointerTarget(tree, node.refId) : QString();
|
||||
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName);
|
||||
QString rawType = fmt::arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
|
||||
bool overflow = state.compactColumns && rawType.size() > typeW;
|
||||
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW, elemStructName, state.compactColumns);
|
||||
} else {
|
||||
// All structs (root and nested) use the same header format
|
||||
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW);
|
||||
QString rawType = fmt::structTypeName(node);
|
||||
bool overflow = state.compactColumns && rawType.size() > typeW;
|
||||
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW, state.compactColumns);
|
||||
}
|
||||
state.emitLine(headerText, lm);
|
||||
}
|
||||
|
||||
if (!node.collapsed || isArrayChild || isRootHeader) {
|
||||
QVector<int> children = state.childMap.value(node.id);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
// Enum with members: render name = value lines instead of offset-based fields
|
||||
if (node.resolvedClassKeyword() == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
|
||||
int childDepth = depth + 1;
|
||||
int maxNameLen = 4;
|
||||
for (const auto& m : node.enumMembers)
|
||||
maxNameLen = qMax(maxNameLen, (int)m.first.size());
|
||||
|
||||
for (int mi = 0; mi < node.enumMembers.size(); mi++) {
|
||||
const auto& m = node.enumMembers[mi];
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.subLine = mi;
|
||||
lm.depth = childDepth;
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.nodeKind = NodeKind::UInt32;
|
||||
lm.foldLevel = computeFoldLevel(childDepth, false);
|
||||
lm.markerMask = 0;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtEnumMember(m.first, m.second, childDepth, maxNameLen), lm);
|
||||
}
|
||||
|
||||
// Footer
|
||||
if (!isArrayChild) {
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx;
|
||||
lm.nodeId = node.id;
|
||||
lm.depth = depth;
|
||||
lm.lineKind = LineKind::Footer;
|
||||
lm.nodeKind = node.kind;
|
||||
lm.isRootHeader = isRootHeader;
|
||||
lm.foldLevel = computeFoldLevel(depth, false);
|
||||
lm.markerMask = 0;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(absAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = absAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
state.emitLine(fmt::fmtStructFooter(node, depth, 0), lm);
|
||||
}
|
||||
|
||||
state.visiting.remove(node.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const QVector<int>& children = childIndices(state, node.id);
|
||||
|
||||
int childDepth = depth + 1;
|
||||
|
||||
@@ -303,16 +365,19 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.lineKind = LineKind::Field;
|
||||
lm.nodeKind = node.elementKind;
|
||||
lm.isArrayElement = true;
|
||||
lm.arrayElementIdx = i;
|
||||
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
|
||||
lm.offsetAddr = elemAddr;
|
||||
lm.ptrBase = state.currentPtrBase;
|
||||
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
|
||||
lm.foldLevel = computeFoldLevel(childDepth, false);
|
||||
lm.effectiveTypeW = eTW;
|
||||
bool elemOverflow = state.compactColumns && elemTypeStr.size() > eTW;
|
||||
lm.effectiveTypeW = elemOverflow ? elemTypeStr.size() : eTW;
|
||||
lm.effectiveNameW = eNW;
|
||||
|
||||
state.emitLine(fmt::fmtNodeLine(elem, prov, elemAddr, childDepth, 0,
|
||||
{}, eTW, eNW, elemTypeStr), lm);
|
||||
{}, eTW, eNW, elemTypeStr,
|
||||
state.compactColumns), lm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,10 +403,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
if (node.kind == NodeKind::Struct && children.isEmpty() && node.refId != 0) {
|
||||
int refIdx = tree.indexOfId(node.refId);
|
||||
if (refIdx >= 0) {
|
||||
QVector<int> refChildren = state.childMap.value(node.refId);
|
||||
std::sort(refChildren.begin(), refChildren.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
const QVector<int>& refChildren = childIndices(state, node.refId);
|
||||
// Use the referenced struct's scope widths (children come from there)
|
||||
uint64_t refScopeId = node.refId;
|
||||
for (int childIdx : refChildren) {
|
||||
@@ -350,6 +412,8 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
if (state.visiting.contains(child.id)) {
|
||||
int typeW = state.effectiveTypeW(refScopeId);
|
||||
int nameW = state.effectiveNameW(refScopeId);
|
||||
QString rawType = fmt::structTypeName(child);
|
||||
bool overflow = state.compactColumns && rawType.size() > typeW;
|
||||
LineMeta lm;
|
||||
lm.nodeIdx = nodeIdx; // parent struct — materialize target
|
||||
lm.nodeId = child.id;
|
||||
@@ -365,10 +429,10 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
lm.foldCollapsed = true;
|
||||
lm.foldLevel = computeFoldLevel(childDepth, true);
|
||||
lm.markerMask = (1u << M_STRUCT_BG) | (1u << M_CYCLE);
|
||||
lm.effectiveTypeW = typeW;
|
||||
lm.effectiveTypeW = overflow ? rawType.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
state.emitLine(fmt::fmtStructHeader(child, childDepth,
|
||||
/*collapsed=*/true, typeW, nameW), lm);
|
||||
/*collapsed=*/true, typeW, nameW, state.compactColumns), lm);
|
||||
continue;
|
||||
}
|
||||
composeNode(state, tree, prov, childIdx, childDepth,
|
||||
@@ -430,7 +494,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
|
||||
|
||||
// Check if this pointer has materialized children (from materializeRefChildren)
|
||||
QVector<int> ptrChildren = state.childMap.value(node.id);
|
||||
const QVector<int>& ptrChildren = childIndices(state, node.id);
|
||||
bool hasMaterialized = !ptrChildren.isEmpty();
|
||||
|
||||
// Force collapsed if this refId is already being virtually expanded
|
||||
@@ -457,12 +521,13 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
lm.foldLevel = computeFoldLevel(depth, true);
|
||||
lm.markerMask = computeMarkers(node, prov, absAddr, false, depth);
|
||||
if (forceCollapsed) lm.markerMask |= (1u << M_CYCLE);
|
||||
lm.effectiveTypeW = typeW;
|
||||
bool ptrOverflow = state.compactColumns && ptrTypeOverride.size() > typeW;
|
||||
lm.effectiveTypeW = ptrOverflow ? ptrTypeOverride.size() : typeW;
|
||||
lm.effectiveNameW = nameW;
|
||||
lm.pointerTargetName = ptrTargetName;
|
||||
state.emitLine(fmt::fmtPointerHeader(node, depth, effectiveCollapsed,
|
||||
prov, absAddr, ptrTypeOverride,
|
||||
typeW, nameW), lm);
|
||||
typeW, nameW, state.compactColumns), lm);
|
||||
}
|
||||
|
||||
if (!effectiveCollapsed) {
|
||||
@@ -495,9 +560,6 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
// Render materialized children at the pointer target address.
|
||||
// These are real tree nodes with independent state — use rootId
|
||||
// so resolveAddr computes offsets relative to the pointer target.
|
||||
std::sort(ptrChildren.begin(), ptrChildren.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
for (int childIdx : ptrChildren) {
|
||||
composeNode(state, tree, childProv, childIdx, depth + 1,
|
||||
pBase, node.id, false, node.id);
|
||||
@@ -557,13 +619,22 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId) {
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
||||
bool compactColumns) {
|
||||
ComposeState state;
|
||||
state.compactColumns = compactColumns;
|
||||
|
||||
// Precompute parent→children map
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
state.childMap[tree.nodes[i].parentId].append(i);
|
||||
|
||||
for (auto it = state.childMap.begin(); it != state.childMap.end(); ++it) {
|
||||
QVector<int>& children = it.value();
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
}
|
||||
|
||||
// Precompute absolute offsets (baseAddress + structure-relative offset)
|
||||
state.absOffsets.resize(tree.nodes.size());
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
@@ -598,11 +669,12 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
|
||||
// Compute effective type column width from longest type name
|
||||
// Include struct/array headers which use "struct TypeName" or "type[count]" format
|
||||
const int typeCap = state.compactColumns ? kCompactTypeW : kMaxTypeW;
|
||||
int maxTypeLen = kMinTypeW;
|
||||
for (const Node& node : tree.nodes) {
|
||||
maxTypeLen = qMax(maxTypeLen, (int)nodeTypeName(node).size());
|
||||
}
|
||||
state.typeW = qBound(kMinTypeW, maxTypeLen, kMaxTypeW);
|
||||
state.typeW = qBound(kMinTypeW, maxTypeLen, typeCap);
|
||||
|
||||
// Compute effective name column width from longest name
|
||||
// Include struct/array names - they now use columnar layout too
|
||||
@@ -646,7 +718,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
scopeMaxType = qMax(scopeMaxType, (int)longestElemType.size());
|
||||
}
|
||||
|
||||
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, kMaxTypeW);
|
||||
state.scopeTypeW[container.id] = qBound(kMinTypeW, scopeMaxType, typeCap);
|
||||
state.scopeNameW[container.id] = qBound(kMinNameW, scopeMaxName, kMaxNameW);
|
||||
}
|
||||
|
||||
@@ -664,7 +736,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
||||
}
|
||||
}
|
||||
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, kMaxTypeW);
|
||||
state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, typeCap);
|
||||
state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW);
|
||||
}
|
||||
|
||||
@@ -687,10 +759,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
|
||||
state.emitLine(cmdRowText, lm);
|
||||
}
|
||||
|
||||
QVector<int> roots = state.childMap.value(0);
|
||||
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
const QVector<int>& roots = childIndices(state, 0);
|
||||
|
||||
for (int idx : roots) {
|
||||
// If viewRootId is set, skip roots that don't match
|
||||
|
||||
@@ -72,8 +72,8 @@ RcxDocument::RcxDocument(QObject* parent)
|
||||
});
|
||||
}
|
||||
|
||||
ComposeResult RcxDocument::compose(uint64_t viewRootId) const {
|
||||
return rcx::compose(tree, *provider, viewRootId);
|
||||
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns) const {
|
||||
return rcx::compose(tree, *provider, viewRootId, compactColumns);
|
||||
}
|
||||
|
||||
bool RcxDocument::save(const QString& path) {
|
||||
@@ -493,9 +493,9 @@ void RcxController::refresh() {
|
||||
|
||||
// Compose against snapshot provider if active, otherwise real provider
|
||||
if (m_snapshotProv)
|
||||
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId);
|
||||
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns);
|
||||
else
|
||||
m_lastResult = m_doc->compose(m_viewRootId);
|
||||
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns);
|
||||
|
||||
s_composeDoc = nullptr;
|
||||
|
||||
@@ -569,9 +569,9 @@ void RcxController::refresh() {
|
||||
// Prune stale selections (nodes removed by undo/redo/delete)
|
||||
QSet<uint64_t> valid;
|
||||
for (uint64_t id : m_selIds) {
|
||||
uint64_t nodeId = id & ~kFooterIdBit; // Strip footer bit for lookup
|
||||
uint64_t nodeId = id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
|
||||
if (m_doc->tree.indexOfId(nodeId) >= 0)
|
||||
valid.insert(id); // Keep original ID (with footer bit if present)
|
||||
valid.insert(id); // Keep original ID (with footer/array bits if present)
|
||||
}
|
||||
m_selIds = valid;
|
||||
|
||||
@@ -805,6 +805,148 @@ void RcxController::deleteRootStruct(uint64_t structId) {
|
||||
if (!m_suppressRefresh) refresh();
|
||||
}
|
||||
|
||||
void RcxController::groupIntoUnion(const QSet<uint64_t>& nodeIds) {
|
||||
if (nodeIds.size() < 2) return;
|
||||
|
||||
// Collect nodes and verify they share the same parent
|
||||
QVector<int> indices;
|
||||
uint64_t parentId = 0;
|
||||
bool first = true;
|
||||
for (uint64_t id : nodeIds) {
|
||||
int idx = m_doc->tree.indexOfId(id);
|
||||
if (idx < 0) return;
|
||||
if (first) { parentId = m_doc->tree.nodes[idx].parentId; first = false; }
|
||||
else if (m_doc->tree.nodes[idx].parentId != parentId) return;
|
||||
indices.append(idx);
|
||||
}
|
||||
|
||||
// Sort by offset to find the union's insertion point
|
||||
std::sort(indices.begin(), indices.end(), [&](int a, int b) {
|
||||
return m_doc->tree.nodes[a].offset < m_doc->tree.nodes[b].offset;
|
||||
});
|
||||
int unionOffset = m_doc->tree.nodes[indices.first()].offset;
|
||||
|
||||
bool wasSuppressed = m_suppressRefresh;
|
||||
m_suppressRefresh = true;
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Group into union"));
|
||||
|
||||
// Save copies of nodes before removal (subtrees included)
|
||||
struct SavedNode { Node node; QVector<Node> subtree; };
|
||||
QVector<SavedNode> saved;
|
||||
for (int idx : indices) {
|
||||
SavedNode sn;
|
||||
sn.node = m_doc->tree.nodes[idx];
|
||||
auto sub = m_doc->tree.subtreeIndices(sn.node.id);
|
||||
for (int si : sub)
|
||||
if (si != idx) sn.subtree.append(m_doc->tree.nodes[si]);
|
||||
saved.append(sn);
|
||||
}
|
||||
|
||||
// Remove selected nodes (in reverse order to keep indices valid)
|
||||
for (int i = indices.size() - 1; i >= 0; i--) {
|
||||
int idx = m_doc->tree.indexOfId(saved[i].node.id);
|
||||
if (idx >= 0) {
|
||||
QVector<Node> subtree;
|
||||
for (int si : m_doc->tree.subtreeIndices(saved[i].node.id))
|
||||
subtree.append(m_doc->tree.nodes[si]);
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::Remove{saved[i].node.id, subtree, {}}));
|
||||
}
|
||||
}
|
||||
|
||||
// Insert union node
|
||||
Node unionNode;
|
||||
unionNode.kind = NodeKind::Struct;
|
||||
unionNode.classKeyword = QStringLiteral("union");
|
||||
unionNode.parentId = parentId;
|
||||
unionNode.offset = unionOffset;
|
||||
unionNode.id = m_doc->tree.reserveId();
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{unionNode}));
|
||||
uint64_t unionId = unionNode.id;
|
||||
|
||||
// Re-insert nodes as children of the union, all at offset 0
|
||||
for (const auto& sn : saved) {
|
||||
Node copy = sn.node;
|
||||
copy.parentId = unionId;
|
||||
copy.offset = 0;
|
||||
copy.id = m_doc->tree.reserveId();
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{copy}));
|
||||
|
||||
// Re-insert subtree with updated parentId for direct children
|
||||
uint64_t oldId = sn.node.id;
|
||||
uint64_t newId = copy.id;
|
||||
for (const auto& child : sn.subtree) {
|
||||
Node cc = child;
|
||||
if (cc.parentId == oldId) cc.parentId = newId;
|
||||
cc.id = m_doc->tree.reserveId();
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{cc}));
|
||||
}
|
||||
}
|
||||
|
||||
m_doc->undoStack.endMacro();
|
||||
m_suppressRefresh = wasSuppressed;
|
||||
if (!m_suppressRefresh) refresh();
|
||||
}
|
||||
|
||||
void RcxController::dissolveUnion(uint64_t unionId) {
|
||||
int ui = m_doc->tree.indexOfId(unionId);
|
||||
if (ui < 0) return;
|
||||
const Node& unionNode = m_doc->tree.nodes[ui];
|
||||
if (unionNode.kind != NodeKind::Struct
|
||||
|| unionNode.resolvedClassKeyword() != QStringLiteral("union")) return;
|
||||
|
||||
uint64_t parentId = unionNode.parentId;
|
||||
int unionOffset = unionNode.offset;
|
||||
|
||||
// Collect union children
|
||||
auto children = m_doc->tree.childrenOf(unionId);
|
||||
struct SavedNode { Node node; QVector<Node> subtree; };
|
||||
QVector<SavedNode> saved;
|
||||
for (int ci : children) {
|
||||
SavedNode sn;
|
||||
sn.node = m_doc->tree.nodes[ci];
|
||||
auto sub = m_doc->tree.subtreeIndices(sn.node.id);
|
||||
for (int si : sub)
|
||||
if (si != ci) sn.subtree.append(m_doc->tree.nodes[si]);
|
||||
saved.append(sn);
|
||||
}
|
||||
|
||||
bool wasSuppressed = m_suppressRefresh;
|
||||
m_suppressRefresh = true;
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Dissolve union"));
|
||||
|
||||
// Remove the union (and all its children)
|
||||
{
|
||||
QVector<Node> subtree;
|
||||
for (int si : m_doc->tree.subtreeIndices(unionId))
|
||||
subtree.append(m_doc->tree.nodes[si]);
|
||||
m_doc->undoStack.push(new RcxCommand(this,
|
||||
cmd::Remove{unionId, subtree, {}}));
|
||||
}
|
||||
|
||||
// Re-insert children under the union's parent, at the union's offset
|
||||
for (const auto& sn : saved) {
|
||||
Node copy = sn.node;
|
||||
copy.parentId = parentId;
|
||||
copy.offset = unionOffset + sn.node.offset;
|
||||
copy.id = m_doc->tree.reserveId();
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{copy}));
|
||||
|
||||
uint64_t oldId = sn.node.id;
|
||||
uint64_t newId = copy.id;
|
||||
for (const auto& child : sn.subtree) {
|
||||
Node cc = child;
|
||||
if (cc.parentId == oldId) cc.parentId = newId;
|
||||
cc.id = m_doc->tree.reserveId();
|
||||
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{cc}));
|
||||
}
|
||||
}
|
||||
|
||||
m_doc->undoStack.endMacro();
|
||||
m_suppressRefresh = wasSuppressed;
|
||||
if (!m_suppressRefresh) refresh();
|
||||
}
|
||||
|
||||
void RcxController::toggleCollapse(int nodeIdx) {
|
||||
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
|
||||
auto& node = m_doc->tree.nodes[nodeIdx];
|
||||
@@ -1343,6 +1485,21 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
}
|
||||
menu.addSeparator();
|
||||
|
||||
// Check if all selected nodes share the same parent (required for grouping)
|
||||
{
|
||||
bool sameParent = true;
|
||||
uint64_t firstParent = 0;
|
||||
bool fp = true;
|
||||
for (uint64_t id : ids) {
|
||||
int idx = m_doc->tree.indexOfId(id);
|
||||
if (idx < 0) { sameParent = false; break; }
|
||||
if (fp) { firstParent = m_doc->tree.nodes[idx].parentId; fp = false; }
|
||||
else if (m_doc->tree.nodes[idx].parentId != firstParent) { sameParent = false; break; }
|
||||
}
|
||||
if (sameParent)
|
||||
menu.addAction("Group into Union", [this, ids]() { groupIntoUnion(ids); });
|
||||
}
|
||||
|
||||
menu.addAction(icon("files.svg"), QString("Duplicate %1 nodes").arg(count), [this, ids]() {
|
||||
for (uint64_t id : ids) {
|
||||
int idx = m_doc->tree.indexOfId(id);
|
||||
@@ -1551,6 +1708,26 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
|
||||
}
|
||||
|
||||
// Dissolve Union: available on union itself or any of its children
|
||||
{
|
||||
uint64_t targetUnionId = 0;
|
||||
if (node.kind == NodeKind::Struct
|
||||
&& node.resolvedClassKeyword() == QStringLiteral("union")) {
|
||||
targetUnionId = nodeId;
|
||||
} else if (node.parentId != 0) {
|
||||
int pi = m_doc->tree.indexOfId(node.parentId);
|
||||
if (pi >= 0 && m_doc->tree.nodes[pi].kind == NodeKind::Struct
|
||||
&& m_doc->tree.nodes[pi].resolvedClassKeyword() == QStringLiteral("union")) {
|
||||
targetUnionId = node.parentId;
|
||||
}
|
||||
}
|
||||
if (targetUnionId != 0) {
|
||||
menu.addAction("Dissolve Union", [this, targetUnionId]() {
|
||||
dissolveUnion(targetUnionId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) duplicateNode(ni);
|
||||
@@ -1583,26 +1760,49 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
|
||||
// ── Always-available actions ──
|
||||
|
||||
menu.addAction(icon("diff-added.svg"), "Append 128 bytes", [this]() {
|
||||
menu.addAction(icon("diff-added.svg"), "Append bytes...", [this, &menu]() {
|
||||
bool ok;
|
||||
QString input = QInputDialog::getText(menu.parentWidget(),
|
||||
QStringLiteral("Append bytes"),
|
||||
QStringLiteral("Byte count (decimal or 0x hex):"),
|
||||
QLineEdit::Normal, QStringLiteral("128"), &ok);
|
||||
if (!ok || input.trimmed().isEmpty()) return;
|
||||
|
||||
QString trimmed = input.trimmed();
|
||||
int byteCount = 0;
|
||||
if (trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
||||
byteCount = trimmed.mid(2).toInt(&ok, 16);
|
||||
else
|
||||
byteCount = trimmed.toInt(&ok, 10);
|
||||
if (!ok || byteCount <= 0) return;
|
||||
|
||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||
int hex64Count = byteCount / 8;
|
||||
int remainBytes = byteCount % 8;
|
||||
|
||||
m_suppressRefresh = true;
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Append 128 bytes"));
|
||||
for (int i = 0; i < 16; i++)
|
||||
m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
|
||||
int idx = 0;
|
||||
for (int i = 0; i < hex64Count; i++, idx++)
|
||||
insertNode(target, -1, NodeKind::Hex64,
|
||||
QStringLiteral("field_%1").arg(i));
|
||||
QStringLiteral("field_%1").arg(idx));
|
||||
for (int i = 0; i < remainBytes; i++, idx++)
|
||||
insertNode(target, -1, NodeKind::Hex8,
|
||||
QStringLiteral("field_%1").arg(idx));
|
||||
m_doc->undoStack.endMacro();
|
||||
m_suppressRefresh = false;
|
||||
refresh();
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
{
|
||||
// Only add Track Value Changes here if not already added in node-specific section
|
||||
if (!hasNode) {
|
||||
auto* act = menu.addAction("Track Value Changes");
|
||||
act->setCheckable(true);
|
||||
act->setChecked(m_trackValues);
|
||||
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
||||
menu.addSeparator();
|
||||
}
|
||||
menu.addSeparator();
|
||||
|
||||
menu.addAction(icon("arrow-left.svg"), "Undo", [this]() {
|
||||
m_doc->undoStack.undo();
|
||||
@@ -1674,11 +1874,17 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
||||
bool ctrl = mods & Qt::ControlModifier;
|
||||
bool shift = mods & Qt::ShiftModifier;
|
||||
|
||||
// Compute effective selection ID: footers use nodeId | kFooterIdBit
|
||||
// Compute effective selection ID:
|
||||
// footers → nodeId | kFooterIdBit
|
||||
// array elements → nodeId | kArrayElemBit | (elemIdx << 48)
|
||||
// everything else → nodeId
|
||||
auto effectiveId = [this](int ln, uint64_t nid) -> uint64_t {
|
||||
if (ln >= 0 && ln < m_lastResult.meta.size() &&
|
||||
m_lastResult.meta[ln].lineKind == LineKind::Footer)
|
||||
if (ln < 0 || ln >= m_lastResult.meta.size()) return nid;
|
||||
const auto& lm = m_lastResult.meta[ln];
|
||||
if (lm.lineKind == LineKind::Footer)
|
||||
return nid | kFooterIdBit;
|
||||
if (lm.isArrayElement && lm.arrayElementIdx >= 0)
|
||||
return makeArrayElemSelId(nid, lm.arrayElementIdx);
|
||||
return nid;
|
||||
};
|
||||
|
||||
@@ -1727,8 +1933,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
||||
|
||||
if (m_selIds.size() == 1) {
|
||||
uint64_t sid = *m_selIds.begin();
|
||||
// Strip footer bit for node lookup
|
||||
int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit);
|
||||
// Strip footer/array bits for node lookup
|
||||
int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask));
|
||||
if (idx >= 0) emit nodeSelected(idx);
|
||||
}
|
||||
}
|
||||
@@ -2298,11 +2504,11 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t newBase = provider->base();
|
||||
m_doc->undoStack.clear();
|
||||
m_doc->provider = std::move(provider);
|
||||
m_doc->dataPath.clear();
|
||||
m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress;
|
||||
// Don't overwrite baseAddress — caller (e.g. selfTest) already set it.
|
||||
// User-initiated source switches go through selectSource() which does update it.
|
||||
|
||||
// Re-evaluate stored formula against the new provider
|
||||
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
||||
@@ -2467,6 +2673,12 @@ void RcxController::clearSources() {
|
||||
refresh();
|
||||
}
|
||||
|
||||
void RcxController::copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx) {
|
||||
m_savedSources = sources;
|
||||
m_activeSourceIdx = activeIdx;
|
||||
pushSavedSourcesToEditors();
|
||||
}
|
||||
|
||||
void RcxController::pushSavedSourcesToEditors() {
|
||||
QVector<SavedSourceDisplay> display;
|
||||
display.reserve(m_savedSources.size());
|
||||
@@ -2488,6 +2700,11 @@ void RcxController::setRefreshInterval(int ms) {
|
||||
m_refreshTimer->setInterval(qMax(1, ms));
|
||||
}
|
||||
|
||||
void RcxController::setCompactColumns(bool v) {
|
||||
m_compactColumns = v;
|
||||
refresh();
|
||||
}
|
||||
|
||||
void RcxController::setupAutoRefresh() {
|
||||
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||
m_refreshTimer = new QTimer(this);
|
||||
|
||||
@@ -40,7 +40,7 @@ public:
|
||||
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
||||
}
|
||||
|
||||
ComposeResult compose(uint64_t viewRootId = 0) const;
|
||||
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false) const;
|
||||
bool save(const QString& path);
|
||||
bool load(const QString& path);
|
||||
void loadData(const QString& binaryPath);
|
||||
@@ -102,6 +102,8 @@ public:
|
||||
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();
|
||||
@@ -122,6 +124,7 @@ public:
|
||||
RcxDocument* document() const { return m_doc; }
|
||||
void setEditorFont(const QString& fontName);
|
||||
void setRefreshInterval(int ms);
|
||||
void setCompactColumns(bool v);
|
||||
|
||||
// MCP bridge accessors
|
||||
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
||||
@@ -131,6 +134,7 @@ public:
|
||||
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; }
|
||||
@@ -153,6 +157,7 @@ private:
|
||||
QSet<uint64_t> m_selIds;
|
||||
int m_anchorLine = -1;
|
||||
bool m_suppressRefresh = false;
|
||||
bool m_compactColumns = false;
|
||||
uint64_t m_viewRootId = 0;
|
||||
|
||||
// ── Saved sources for quick-switch ──
|
||||
|
||||
44
src/core.h
44
src/core.h
@@ -196,6 +196,7 @@ struct Node {
|
||||
NodeKind elementKind = NodeKind::UInt8; // Array: element type; Pointer with ptrDepth>0: target type
|
||||
int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive**
|
||||
int viewIndex = 0; // Array: current view offset (transient)
|
||||
QVector<QPair<QString, int64_t>> enumMembers; // Enum: name→value pairs
|
||||
|
||||
// Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size.
|
||||
int byteSize() const {
|
||||
@@ -229,6 +230,16 @@ struct Node {
|
||||
o["elementKind"] = kindToString(elementKind);
|
||||
if (ptrDepth > 0)
|
||||
o["ptrDepth"] = ptrDepth;
|
||||
if (!enumMembers.isEmpty()) {
|
||||
QJsonArray arr;
|
||||
for (const auto& m : enumMembers) {
|
||||
QJsonObject em;
|
||||
em["name"] = m.first;
|
||||
em["value"] = QString::number(m.second);
|
||||
arr.append(em);
|
||||
}
|
||||
o["enumMembers"] = arr;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
static Node fromJson(const QJsonObject& o) {
|
||||
@@ -246,6 +257,14 @@ struct Node {
|
||||
n.refId = o["refId"].toString("0").toULongLong();
|
||||
n.elementKind = kindFromString(o["elementKind"].toString("UInt8"));
|
||||
n.ptrDepth = qBound(0, o["ptrDepth"].toInt(0), 2);
|
||||
if (o.contains("enumMembers")) {
|
||||
QJsonArray arr = o["enumMembers"].toArray();
|
||||
for (const auto& v : arr) {
|
||||
QJsonObject em = v.toObject();
|
||||
n.enumMembers.append({em["name"].toString(),
|
||||
em["value"].toString("0").toLongLong()});
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -481,6 +500,17 @@ static constexpr uint64_t kCommandRowId = UINT64_MAX;
|
||||
static constexpr int kCommandRowLine = 0;
|
||||
static constexpr int kFirstDataLine = 1;
|
||||
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
|
||||
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
|
||||
static constexpr uint64_t kArrayElemShift = 48; // bits 48-61 hold element index
|
||||
static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements
|
||||
|
||||
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
|
||||
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
|
||||
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
|
||||
}
|
||||
inline int arrayElemIdxFromSelId(uint64_t selId) {
|
||||
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
|
||||
}
|
||||
|
||||
struct LineMeta {
|
||||
int nodeIdx = -1;
|
||||
@@ -588,6 +618,7 @@ inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uin
|
||||
inline constexpr int kMaxTypeW = 128; // Maximum type column width
|
||||
inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview)
|
||||
inline constexpr int kMaxNameW = 128; // Maximum name column width
|
||||
inline constexpr int kCompactTypeW = 20; // Type column cap for compact column mode
|
||||
|
||||
inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) {
|
||||
if (lm.lineKind != LineKind::Field || lm.isContinuation) return {};
|
||||
@@ -840,17 +871,18 @@ namespace fmt {
|
||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
uint64_t addr, int depth, int subLine = 0,
|
||||
const QString& comment = {}, int colType = kColType, int colName = kColName,
|
||||
const QString& typeOverride = {});
|
||||
const QString& typeOverride = {}, bool compact = false);
|
||||
QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits = 8);
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName);
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName, bool compact = false);
|
||||
QString fmtStructFooter(const Node& node, int depth, int totalSize = -1);
|
||||
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {});
|
||||
QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName, const QString& elemStructName = {}, bool compact = false);
|
||||
QString structTypeName(const Node& node); // Full type string for struct headers
|
||||
QString arrayTypeName(NodeKind elemKind, int count, const QString& structName = {});
|
||||
QString pointerTypeName(NodeKind kind, const QString& targetName);
|
||||
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
||||
const Provider& prov, uint64_t addr,
|
||||
const QString& ptrTypeName, int colType = kColType, int colName = kColName);
|
||||
const QString& ptrTypeName, int colType = kColType, int colName = kColName,
|
||||
bool compact = false);
|
||||
QString validateBaseAddress(const QString& text);
|
||||
QString indent(int depth);
|
||||
QString readValue(const Node& node, const Provider& prov,
|
||||
@@ -860,10 +892,12 @@ namespace fmt {
|
||||
QByteArray parseValue(NodeKind kind, const QString& text, bool* ok);
|
||||
QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok);
|
||||
QString validateValue(NodeKind kind, const QString& text);
|
||||
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW);
|
||||
} // namespace fmt
|
||||
|
||||
// ── Compose function forward declaration ──
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0);
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
||||
bool compactColumns = false);
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
114
src/editor.cpp
114
src/editor.cpp
@@ -787,6 +787,14 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
m_meta = result.meta;
|
||||
m_layout = result.layout;
|
||||
|
||||
// Build nodeId → display-line index for O(1) hover/selection lookup
|
||||
m_nodeLineIndex.clear();
|
||||
m_nodeLineIndex.reserve(m_meta.size());
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
if (m_meta[i].nodeId != 0)
|
||||
m_nodeLineIndex[m_meta[i].nodeId].append(i);
|
||||
}
|
||||
|
||||
// Dynamically resize margin to fit the current hex digit tier
|
||||
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
|
||||
m_sci->setMarginWidth(0, marginSizer);
|
||||
@@ -835,9 +843,12 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
m_applyingDocument = false;
|
||||
|
||||
// Re-apply hover markers (setText() clears all Scintilla markers).
|
||||
// Reset m_prevHoveredNodeId so the incremental logic re-adds markers.
|
||||
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
|
||||
// composed text that updateCommandRow() will overwrite. The correct call
|
||||
// happens via applySelectionOverlays() after all text is finalized.
|
||||
m_prevHoveredNodeId = 0;
|
||||
m_prevHoveredLine = -1;
|
||||
applyHoverHighlight();
|
||||
}
|
||||
|
||||
@@ -1064,18 +1075,33 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
|
||||
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
if (isSyntheticLine(m_meta[i])) continue;
|
||||
uint64_t nodeId = m_meta[i].nodeId;
|
||||
bool isFooter = (m_meta[i].lineKind == LineKind::Footer);
|
||||
|
||||
// Footers check for footerId, non-footers check for plain nodeId
|
||||
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId;
|
||||
if (selIds.contains(checkId)) {
|
||||
m_sci->markerAdd(i, M_SELECTED);
|
||||
m_sci->markerAdd(i, M_ACCENT);
|
||||
// Use index: iterate selected IDs, look up their lines
|
||||
for (uint64_t selId : selIds) {
|
||||
bool isFooterSel = (selId & kFooterIdBit) != 0;
|
||||
bool isArrayElemSel = (selId & kArrayElemBit) != 0;
|
||||
int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1;
|
||||
uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
|
||||
auto it = m_nodeLineIndex.constFind(nodeId);
|
||||
if (it == m_nodeLineIndex.constEnd()) continue;
|
||||
for (int ln : *it) {
|
||||
if (isSyntheticLine(m_meta[ln])) continue;
|
||||
bool isFooter = (m_meta[ln].lineKind == LineKind::Footer);
|
||||
// Match selection type to line type
|
||||
if (isFooterSel && !isFooter) continue;
|
||||
if (!isFooterSel && isFooter) continue;
|
||||
// Array element: match by element index
|
||||
if (isArrayElemSel) {
|
||||
if (!m_meta[ln].isArrayElement || m_meta[ln].arrayElementIdx != arrayElemIdx)
|
||||
continue;
|
||||
} else if (m_meta[ln].isArrayElement) {
|
||||
// Plain nodeId selection shouldn't highlight individual array elements
|
||||
// (the header line is enough)
|
||||
continue;
|
||||
}
|
||||
m_sci->markerAdd(ln, M_SELECTED);
|
||||
m_sci->markerAdd(ln, M_ACCENT);
|
||||
if (!isFooter)
|
||||
paintEditableSpans(i);
|
||||
paintEditableSpans(ln);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1088,28 +1114,63 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
||||
}
|
||||
|
||||
void RcxEditor::applyHoverHighlight() {
|
||||
m_sci->markerDeleteAll(M_HOVER);
|
||||
uint64_t prevId = m_prevHoveredNodeId;
|
||||
int prevLine = m_prevHoveredLine;
|
||||
m_prevHoveredNodeId = m_hoveredNodeId;
|
||||
m_prevHoveredLine = m_hoveredLine;
|
||||
|
||||
// Fast path: nothing changed (same node AND same line)
|
||||
if (prevId == m_hoveredNodeId && prevLine == m_hoveredLine
|
||||
&& m_hoveredNodeId != 0) return;
|
||||
|
||||
// Remove old hover markers
|
||||
if (prevId != 0) {
|
||||
// Check if old hovered line was a single-line highlight (footer or array element)
|
||||
bool prevSingleLine = (prevLine >= 0 && prevLine < m_meta.size() &&
|
||||
(m_meta[prevLine].lineKind == LineKind::Footer || m_meta[prevLine].isArrayElement));
|
||||
if (prevSingleLine) {
|
||||
m_sci->markerDelete(prevLine, M_HOVER);
|
||||
} else {
|
||||
auto it = m_nodeLineIndex.constFind(prevId);
|
||||
if (it != m_nodeLineIndex.constEnd()) {
|
||||
for (int ln : *it)
|
||||
m_sci->markerDelete(ln, M_HOVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_editState.active) return;
|
||||
if (!m_hoverInside) return;
|
||||
if (m_hoveredNodeId == 0) return;
|
||||
|
||||
// Check if hovered line is a footer - footers highlight independently
|
||||
// Footer and array elements highlight only the specific line
|
||||
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
|
||||
bool hoveringArrayElem = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||
m_meta[m_hoveredLine].isArrayElement);
|
||||
|
||||
// Check if the hovered item is already selected (using appropriate ID)
|
||||
uint64_t checkId = hoveringFooter ? (m_hoveredNodeId | kFooterIdBit) : m_hoveredNodeId;
|
||||
uint64_t checkId;
|
||||
if (hoveringFooter)
|
||||
checkId = m_hoveredNodeId | kFooterIdBit;
|
||||
else if (hoveringArrayElem)
|
||||
checkId = makeArrayElemSelId(m_hoveredNodeId, m_meta[m_hoveredLine].arrayElementIdx);
|
||||
else
|
||||
checkId = m_hoveredNodeId;
|
||||
if (m_currentSelIds.contains(checkId)) return;
|
||||
|
||||
if (hoveringFooter) {
|
||||
// Footer: only highlight this specific line
|
||||
if (hoveringFooter || hoveringArrayElem) {
|
||||
// Single-line highlight for footers and array elements
|
||||
m_sci->markerAdd(m_hoveredLine, M_HOVER);
|
||||
} else {
|
||||
// Non-footer: highlight all matching lines except footers
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
if (m_meta[i].nodeId == m_hoveredNodeId &&
|
||||
m_meta[i].lineKind != LineKind::Footer)
|
||||
m_sci->markerAdd(i, M_HOVER);
|
||||
// Non-footer, non-array-element: highlight all lines for this node
|
||||
auto it = m_nodeLineIndex.constFind(m_hoveredNodeId);
|
||||
if (it != m_nodeLineIndex.constEnd()) {
|
||||
for (int ln : *it) {
|
||||
if (m_meta[ln].lineKind != LineKind::Footer &&
|
||||
!m_meta[ln].isArrayElement)
|
||||
m_sci->markerAdd(ln, M_HOVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2617,11 +2678,16 @@ void RcxEditor::updateEditableIndicators(int line) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper to check if a line's node is selected (handles footer IDs)
|
||||
// Helper to check if a line's node is selected (handles footer/array element IDs)
|
||||
auto isLineSelected = [this](const LineMeta* lm) -> bool {
|
||||
if (!lm) return false;
|
||||
bool isFooter = (lm->lineKind == LineKind::Footer);
|
||||
uint64_t checkId = isFooter ? (lm->nodeId | kFooterIdBit) : lm->nodeId;
|
||||
uint64_t checkId;
|
||||
if (lm->lineKind == LineKind::Footer)
|
||||
checkId = lm->nodeId | kFooterIdBit;
|
||||
else if (lm->isArrayElement && lm->arrayElementIdx >= 0)
|
||||
checkId = makeArrayElemSelId(lm->nodeId, lm->arrayElementIdx);
|
||||
else
|
||||
checkId = lm->nodeId;
|
||||
return m_currentSelIds.contains(checkId);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QWidget>
|
||||
#include <QSet>
|
||||
#include <QPoint>
|
||||
#include <QHash>
|
||||
|
||||
class QsciScintilla;
|
||||
class QsciLexerCPP;
|
||||
@@ -95,8 +96,12 @@ private:
|
||||
bool m_hoverInside = false;
|
||||
uint64_t m_hoveredNodeId = 0;
|
||||
int m_hoveredLine = -1;
|
||||
uint64_t m_prevHoveredNodeId = 0; // for incremental marker update
|
||||
int m_prevHoveredLine = -1; // for incremental marker update
|
||||
QSet<uint64_t> m_currentSelIds;
|
||||
QVector<int> m_hoverSpanLines; // Lines with hover span indicators
|
||||
// ── nodeId → display-line index (built in applyDocument) ──
|
||||
QHash<uint64_t, QVector<int>> m_nodeLineIndex;
|
||||
// ── Drag selection ──
|
||||
bool m_dragging = false;
|
||||
bool m_dragStarted = false; // true once drag threshold exceeded
|
||||
|
||||
353
src/examples/EPROCESS.rcx
Normal file
353
src/examples/EPROCESS.rcx
Normal file
@@ -0,0 +1,353 @@
|
||||
{
|
||||
"baseAddress": "FFFF800000000000",
|
||||
"nextId": "9000",
|
||||
"nodes": [
|
||||
{"id":"100","kind":"Struct","name":"list_entry","structTypeName":"_LIST_ENTRY","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"101","kind":"Pointer64","name":"Flink","offset":0,"parentId":"100","refId":"100","collapsed":true},
|
||||
{"id":"102","kind":"Pointer64","name":"Blink","offset":8,"parentId":"100","refId":"100","collapsed":true},
|
||||
|
||||
{"id":"110","kind":"Struct","name":"single_list_entry","structTypeName":"_SINGLE_LIST_ENTRY","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"111","kind":"Pointer64","name":"Next","offset":0,"parentId":"110","refId":"110","collapsed":true},
|
||||
|
||||
{"id":"120","kind":"Struct","name":"ex_push_lock","structTypeName":"_EX_PUSH_LOCK","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"121","kind":"Hex64","name":"Value","offset":0,"parentId":"120"},
|
||||
|
||||
{"id":"130","kind":"Struct","name":"ex_rundown_ref","structTypeName":"_EX_RUNDOWN_REF","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"131","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"130","refId":"0","collapsed":false},
|
||||
{"id":"132","kind":"UInt64","name":"Count","offset":0,"parentId":"131"},
|
||||
{"id":"133","kind":"Pointer64","name":"Ptr","offset":0,"parentId":"131"},
|
||||
|
||||
{"id":"140","kind":"Struct","name":"ex_fast_ref","structTypeName":"_EX_FAST_REF","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"141","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"140","refId":"0","collapsed":false},
|
||||
{"id":"142","kind":"Pointer64","name":"Object","offset":0,"parentId":"141"},
|
||||
{"id":"143","kind":"UInt64","name":"Value","offset":0,"parentId":"141"},
|
||||
|
||||
{"id":"150","kind":"Struct","name":"unicode_string","structTypeName":"_UNICODE_STRING","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"151","kind":"UInt16","name":"Length","offset":0,"parentId":"150"},
|
||||
{"id":"152","kind":"UInt16","name":"MaximumLength","offset":2,"parentId":"150"},
|
||||
{"id":"153","kind":"Pointer64","name":"Buffer","offset":8,"parentId":"150"},
|
||||
|
||||
{"id":"160","kind":"Struct","name":"large_integer","structTypeName":"_LARGE_INTEGER","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"161","kind":"Struct","name":"","offset":0,"parentId":"160","refId":"0","collapsed":false},
|
||||
{"id":"162","kind":"UInt32","name":"LowPart","offset":0,"parentId":"161"},
|
||||
{"id":"163","kind":"Int32","name":"HighPart","offset":4,"parentId":"161"},
|
||||
{"id":"164","kind":"Int64","name":"QuadPart","offset":0,"parentId":"160"},
|
||||
|
||||
{"id":"170","kind":"Struct","name":"rtl_avl_tree","structTypeName":"_RTL_AVL_TREE","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"171","kind":"Pointer64","name":"Root","offset":0,"parentId":"170"},
|
||||
|
||||
{"id":"180","kind":"Struct","name":"kstack_count","structTypeName":"_KSTACK_COUNT","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"181","kind":"Int32","name":"Value","offset":0,"parentId":"180"},
|
||||
{"id":"182","kind":"Hex32","name":"State:3 StackCount:29","offset":0,"parentId":"180"},
|
||||
|
||||
{"id":"190","kind":"Struct","name":"kexecute_options","structTypeName":"_KEXECUTE_OPTIONS","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"191","kind":"Hex8","name":"ExecuteOptions","offset":0,"parentId":"190"},
|
||||
|
||||
{"id":"200","kind":"Struct","name":"se_audit_info","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"201","kind":"Pointer64","name":"ImageFileName","offset":0,"parentId":"200"},
|
||||
|
||||
{"id":"210","kind":"Struct","name":"ps_protection","structTypeName":"_PS_PROTECTION","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"211","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"210","refId":"0","collapsed":false},
|
||||
{"id":"212","kind":"UInt8","name":"Level","offset":0,"parentId":"211"},
|
||||
{"id":"213","kind":"Hex8","name":"Type:3 Audit:1 Signer:4","offset":0,"parentId":"211"},
|
||||
|
||||
{"id":"220","kind":"Struct","name":"timer_delay","structTypeName":"_PS_INTERLOCKED_TIMER_DELAY_VALUES","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"221","kind":"Hex64","name":"DelayMs:30 CoalescingWindowMs:30 Reserved:1 NewTimerWheel:1 Retry:1 Locked:1","offset":0,"parentId":"220"},
|
||||
{"id":"222","kind":"UInt64","name":"All","offset":0,"parentId":"220"},
|
||||
|
||||
{"id":"230","kind":"Struct","name":"wnf_state_name","structTypeName":"_WNF_STATE_NAME","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"231","kind":"UInt32","name":"Data_0","offset":0,"parentId":"230"},
|
||||
{"id":"232","kind":"UInt32","name":"Data_1","offset":4,"parentId":"230"},
|
||||
|
||||
{"id":"240","kind":"Struct","name":"dynamic_ranges","structTypeName":"_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"241","kind":"Struct","name":"Tree","structTypeName":"_RTL_AVL_TREE","offset":0,"parentId":"240","refId":"170","collapsed":true},
|
||||
{"id":"242","kind":"Struct","name":"Lock","structTypeName":"_EX_PUSH_LOCK","offset":8,"parentId":"240","refId":"120","collapsed":true},
|
||||
|
||||
{"id":"250","kind":"Struct","name":"alpc_context","structTypeName":"_ALPC_PROCESS_CONTEXT","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"251","kind":"Struct","name":"Lock","structTypeName":"_EX_PUSH_LOCK","offset":0,"parentId":"250","refId":"120","collapsed":true},
|
||||
{"id":"252","kind":"Struct","name":"ViewListHead","structTypeName":"_LIST_ENTRY","offset":8,"parentId":"250","refId":"100","collapsed":true},
|
||||
{"id":"253","kind":"UInt64","name":"PagedPoolQuotaCache","offset":24,"parentId":"250"},
|
||||
|
||||
{"id":"260","kind":"Struct","name":"mmsupport_flags","structTypeName":"_MMSUPPORT_FLAGS","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"261","kind":"Hex32","name":"EntireFlags","offset":0,"parentId":"260"},
|
||||
|
||||
{"id":"270","kind":"Struct","name":"mmsupport_shared","structTypeName":"_MMSUPPORT_SHARED","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"271","kind":"Pointer64","name":"WorkingSetLockArray","offset":0,"parentId":"270"},
|
||||
{"id":"272","kind":"UInt64","name":"ReleasedCommitDebt","offset":8,"parentId":"270"},
|
||||
{"id":"273","kind":"UInt64","name":"ResetPagesRepurposedCount","offset":16,"parentId":"270"},
|
||||
{"id":"274","kind":"Pointer64","name":"WsSwapSupport","offset":24,"parentId":"270"},
|
||||
{"id":"275","kind":"Pointer64","name":"CommitReleaseContext","offset":32,"parentId":"270"},
|
||||
{"id":"276","kind":"Pointer64","name":"AccessLog","offset":40,"parentId":"270"},
|
||||
{"id":"277","kind":"UInt64","name":"ChargedWslePages","offset":48,"parentId":"270"},
|
||||
{"id":"278","kind":"UInt64","name":"ActualWslePages","offset":56,"parentId":"270"},
|
||||
{"id":"279","kind":"Int32","name":"WorkingSetCoreLock","offset":64,"parentId":"270"},
|
||||
{"id":"280","kind":"Pointer64","name":"ShadowMapping","offset":72,"parentId":"270"},
|
||||
|
||||
{"id":"300","kind":"Struct","name":"mmsupport_instance","structTypeName":"_MMSUPPORT_INSTANCE","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"301","kind":"UInt32","name":"NextPageColor","offset":0,"parentId":"300"},
|
||||
{"id":"302","kind":"UInt32","name":"PageFaultCount","offset":4,"parentId":"300"},
|
||||
{"id":"303","kind":"UInt64","name":"TrimmedPageCount","offset":8,"parentId":"300"},
|
||||
{"id":"304","kind":"Pointer64","name":"VmWorkingSetList","offset":16,"parentId":"300"},
|
||||
{"id":"305","kind":"Struct","name":"WorkingSetExpansionLinks","structTypeName":"_LIST_ENTRY","offset":24,"parentId":"300","refId":"100","collapsed":true},
|
||||
{"id":"306","kind":"UInt64","name":"AgeDistribution_0","offset":40,"parentId":"300"},
|
||||
{"id":"307","kind":"UInt64","name":"AgeDistribution_1","offset":48,"parentId":"300"},
|
||||
{"id":"308","kind":"UInt64","name":"AgeDistribution_2","offset":56,"parentId":"300"},
|
||||
{"id":"309","kind":"UInt64","name":"AgeDistribution_3","offset":64,"parentId":"300"},
|
||||
{"id":"310","kind":"UInt64","name":"AgeDistribution_4","offset":72,"parentId":"300"},
|
||||
{"id":"311","kind":"UInt64","name":"AgeDistribution_5","offset":80,"parentId":"300"},
|
||||
{"id":"312","kind":"UInt64","name":"AgeDistribution_6","offset":88,"parentId":"300"},
|
||||
{"id":"313","kind":"UInt64","name":"AgeDistribution_7","offset":96,"parentId":"300"},
|
||||
{"id":"314","kind":"Pointer64","name":"ExitOutswapGate","offset":104,"parentId":"300"},
|
||||
{"id":"315","kind":"UInt64","name":"MinimumWorkingSetSize","offset":112,"parentId":"300"},
|
||||
{"id":"316","kind":"UInt64","name":"MaximumWorkingSetSize","offset":120,"parentId":"300"},
|
||||
{"id":"317","kind":"UInt64","name":"WorkingSetLeafSize","offset":128,"parentId":"300"},
|
||||
{"id":"318","kind":"UInt64","name":"WorkingSetLeafPrivateSize","offset":136,"parentId":"300"},
|
||||
{"id":"319","kind":"UInt64","name":"WorkingSetSize","offset":144,"parentId":"300"},
|
||||
{"id":"320","kind":"UInt64","name":"WorkingSetPrivateSize","offset":152,"parentId":"300"},
|
||||
{"id":"321","kind":"UInt64","name":"PeakWorkingSetSize","offset":160,"parentId":"300"},
|
||||
{"id":"322","kind":"UInt32","name":"HardFaultCount","offset":168,"parentId":"300"},
|
||||
{"id":"323","kind":"UInt16","name":"LastTrimStamp","offset":172,"parentId":"300"},
|
||||
{"id":"324","kind":"UInt16","name":"PartitionId","offset":174,"parentId":"300"},
|
||||
{"id":"325","kind":"UInt64","name":"SelfmapLock","offset":176,"parentId":"300"},
|
||||
{"id":"326","kind":"Struct","name":"Flags","structTypeName":"_MMSUPPORT_FLAGS","offset":184,"parentId":"300","refId":"260","collapsed":true},
|
||||
|
||||
{"id":"350","kind":"Struct","name":"mmsupport_full","structTypeName":"_MMSUPPORT_FULL","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"351","kind":"Struct","name":"Instance","structTypeName":"_MMSUPPORT_INSTANCE","offset":0,"parentId":"350","refId":"300","collapsed":true},
|
||||
{"id":"352","kind":"Struct","name":"Shared","structTypeName":"_MMSUPPORT_SHARED","offset":192,"parentId":"350","refId":"270","collapsed":true},
|
||||
|
||||
{"id":"400","kind":"Struct","name":"dispatcher_header","structTypeName":"_DISPATCHER_HEADER","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"401","kind":"Struct","name":"","classKeyword":"union","offset":0,"parentId":"400","refId":"0","collapsed":false},
|
||||
{"id":"402","kind":"UInt8","name":"Type","offset":0,"parentId":"401"},
|
||||
{"id":"403","kind":"UInt8","name":"Signalling","offset":1,"parentId":"401"},
|
||||
{"id":"404","kind":"UInt8","name":"Size","offset":2,"parentId":"401"},
|
||||
{"id":"405","kind":"UInt8","name":"Reserved1","offset":3,"parentId":"401"},
|
||||
{"id":"406","kind":"Int32","name":"Lock","offset":0,"parentId":"401"},
|
||||
{"id":"407","kind":"Int32","name":"SignalState","offset":4,"parentId":"400"},
|
||||
{"id":"408","kind":"Struct","name":"WaitListHead","structTypeName":"_LIST_ENTRY","offset":8,"parentId":"400","refId":"100","collapsed":true},
|
||||
|
||||
{"id":"500","kind":"Struct","name":"kprocess","structTypeName":"_KPROCESS","offset":0,"parentId":"0","refId":"0","collapsed":true},
|
||||
{"id":"501","kind":"Struct","name":"Header","structTypeName":"_DISPATCHER_HEADER","offset":0,"parentId":"500","refId":"400","collapsed":true},
|
||||
{"id":"502","kind":"Struct","name":"ProfileListHead","structTypeName":"_LIST_ENTRY","offset":24,"parentId":"500","refId":"100","collapsed":true},
|
||||
{"id":"503","kind":"UInt64","name":"DirectoryTableBase","offset":40,"parentId":"500"},
|
||||
{"id":"504","kind":"Struct","name":"ThreadListHead","structTypeName":"_LIST_ENTRY","offset":48,"parentId":"500","refId":"100","collapsed":true},
|
||||
{"id":"505","kind":"UInt32","name":"ProcessLock","offset":64,"parentId":"500"},
|
||||
{"id":"506","kind":"UInt32","name":"ProcessTimerDelay","offset":68,"parentId":"500"},
|
||||
{"id":"507","kind":"UInt64","name":"DeepFreezeStartTime","offset":72,"parentId":"500"},
|
||||
{"id":"508","kind":"Pointer64","name":"Affinity","offset":80,"parentId":"500"},
|
||||
{"id":"509","kind":"Hex64","name":"AutoBoostState","offset":88,"parentId":"500"},
|
||||
{"id":"510","kind":"Struct","name":"ReadyListHead","structTypeName":"_LIST_ENTRY","offset":104,"parentId":"500","refId":"100","collapsed":true},
|
||||
{"id":"511","kind":"Struct","name":"SwapListEntry","structTypeName":"_SINGLE_LIST_ENTRY","offset":120,"parentId":"500","refId":"110","collapsed":true},
|
||||
{"id":"512","kind":"Pointer64","name":"ActiveProcessors","offset":128,"parentId":"500"},
|
||||
{"id":"513","kind":"Struct","name":"","classKeyword":"union","offset":136,"parentId":"500","refId":"0","collapsed":false},
|
||||
{"id":"514","kind":"Hex32","name":"AutoAlignment:1 DisableBoost:1 DisableQuantum:1 DeepFreeze:1 TimerVirtualization:1 CheckStackExtents:1 CacheIsolationEnabled:1 PpmPolicy:4 VaSpaceDeleted:1 MultiGroup:1 ForegroundProcess:1 ReservedFlags:18","offset":0,"parentId":"513"},
|
||||
{"id":"515","kind":"Int32","name":"ProcessFlags","offset":0,"parentId":"513"},
|
||||
{"id":"516","kind":"Int8","name":"BasePriority","offset":144,"parentId":"500"},
|
||||
{"id":"517","kind":"Int8","name":"QuantumReset","offset":145,"parentId":"500"},
|
||||
{"id":"518","kind":"Int8","name":"Visited","offset":146,"parentId":"500"},
|
||||
{"id":"519","kind":"Struct","name":"Flags","structTypeName":"_KEXECUTE_OPTIONS","offset":147,"parentId":"500","refId":"190","collapsed":true},
|
||||
{"id":"520","kind":"Struct","name":"StackCount","structTypeName":"_KSTACK_COUNT","offset":264,"parentId":"500","refId":"180","collapsed":true},
|
||||
{"id":"521","kind":"Struct","name":"ProcessListEntry","structTypeName":"_LIST_ENTRY","offset":272,"parentId":"500","refId":"100","collapsed":true},
|
||||
{"id":"522","kind":"UInt64","name":"CycleTime","offset":288,"parentId":"500"},
|
||||
{"id":"523","kind":"UInt64","name":"ContextSwitches","offset":296,"parentId":"500"},
|
||||
{"id":"524","kind":"Pointer64","name":"SchedulingGroup","offset":304,"parentId":"500"},
|
||||
{"id":"525","kind":"UInt64","name":"KernelTime","offset":312,"parentId":"500"},
|
||||
{"id":"526","kind":"UInt64","name":"UserTime","offset":320,"parentId":"500"},
|
||||
{"id":"527","kind":"UInt64","name":"ReadyTime","offset":328,"parentId":"500"},
|
||||
{"id":"528","kind":"UInt32","name":"FreezeCount","offset":336,"parentId":"500"},
|
||||
{"id":"529","kind":"UInt64","name":"UserDirectoryTableBase","offset":344,"parentId":"500"},
|
||||
{"id":"530","kind":"UInt8","name":"AddressPolicy","offset":352,"parentId":"500"},
|
||||
{"id":"531","kind":"Pointer64","name":"InstrumentationCallback","offset":360,"parentId":"500"},
|
||||
{"id":"532","kind":"UInt64","name":"SecureHandle","offset":368,"parentId":"500"},
|
||||
{"id":"533","kind":"UInt64","name":"KernelWaitTime","offset":376,"parentId":"500"},
|
||||
{"id":"534","kind":"UInt64","name":"UserWaitTime","offset":384,"parentId":"500"},
|
||||
{"id":"535","kind":"UInt64","name":"LastRebalanceQpc","offset":392,"parentId":"500"},
|
||||
{"id":"536","kind":"Pointer64","name":"PerProcessorCycleTimes","offset":400,"parentId":"500"},
|
||||
{"id":"537","kind":"UInt64","name":"ExtendedFeatureDisableMask","offset":408,"parentId":"500"},
|
||||
{"id":"538","kind":"UInt16","name":"PrimaryGroup","offset":416,"parentId":"500"},
|
||||
{"id":"539","kind":"Pointer64","name":"UserCetLogging","offset":424,"parentId":"500"},
|
||||
{"id":"540","kind":"Struct","name":"CpuPartitionList","structTypeName":"_LIST_ENTRY","offset":432,"parentId":"500","refId":"100","collapsed":true},
|
||||
{"id":"541","kind":"Pointer64","name":"AvailableCpuState","offset":448,"parentId":"500"},
|
||||
|
||||
{"id":"2000","kind":"Struct","name":"eprocess","structTypeName":"_EPROCESS","offset":0,"parentId":"0","refId":"0","collapsed":false},
|
||||
{"id":"2001","kind":"Struct","name":"Pcb","structTypeName":"_KPROCESS","offset":0,"parentId":"2000","refId":"500","collapsed":true},
|
||||
{"id":"2002","kind":"Struct","name":"ProcessLock","structTypeName":"_EX_PUSH_LOCK","offset":456,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2003","kind":"Pointer64","name":"UniqueProcessId","offset":464,"parentId":"2000"},
|
||||
{"id":"2004","kind":"Struct","name":"ActiveProcessLinks","structTypeName":"_LIST_ENTRY","offset":472,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2005","kind":"Struct","name":"RundownProtect","structTypeName":"_EX_RUNDOWN_REF","offset":488,"parentId":"2000","refId":"130","collapsed":true},
|
||||
{"id":"2006","kind":"Struct","name":"","classKeyword":"union","offset":496,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2007","kind":"UInt32","name":"Flags2","offset":0,"parentId":"2006"},
|
||||
{"id":"2008","kind":"Hex32","name":"JobNotReallyActive:1 AccountingFolded:1 NewProcessReported:1 ExitProcessReported:1 ReportCommitChanges:1 LastReportMemory:1 ForceWakeCharge:1 CrossSessionCreate:1 NeedsHandleRundown:1 RefTraceEnabled:1 PicoCreated:1 EmptyJobEvaluated:1 DefaultPagePriority:3 PrimaryTokenFrozen:1","offset":0,"parentId":"2006"},
|
||||
{"id":"2009","kind":"Struct","name":"","classKeyword":"union","offset":500,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2010","kind":"UInt32","name":"Flags","offset":0,"parentId":"2009"},
|
||||
{"id":"2011","kind":"Hex32","name":"CreateReported:1 NoDebugInherit:1 ProcessExiting:1 ProcessDelete:1 ManageExecutableMemoryWrites:1 VmDeleted:1 OutswapEnabled:1 Outswapped:1 FailFastOnCommitFail:1 Wow64VaSpace4Gb:1 AddressSpaceInitialized:2 SetTimerResolution:1 BreakOnTermination:1","offset":0,"parentId":"2009"},
|
||||
{"id":"2012","kind":"Int64","name":"CreateTime","offset":504,"parentId":"2000"},
|
||||
{"id":"2013","kind":"UInt64","name":"ProcessQuotaUsage_0","offset":512,"parentId":"2000"},
|
||||
{"id":"2014","kind":"UInt64","name":"ProcessQuotaUsage_1","offset":520,"parentId":"2000"},
|
||||
{"id":"2015","kind":"UInt64","name":"ProcessQuotaPeak_0","offset":528,"parentId":"2000"},
|
||||
{"id":"2016","kind":"UInt64","name":"ProcessQuotaPeak_1","offset":536,"parentId":"2000"},
|
||||
{"id":"2017","kind":"UInt64","name":"PeakVirtualSize","offset":544,"parentId":"2000"},
|
||||
{"id":"2018","kind":"UInt64","name":"VirtualSize","offset":552,"parentId":"2000"},
|
||||
{"id":"2019","kind":"Struct","name":"SessionProcessLinks","structTypeName":"_LIST_ENTRY","offset":560,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2020","kind":"Struct","name":"","classKeyword":"union","offset":576,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2021","kind":"Pointer64","name":"ExceptionPortData","offset":0,"parentId":"2020"},
|
||||
{"id":"2022","kind":"UInt64","name":"ExceptionPortValue","offset":0,"parentId":"2020"},
|
||||
{"id":"2023","kind":"Struct","name":"Token","structTypeName":"_EX_FAST_REF","offset":584,"parentId":"2000","refId":"140","collapsed":true},
|
||||
{"id":"2024","kind":"UInt64","name":"MmReserved","offset":592,"parentId":"2000"},
|
||||
{"id":"2025","kind":"Struct","name":"AddressCreationLock","structTypeName":"_EX_PUSH_LOCK","offset":600,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2026","kind":"Struct","name":"PageTableCommitmentLock","structTypeName":"_EX_PUSH_LOCK","offset":608,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2027","kind":"Pointer64","name":"RotateInProgress","offset":616,"parentId":"2000"},
|
||||
{"id":"2028","kind":"Pointer64","name":"ForkInProgress","offset":624,"parentId":"2000"},
|
||||
{"id":"2029","kind":"Pointer64","name":"CommitChargeJob","offset":632,"parentId":"2000"},
|
||||
{"id":"2030","kind":"Struct","name":"CloneRoot","structTypeName":"_RTL_AVL_TREE","offset":640,"parentId":"2000","refId":"170","collapsed":true},
|
||||
{"id":"2031","kind":"UInt64","name":"NumberOfPrivatePages","offset":648,"parentId":"2000"},
|
||||
{"id":"2032","kind":"UInt64","name":"NumberOfLockedPages","offset":656,"parentId":"2000"},
|
||||
{"id":"2033","kind":"Pointer64","name":"Win32Process","offset":664,"parentId":"2000"},
|
||||
{"id":"2034","kind":"Pointer64","name":"Job","offset":672,"parentId":"2000"},
|
||||
{"id":"2035","kind":"Pointer64","name":"SectionObject","offset":680,"parentId":"2000"},
|
||||
{"id":"2036","kind":"Pointer64","name":"SectionBaseAddress","offset":688,"parentId":"2000"},
|
||||
{"id":"2037","kind":"UInt32","name":"Cookie","offset":696,"parentId":"2000"},
|
||||
{"id":"2038","kind":"Pointer64","name":"WorkingSetWatch","offset":704,"parentId":"2000"},
|
||||
{"id":"2039","kind":"Pointer64","name":"Win32WindowStation","offset":712,"parentId":"2000"},
|
||||
{"id":"2040","kind":"Pointer64","name":"InheritedFromUniqueProcessId","offset":720,"parentId":"2000"},
|
||||
{"id":"2041","kind":"UInt64","name":"OwnerProcessId","offset":728,"parentId":"2000"},
|
||||
{"id":"2042","kind":"Pointer64","name":"Peb","offset":736,"parentId":"2000"},
|
||||
{"id":"2043","kind":"Pointer64","name":"Session","offset":744,"parentId":"2000"},
|
||||
{"id":"2044","kind":"Pointer64","name":"Spare1","offset":752,"parentId":"2000"},
|
||||
{"id":"2045","kind":"Pointer64","name":"QuotaBlock","offset":760,"parentId":"2000"},
|
||||
{"id":"2046","kind":"Pointer64","name":"ObjectTable","offset":768,"parentId":"2000"},
|
||||
{"id":"2047","kind":"Pointer64","name":"DebugPort","offset":776,"parentId":"2000"},
|
||||
{"id":"2048","kind":"Pointer64","name":"WoW64Process","offset":784,"parentId":"2000"},
|
||||
{"id":"2049","kind":"Struct","name":"DeviceMap","structTypeName":"_EX_FAST_REF","offset":792,"parentId":"2000","refId":"140","collapsed":true},
|
||||
{"id":"2050","kind":"Pointer64","name":"EtwDataSource","offset":800,"parentId":"2000"},
|
||||
{"id":"2051","kind":"UInt64","name":"PageDirectoryPte","offset":808,"parentId":"2000"},
|
||||
{"id":"2052","kind":"Pointer64","name":"ImageFilePointer","offset":816,"parentId":"2000"},
|
||||
{"id":"2053","kind":"Hex64","name":"ImageFileName_lo","offset":824,"parentId":"2000"},
|
||||
{"id":"2054","kind":"Hex32","name":"ImageFileName_mi","offset":832,"parentId":"2000"},
|
||||
{"id":"2055","kind":"Hex16","name":"ImageFileName_hi","offset":836,"parentId":"2000"},
|
||||
{"id":"2056","kind":"UInt8","name":"ImageFileName_14","offset":838,"parentId":"2000"},
|
||||
{"id":"2057","kind":"UInt8","name":"PriorityClass","offset":839,"parentId":"2000"},
|
||||
{"id":"2058","kind":"Pointer64","name":"SecurityPort","offset":840,"parentId":"2000"},
|
||||
{"id":"2059","kind":"Struct","name":"SeAuditProcessCreationInfo","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":848,"parentId":"2000","refId":"200","collapsed":true},
|
||||
{"id":"2060","kind":"Struct","name":"JobLinks","structTypeName":"_LIST_ENTRY","offset":856,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2061","kind":"Pointer64","name":"HighestUserAddress","offset":872,"parentId":"2000"},
|
||||
{"id":"2062","kind":"Struct","name":"ThreadListHead","structTypeName":"_LIST_ENTRY","offset":880,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2063","kind":"UInt32","name":"ActiveThreads","offset":896,"parentId":"2000"},
|
||||
{"id":"2064","kind":"UInt32","name":"ImagePathHash","offset":900,"parentId":"2000"},
|
||||
{"id":"2065","kind":"UInt32","name":"DefaultHardErrorProcessing","offset":904,"parentId":"2000"},
|
||||
{"id":"2066","kind":"Int32","name":"LastThreadExitStatus","offset":908,"parentId":"2000"},
|
||||
{"id":"2067","kind":"Struct","name":"PrefetchTrace","structTypeName":"_EX_FAST_REF","offset":912,"parentId":"2000","refId":"140","collapsed":true},
|
||||
{"id":"2068","kind":"Pointer64","name":"LockedPagesList","offset":920,"parentId":"2000"},
|
||||
{"id":"2069","kind":"Int64","name":"ReadOperationCount","offset":928,"parentId":"2000"},
|
||||
{"id":"2070","kind":"Int64","name":"WriteOperationCount","offset":936,"parentId":"2000"},
|
||||
{"id":"2071","kind":"Int64","name":"OtherOperationCount","offset":944,"parentId":"2000"},
|
||||
{"id":"2072","kind":"Int64","name":"ReadTransferCount","offset":952,"parentId":"2000"},
|
||||
{"id":"2073","kind":"Int64","name":"WriteTransferCount","offset":960,"parentId":"2000"},
|
||||
{"id":"2074","kind":"Int64","name":"OtherTransferCount","offset":968,"parentId":"2000"},
|
||||
{"id":"2075","kind":"UInt64","name":"CommitChargeLimit","offset":976,"parentId":"2000"},
|
||||
{"id":"2076","kind":"UInt64","name":"CommitCharge","offset":984,"parentId":"2000"},
|
||||
{"id":"2077","kind":"UInt64","name":"CommitChargePeak","offset":992,"parentId":"2000"},
|
||||
{"id":"2078","kind":"Struct","name":"Vm","structTypeName":"_MMSUPPORT_FULL","offset":1024,"parentId":"2000","refId":"350","collapsed":true},
|
||||
{"id":"2079","kind":"Struct","name":"MmProcessLinks","structTypeName":"_LIST_ENTRY","offset":1344,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2080","kind":"UInt32","name":"ModifiedPageCount","offset":1360,"parentId":"2000"},
|
||||
{"id":"2081","kind":"Int32","name":"ExitStatus","offset":1364,"parentId":"2000"},
|
||||
{"id":"2082","kind":"Struct","name":"VadRoot","structTypeName":"_RTL_AVL_TREE","offset":1368,"parentId":"2000","refId":"170","collapsed":true},
|
||||
{"id":"2083","kind":"Pointer64","name":"VadHint","offset":1376,"parentId":"2000"},
|
||||
{"id":"2084","kind":"UInt64","name":"VadCount","offset":1384,"parentId":"2000"},
|
||||
{"id":"2085","kind":"UInt64","name":"VadPhysicalPages","offset":1392,"parentId":"2000"},
|
||||
{"id":"2086","kind":"UInt64","name":"VadPhysicalPagesLimit","offset":1400,"parentId":"2000"},
|
||||
{"id":"2087","kind":"Struct","name":"AlpcContext","structTypeName":"_ALPC_PROCESS_CONTEXT","offset":1408,"parentId":"2000","refId":"250","collapsed":true},
|
||||
{"id":"2088","kind":"Struct","name":"TimerResolutionLink","structTypeName":"_LIST_ENTRY","offset":1440,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2089","kind":"Pointer64","name":"TimerResolutionStackRecord","offset":1456,"parentId":"2000"},
|
||||
{"id":"2090","kind":"UInt32","name":"RequestedTimerResolution","offset":1464,"parentId":"2000"},
|
||||
{"id":"2091","kind":"UInt32","name":"SmallestTimerResolution","offset":1468,"parentId":"2000"},
|
||||
{"id":"2092","kind":"Int64","name":"ExitTime","offset":1472,"parentId":"2000"},
|
||||
{"id":"2093","kind":"Pointer64","name":"InvertedFunctionTable","offset":1480,"parentId":"2000"},
|
||||
{"id":"2094","kind":"Struct","name":"InvertedFunctionTableLock","structTypeName":"_EX_PUSH_LOCK","offset":1488,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2095","kind":"UInt32","name":"ActiveThreadsHighWatermark","offset":1496,"parentId":"2000"},
|
||||
{"id":"2096","kind":"UInt32","name":"LargePrivateVadCount","offset":1500,"parentId":"2000"},
|
||||
{"id":"2097","kind":"Struct","name":"ThreadListLock","structTypeName":"_EX_PUSH_LOCK","offset":1504,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2098","kind":"Pointer64","name":"WnfContext","offset":1512,"parentId":"2000"},
|
||||
{"id":"2099","kind":"Pointer64","name":"ServerSilo","offset":1520,"parentId":"2000"},
|
||||
{"id":"2100","kind":"UInt8","name":"SignatureLevel","offset":1528,"parentId":"2000"},
|
||||
{"id":"2101","kind":"UInt8","name":"SectionSignatureLevel","offset":1529,"parentId":"2000"},
|
||||
{"id":"2102","kind":"Struct","name":"Protection","structTypeName":"_PS_PROTECTION","offset":1530,"parentId":"2000","refId":"210","collapsed":true},
|
||||
{"id":"2103","kind":"Hex8","name":"HangCount:3 GhostCount:3 PrefilterException:1","offset":1531,"parentId":"2000"},
|
||||
{"id":"2104","kind":"Struct","name":"","classKeyword":"union","offset":1532,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2105","kind":"UInt32","name":"Flags3","offset":0,"parentId":"2104"},
|
||||
{"id":"2106","kind":"Hex32","name":"Minimal:1 ReplacingPageRoot:1 Crashed:1 JobVadsAreTracked:1 VadTrackingDisabled:1 AuxiliaryProcess:1 SubsystemProcess:1 IndirectCpuSets:1 RelinquishedCommit:1 HighGraphicsPriority:1 CommitFailLogged:1 ReserveFailLogged:1 SystemProcess:1","offset":0,"parentId":"2104"},
|
||||
{"id":"2107","kind":"Int32","name":"DeviceAsid","offset":1536,"parentId":"2000"},
|
||||
{"id":"2108","kind":"Pointer64","name":"SvmData","offset":1544,"parentId":"2000"},
|
||||
{"id":"2109","kind":"Struct","name":"SvmProcessLock","structTypeName":"_EX_PUSH_LOCK","offset":1552,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2110","kind":"UInt64","name":"SvmLock","offset":1560,"parentId":"2000"},
|
||||
{"id":"2111","kind":"Struct","name":"SvmProcessDeviceListHead","structTypeName":"_LIST_ENTRY","offset":1568,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2112","kind":"UInt64","name":"LastFreezeInterruptTime","offset":1584,"parentId":"2000"},
|
||||
{"id":"2113","kind":"Pointer64","name":"DiskCounters","offset":1592,"parentId":"2000"},
|
||||
{"id":"2114","kind":"Pointer64","name":"PicoContext","offset":1600,"parentId":"2000"},
|
||||
{"id":"2115","kind":"Pointer64","name":"EnclaveTable","offset":1608,"parentId":"2000"},
|
||||
{"id":"2116","kind":"UInt64","name":"EnclaveNumber","offset":1616,"parentId":"2000"},
|
||||
{"id":"2117","kind":"Struct","name":"EnclaveLock","structTypeName":"_EX_PUSH_LOCK","offset":1624,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2118","kind":"UInt32","name":"HighPriorityFaultsAllowed","offset":1632,"parentId":"2000"},
|
||||
{"id":"2119","kind":"Pointer64","name":"EnergyContext","offset":1640,"parentId":"2000"},
|
||||
{"id":"2120","kind":"Pointer64","name":"VmContext","offset":1648,"parentId":"2000"},
|
||||
{"id":"2121","kind":"UInt64","name":"SequenceNumber","offset":1656,"parentId":"2000"},
|
||||
{"id":"2122","kind":"UInt64","name":"CreateInterruptTime","offset":1664,"parentId":"2000"},
|
||||
{"id":"2123","kind":"UInt64","name":"CreateUnbiasedInterruptTime","offset":1672,"parentId":"2000"},
|
||||
{"id":"2124","kind":"UInt64","name":"TotalUnbiasedFrozenTime","offset":1680,"parentId":"2000"},
|
||||
{"id":"2125","kind":"UInt64","name":"LastAppStateUpdateTime","offset":1688,"parentId":"2000"},
|
||||
{"id":"2126","kind":"Hex64","name":"LastAppStateUptime:61 LastAppState:3","offset":1696,"parentId":"2000"},
|
||||
{"id":"2127","kind":"UInt64","name":"SharedCommitCharge","offset":1704,"parentId":"2000"},
|
||||
{"id":"2128","kind":"Struct","name":"SharedCommitLock","structTypeName":"_EX_PUSH_LOCK","offset":1712,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2129","kind":"Struct","name":"SharedCommitLinks","structTypeName":"_LIST_ENTRY","offset":1720,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2130","kind":"UInt64","name":"AllowedCpuSets","offset":1736,"parentId":"2000"},
|
||||
{"id":"2131","kind":"UInt64","name":"DefaultCpuSets","offset":1744,"parentId":"2000"},
|
||||
{"id":"2132","kind":"Pointer64","name":"DiskIoAttribution","offset":1752,"parentId":"2000"},
|
||||
{"id":"2133","kind":"Pointer64","name":"DxgProcess","offset":1760,"parentId":"2000"},
|
||||
{"id":"2134","kind":"UInt32","name":"Win32KFilterSet","offset":1768,"parentId":"2000"},
|
||||
{"id":"2135","kind":"UInt16","name":"Machine","offset":1772,"parentId":"2000"},
|
||||
{"id":"2136","kind":"UInt8","name":"MmSlabIdentity","offset":1774,"parentId":"2000"},
|
||||
{"id":"2137","kind":"UInt8","name":"Spare0","offset":1775,"parentId":"2000"},
|
||||
{"id":"2138","kind":"Struct","name":"ProcessTimerDelay","structTypeName":"_PS_INTERLOCKED_TIMER_DELAY_VALUES","offset":1776,"parentId":"2000","refId":"220","collapsed":true},
|
||||
{"id":"2139","kind":"UInt32","name":"KTimerSets","offset":1784,"parentId":"2000"},
|
||||
{"id":"2140","kind":"UInt32","name":"KTimer2Sets","offset":1788,"parentId":"2000"},
|
||||
{"id":"2141","kind":"UInt32","name":"ThreadTimerSets","offset":1792,"parentId":"2000"},
|
||||
{"id":"2142","kind":"UInt64","name":"VirtualTimerListLock","offset":1800,"parentId":"2000"},
|
||||
{"id":"2143","kind":"Struct","name":"VirtualTimerListHead","structTypeName":"_LIST_ENTRY","offset":1808,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2144","kind":"Struct","name":"WakeChannel","structTypeName":"_WNF_STATE_NAME","offset":1824,"parentId":"2000","refId":"230","collapsed":true},
|
||||
{"id":"2145","kind":"Struct","name":"","classKeyword":"union","offset":1872,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2146","kind":"UInt32","name":"MitigationFlags","offset":0,"parentId":"2145"},
|
||||
{"id":"2147","kind":"Hex32","name":"ControlFlowGuardEnabled:1 ControlFlowGuardExportSuppressionEnabled:1 ControlFlowGuardStrict:1 DisallowStrippedImages:1 ForceRelocateImages:1 HighEntropyASLREnabled:1 StackRandomizationDisabled:1 ExtensionPointDisable:1 DisableDynamicCode:1","offset":0,"parentId":"2145"},
|
||||
{"id":"2148","kind":"Struct","name":"","classKeyword":"union","offset":1876,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2149","kind":"UInt32","name":"MitigationFlags2","offset":0,"parentId":"2148"},
|
||||
{"id":"2150","kind":"Hex32","name":"EnableExportAddressFilter:1 AuditExportAddressFilter:1 EnableRopStackPivot:1 AuditRopStackPivot:1 CetUserShadowStacks:1 SpeculativeStoreBypassDisable:1","offset":0,"parentId":"2148"},
|
||||
{"id":"2151","kind":"Pointer64","name":"PartitionObject","offset":1880,"parentId":"2000"},
|
||||
{"id":"2152","kind":"UInt64","name":"SecurityDomain","offset":1888,"parentId":"2000"},
|
||||
{"id":"2153","kind":"UInt64","name":"ParentSecurityDomain","offset":1896,"parentId":"2000"},
|
||||
{"id":"2154","kind":"Pointer64","name":"CoverageSamplerContext","offset":1904,"parentId":"2000"},
|
||||
{"id":"2155","kind":"Pointer64","name":"MmHotPatchContext","offset":1912,"parentId":"2000"},
|
||||
{"id":"2156","kind":"Struct","name":"DynamicEHContinuationTargetsTree","structTypeName":"_RTL_AVL_TREE","offset":1920,"parentId":"2000","refId":"170","collapsed":true},
|
||||
{"id":"2157","kind":"Struct","name":"DynamicEHContinuationTargetsLock","structTypeName":"_EX_PUSH_LOCK","offset":1928,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2158","kind":"Struct","name":"DynamicEnforcedCetCompatibleRanges","structTypeName":"_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES","offset":1936,"parentId":"2000","refId":"240","collapsed":true},
|
||||
{"id":"2159","kind":"UInt32","name":"DisabledComponentFlags","offset":1952,"parentId":"2000"},
|
||||
{"id":"2160","kind":"Int32","name":"PageCombineSequence","offset":1956,"parentId":"2000"},
|
||||
{"id":"2161","kind":"Struct","name":"EnableOptionalXStateFeaturesLock","structTypeName":"_EX_PUSH_LOCK","offset":1960,"parentId":"2000","refId":"120","collapsed":true},
|
||||
{"id":"2162","kind":"Pointer64","name":"PathRedirectionHashes","offset":1968,"parentId":"2000"},
|
||||
{"id":"2163","kind":"Pointer64","name":"SyscallProvider","offset":1976,"parentId":"2000"},
|
||||
{"id":"2164","kind":"Struct","name":"SyscallProviderProcessLinks","structTypeName":"_LIST_ENTRY","offset":1984,"parentId":"2000","refId":"100","collapsed":true},
|
||||
{"id":"2165","kind":"Hex64","name":"SyscallProviderDispatchContext","offset":2000,"parentId":"2000"},
|
||||
{"id":"2166","kind":"Struct","name":"","classKeyword":"union","offset":2008,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2167","kind":"UInt32","name":"MitigationFlags3","offset":0,"parentId":"2166"},
|
||||
{"id":"2168","kind":"Hex32","name":"RestrictCoreSharing:1 DisallowFsctlSystemCalls:1 AuditDisallowFsctlSystemCalls:1 MitigationFlags3Spare:29","offset":0,"parentId":"2166"},
|
||||
{"id":"2169","kind":"Struct","name":"","classKeyword":"union","offset":2012,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2170","kind":"UInt32","name":"Flags4","offset":0,"parentId":"2169"},
|
||||
{"id":"2171","kind":"Hex32","name":"ThreadWasActive:1 MinimalTerminate:1 ImageExpansionDisable:1 SessionFirstProcess:1","offset":0,"parentId":"2169"},
|
||||
{"id":"2172","kind":"Struct","name":"","classKeyword":"union","offset":2016,"parentId":"2000","refId":"0","collapsed":true},
|
||||
{"id":"2173","kind":"UInt32","name":"SyscallUsage","offset":0,"parentId":"2172"},
|
||||
{"id":"2174","kind":"Hex32","name":"SystemModuleInformation:1 SystemModuleInformationEx:1 SystemLocksInformation:1 SystemHandleInformation:1 SystemExtendedHandleInformation:1","offset":0,"parentId":"2172"},
|
||||
{"id":"2175","kind":"Int32","name":"SupervisorDeviceAsid","offset":2020,"parentId":"2000"},
|
||||
{"id":"2176","kind":"Pointer64","name":"SupervisorSvmData","offset":2024,"parentId":"2000"},
|
||||
{"id":"2177","kind":"Pointer64","name":"NetworkCounters","offset":2032,"parentId":"2000"},
|
||||
{"id":"2178","kind":"Hex64","name":"Execution","offset":2040,"parentId":"2000"},
|
||||
{"id":"2179","kind":"Pointer64","name":"ThreadIndexTable","offset":2048,"parentId":"2000"},
|
||||
{"id":"2180","kind":"Struct","name":"FreezeWorkLinks","structTypeName":"_LIST_ENTRY","offset":2056,"parentId":"2000","refId":"100","collapsed":true}
|
||||
]
|
||||
}
|
||||
616
src/examples/MMPFN.rcx
Normal file
616
src/examples/MMPFN.rcx
Normal file
@@ -0,0 +1,616 @@
|
||||
{
|
||||
"baseAddress": "FFFFCA8010000000",
|
||||
"nextId": "3000",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "100",
|
||||
"kind": "Struct",
|
||||
"name": "list_entry",
|
||||
"structTypeName": "_LIST_ENTRY",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "101",
|
||||
"kind": "Pointer64",
|
||||
"name": "Flink",
|
||||
"offset": 0,
|
||||
"parentId": "100",
|
||||
"refId": "100",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "102",
|
||||
"kind": "Pointer64",
|
||||
"name": "Blink",
|
||||
"offset": 8,
|
||||
"parentId": "100",
|
||||
"refId": "100",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "200",
|
||||
"kind": "Struct",
|
||||
"name": "balanced_node",
|
||||
"structTypeName": "_RTL_BALANCED_NODE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "210",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 0,
|
||||
"parentId": "200",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "211",
|
||||
"kind": "Pointer64",
|
||||
"name": "Left",
|
||||
"offset": 0,
|
||||
"parentId": "210",
|
||||
"refId": "200",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "212",
|
||||
"kind": "Pointer64",
|
||||
"name": "Right",
|
||||
"offset": 8,
|
||||
"parentId": "210",
|
||||
"refId": "200",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "220",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 16,
|
||||
"parentId": "200",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "221",
|
||||
"kind": "UInt64",
|
||||
"name": "ParentValue",
|
||||
"offset": 0,
|
||||
"parentId": "220"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "300",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte",
|
||||
"structTypeName": "_MMPTE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "301",
|
||||
"kind": "Struct",
|
||||
"name": "u",
|
||||
"classKeyword": "union",
|
||||
"offset": 0,
|
||||
"parentId": "300",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "302",
|
||||
"kind": "UInt64",
|
||||
"name": "Long",
|
||||
"offset": 0,
|
||||
"parentId": "301"
|
||||
},
|
||||
{
|
||||
"id": "303",
|
||||
"kind": "Struct",
|
||||
"name": "Hard",
|
||||
"structTypeName": "_MMPTE_HARDWARE",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "400",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "304",
|
||||
"kind": "Struct",
|
||||
"name": "Proto",
|
||||
"structTypeName": "_MMPTE_PROTOTYPE",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "600",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "305",
|
||||
"kind": "Struct",
|
||||
"name": "Soft",
|
||||
"structTypeName": "_MMPTE_SOFTWARE",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "500",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "306",
|
||||
"kind": "Struct",
|
||||
"name": "Trans",
|
||||
"structTypeName": "_MMPTE_TRANSITION",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "700",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "307",
|
||||
"kind": "Struct",
|
||||
"name": "Subsect",
|
||||
"structTypeName": "_MMPTE_SUBSECTION",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "800",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "308",
|
||||
"kind": "Struct",
|
||||
"name": "TimeStamp",
|
||||
"structTypeName": "_MMPTE_TIMESTAMP",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "900",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "309",
|
||||
"kind": "Struct",
|
||||
"name": "List",
|
||||
"structTypeName": "_MMPTE_LIST",
|
||||
"offset": 0,
|
||||
"parentId": "301",
|
||||
"refId": "1000",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "400",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_hardware",
|
||||
"structTypeName": "_MMPTE_HARDWARE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "401",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 Dirty1:1 Owner:1 WriteThrough:1 CacheDisable:1 Accessed:1 Dirty:1 LargePage:1 Global:1 CopyOnWrite:1 Unused:1 Write:1 PageFrameNumber:40 ReservedForSoftware:4 WsleAge:4 WsleProtection:3 NoExecute:1",
|
||||
"offset": 0,
|
||||
"parentId": "400"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "500",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_software",
|
||||
"structTypeName": "_MMPTE_SOFTWARE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "501",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 PageFileReserved:1 PageFileAllocated:1 ColdPage:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 UsedPageTableEntries:10 ShadowStack:1 OnStandbyLookaside:1 Unused:4 PageFileHigh:32",
|
||||
"offset": 0,
|
||||
"parentId": "500"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "600",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_prototype",
|
||||
"structTypeName": "_MMPTE_PROTOTYPE",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "601",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 DemandFillProto:1 HiberVerifyConverted:1 ReadOnly:1 SwizzleBit:1 Protection:5 Prototype:1 Combined:1 Unused1:4 ProtoAddress:48",
|
||||
"offset": 0,
|
||||
"parentId": "600"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "700",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_transition",
|
||||
"structTypeName": "_MMPTE_TRANSITION",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "701",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 Write:1 OnStandbyLookaside:1 IoTracker:1 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFrameNumber:40 Unused:12",
|
||||
"offset": 0,
|
||||
"parentId": "700"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "800",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_subsection",
|
||||
"structTypeName": "_MMPTE_SUBSECTION",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "801",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 Unused0:2 OnStandbyLookaside:1 SwizzleBit:1 Protection:5 Prototype:1 ColdPage:1 Unused2:3 ExecutePrivilege:1 SubsectionAddress:48",
|
||||
"offset": 0,
|
||||
"parentId": "800"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "900",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_timestamp",
|
||||
"structTypeName": "_MMPTE_TIMESTAMP",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "901",
|
||||
"kind": "Hex64",
|
||||
"name": "MustBeZero:1 Unused:3 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 PageFileLow:4 Reserved:16 GlobalTimeStamp:32",
|
||||
"offset": 0,
|
||||
"parentId": "900"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1000",
|
||||
"kind": "Struct",
|
||||
"name": "mmpte_list",
|
||||
"structTypeName": "_MMPTE_LIST",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1001",
|
||||
"kind": "Hex64",
|
||||
"name": "Valid:1 OneEntry:1 filler0:2 SwizzleBit:1 Protection:5 Prototype:1 Transition:1 filler1:13 NextEntry:39",
|
||||
"offset": 0,
|
||||
"parentId": "1000"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1100",
|
||||
"kind": "Struct",
|
||||
"name": "mipfnflink",
|
||||
"structTypeName": "_MIPFNFLINK",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1101",
|
||||
"kind": "Hex64",
|
||||
"name": "Flink",
|
||||
"offset": 0,
|
||||
"parentId": "1100"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1200",
|
||||
"kind": "Struct",
|
||||
"name": "mipfnblink",
|
||||
"structTypeName": "_MIPFNBLINK",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1201",
|
||||
"kind": "Hex64",
|
||||
"name": "Blink",
|
||||
"offset": 0,
|
||||
"parentId": "1200"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1300",
|
||||
"kind": "Struct",
|
||||
"name": "mmpfnentry1",
|
||||
"structTypeName": "_MMPFNENTRY1",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1301",
|
||||
"kind": "Hex8",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1300"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1400",
|
||||
"kind": "Struct",
|
||||
"name": "mmpfnentry3",
|
||||
"structTypeName": "_MMPFNENTRY3",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1401",
|
||||
"kind": "Hex8",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1400"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1500",
|
||||
"kind": "Struct",
|
||||
"name": "mi_pfn_flags",
|
||||
"structTypeName": "_MI_PFN_FLAGS",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1501",
|
||||
"kind": "Hex32",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1500"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1600",
|
||||
"kind": "Struct",
|
||||
"name": "mi_pfn_flags4",
|
||||
"structTypeName": "_MI_PFN_FLAGS4",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1601",
|
||||
"kind": "Hex64",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1600"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "1700",
|
||||
"kind": "Struct",
|
||||
"name": "mi_pfn_flags5",
|
||||
"structTypeName": "_MI_PFN_FLAGS5",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "1701",
|
||||
"kind": "Hex32",
|
||||
"name": "Flags",
|
||||
"offset": 0,
|
||||
"parentId": "1700"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2000",
|
||||
"kind": "Struct",
|
||||
"name": "mmpfn",
|
||||
"structTypeName": "_MMPFN",
|
||||
"offset": 0,
|
||||
"parentId": "0",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2001",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 0,
|
||||
"parentId": "2000",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2010",
|
||||
"kind": "Struct",
|
||||
"name": "ListEntry",
|
||||
"structTypeName": "_LIST_ENTRY",
|
||||
"offset": 0,
|
||||
"parentId": "2001",
|
||||
"refId": "100",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2011",
|
||||
"kind": "Struct",
|
||||
"name": "TreeNode",
|
||||
"structTypeName": "_RTL_BALANCED_NODE",
|
||||
"offset": 0,
|
||||
"parentId": "2001",
|
||||
"refId": "200",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2012",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"offset": 0,
|
||||
"parentId": "2001",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2013",
|
||||
"kind": "Struct",
|
||||
"name": "u1",
|
||||
"structTypeName": "_MIPFNFLINK",
|
||||
"offset": 0,
|
||||
"parentId": "2012",
|
||||
"refId": "1100",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2014",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"classKeyword": "union",
|
||||
"offset": 8,
|
||||
"parentId": "2012",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2015",
|
||||
"kind": "Pointer64",
|
||||
"name": "PteAddress",
|
||||
"offset": 0,
|
||||
"parentId": "2014",
|
||||
"refId": "300",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2016",
|
||||
"kind": "UInt64",
|
||||
"name": "PteLong",
|
||||
"offset": 0,
|
||||
"parentId": "2014"
|
||||
},
|
||||
{
|
||||
"id": "2017",
|
||||
"kind": "Struct",
|
||||
"name": "OriginalPte",
|
||||
"structTypeName": "_MMPTE",
|
||||
"offset": 16,
|
||||
"parentId": "2012",
|
||||
"refId": "300",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2020",
|
||||
"kind": "Struct",
|
||||
"name": "u2",
|
||||
"structTypeName": "_MIPFNBLINK",
|
||||
"offset": 24,
|
||||
"parentId": "2000",
|
||||
"refId": "1200",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2030",
|
||||
"kind": "Struct",
|
||||
"name": "u3",
|
||||
"classKeyword": "union",
|
||||
"offset": 32,
|
||||
"parentId": "2000",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2031",
|
||||
"kind": "Struct",
|
||||
"name": "",
|
||||
"offset": 0,
|
||||
"parentId": "2030",
|
||||
"refId": "0",
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "2032",
|
||||
"kind": "UInt16",
|
||||
"name": "ReferenceCount",
|
||||
"offset": 0,
|
||||
"parentId": "2031"
|
||||
},
|
||||
{
|
||||
"id": "2033",
|
||||
"kind": "Struct",
|
||||
"name": "e1",
|
||||
"structTypeName": "_MMPFNENTRY1",
|
||||
"offset": 2,
|
||||
"parentId": "2031",
|
||||
"refId": "1300",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "2034",
|
||||
"kind": "Struct",
|
||||
"name": "e4",
|
||||
"structTypeName": "_MI_PFN_FLAGS",
|
||||
"offset": 0,
|
||||
"parentId": "2030",
|
||||
"refId": "1500",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2040",
|
||||
"kind": "Struct",
|
||||
"name": "u5",
|
||||
"structTypeName": "_MI_PFN_FLAGS5",
|
||||
"offset": 36,
|
||||
"parentId": "2000",
|
||||
"refId": "1700",
|
||||
"collapsed": true
|
||||
},
|
||||
|
||||
{
|
||||
"id": "2050",
|
||||
"kind": "Struct",
|
||||
"name": "u4",
|
||||
"structTypeName": "_MI_PFN_FLAGS4",
|
||||
"offset": 40,
|
||||
"parentId": "2000",
|
||||
"refId": "1600",
|
||||
"collapsed": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,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
|
||||
@@ -140,8 +148,8 @@ QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDig
|
||||
// ── Struct type name (for width calculation) ──
|
||||
|
||||
QString structTypeName(const Node& node) {
|
||||
// Full type string: "struct TypeName" or just "struct" if no typename
|
||||
QString base = typeName(node.kind).trimmed(); // "struct"
|
||||
// Full type string: "struct TypeName", "union TypeName", "class TypeName", etc.
|
||||
QString base = node.resolvedClassKeyword();
|
||||
if (!node.structTypeName.isEmpty())
|
||||
return base + QStringLiteral(" ") + node.structTypeName;
|
||||
return base;
|
||||
@@ -149,11 +157,16 @@ QString structTypeName(const Node& node) {
|
||||
|
||||
// ── Struct header / footer ──
|
||||
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName) {
|
||||
QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName, bool compact) {
|
||||
// Columnar format: <type> <name> { (or no brace when collapsed)
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(structTypeName(node), colType);
|
||||
QString rawType = structTypeName(node);
|
||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||
if (node.name.isEmpty()) {
|
||||
// Anonymous struct/union: "union {" with no column padding
|
||||
return ind + rawType + SEP + suffix;
|
||||
}
|
||||
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
|
||||
return ind + type + SEP + node.name + SEP + suffix;
|
||||
}
|
||||
|
||||
@@ -163,9 +176,10 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
||||
|
||||
// ── Array header ──
|
||||
// Columnar format: <type[count]> <name> { (or no brace when collapsed)
|
||||
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName) {
|
||||
QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName, const QString& elemStructName, bool compact) {
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(arrayTypeName(node.elementKind, node.arrayLen, elemStructName), colType);
|
||||
QString rawType = arrayTypeName(node.elementKind, node.arrayLen, elemStructName);
|
||||
QString type = compact ? fitOverflow(rawType, colType) : fit(rawType, colType);
|
||||
QString suffix = collapsed ? QString() : QStringLiteral("{");
|
||||
return ind + type + SEP + node.name + SEP + suffix;
|
||||
}
|
||||
@@ -174,10 +188,16 @@ QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collap
|
||||
|
||||
QString fmtPointerHeader(const Node& node, int depth, bool collapsed,
|
||||
const Provider& prov, uint64_t addr,
|
||||
const QString& ptrTypeName, int colType, int colName) {
|
||||
const QString& ptrTypeName, int colType, int colName,
|
||||
bool compact) {
|
||||
QString ind = indent(depth);
|
||||
QString type = fit(ptrTypeName, colType);
|
||||
bool overflow = compact && ptrTypeName.size() > colType;
|
||||
QString type = compact ? fitOverflow(ptrTypeName, colType) : fit(ptrTypeName, colType);
|
||||
if (collapsed) {
|
||||
if (overflow) {
|
||||
// Overflow: no column padding
|
||||
return ind + type + SEP + node.name + SEP + readValue(node, prov, addr, 0);
|
||||
}
|
||||
// Collapsed: show pointer value instead of brace (name padded for value alignment)
|
||||
QString name = fit(node.name, colName);
|
||||
QString val = fit(readValue(node, prov, addr, 0), COL_VALUE);
|
||||
@@ -366,12 +386,22 @@ QString readValue(const Node& node, const Provider& prov,
|
||||
QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
uint64_t addr, int depth, int subLine,
|
||||
const QString& comment, int colType, int colName,
|
||||
const QString& typeOverride) {
|
||||
const QString& typeOverride, bool compact) {
|
||||
QString ind = indent(depth);
|
||||
QString type = typeOverride.isEmpty() ? typeName(node.kind, colType) : fit(typeOverride, colType);
|
||||
QString name = fit(node.name, colName);
|
||||
|
||||
// Compute raw type string for overflow detection
|
||||
QString rawType = typeOverride.isEmpty() ? typeNameRaw(node.kind) : typeOverride;
|
||||
bool overflow = compact && rawType.size() > colType;
|
||||
|
||||
QString type = overflow ? fitOverflow(rawType, colType)
|
||||
: (typeOverride.isEmpty() ? typeName(node.kind, colType)
|
||||
: fit(typeOverride, colType));
|
||||
QString name = overflow ? node.name : fit(node.name, colName);
|
||||
|
||||
// Effective column width for this line (accounts for overflow, clamped to hard max)
|
||||
int effectiveColType = overflow ? rawType.size() : colType;
|
||||
// Blank prefix for continuation lines (same width as type+sep+name+sep)
|
||||
const int prefixW = colType + colName + 2 * kSepWidth;
|
||||
const int prefixW = effectiveColType + (overflow ? name.size() : colName) + 2 * kSepWidth;
|
||||
|
||||
// Comment suffix (only present when a comment is provided; no trailing padding)
|
||||
QString cmtSuffix = comment.isEmpty() ? QString()
|
||||
@@ -394,7 +424,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
}
|
||||
|
||||
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||
QString val = overflow ? readValue(node, prov, addr, subLine)
|
||||
: fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||
return ind + type + SEP + name + SEP + val + cmtSuffix;
|
||||
}
|
||||
|
||||
@@ -674,4 +705,9 @@ QString validateBaseAddress(const QString& text) {
|
||||
return AddressParser::validate(s);
|
||||
}
|
||||
|
||||
QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW) {
|
||||
QString ind = indent(depth);
|
||||
return ind + name.leftJustified(nameW) + QStringLiteral(" = ") + QString::number(value);
|
||||
}
|
||||
|
||||
} // namespace rcx::fmt
|
||||
|
||||
@@ -315,7 +315,8 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
&& !ctx.forwardDeclared.contains(child.refId)) {
|
||||
QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]);
|
||||
QString fwdKw = ctx.tree.nodes[refIdx].resolvedClassKeyword();
|
||||
if (fwdKw == QStringLiteral("enum")) fwdKw = QStringLiteral("struct");
|
||||
if (fwdKw == QStringLiteral("enum") && ctx.tree.nodes[refIdx].enumMembers.isEmpty())
|
||||
fwdKw = QStringLiteral("struct");
|
||||
ctx.output += QStringLiteral("%1 %2;\n").arg(fwdKw, fwdName);
|
||||
ctx.forwardDeclared.insert(child.refId);
|
||||
}
|
||||
@@ -327,7 +328,21 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
||||
|
||||
QString kw = node.resolvedClassKeyword();
|
||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum is cosmetic
|
||||
|
||||
// Enum with members: emit as proper C enum
|
||||
if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
|
||||
ctx.output += QStringLiteral("enum %1 {\n").arg(typeName);
|
||||
for (const auto& m : node.enumMembers) {
|
||||
ctx.output += QStringLiteral(" %1 = %2,\n")
|
||||
.arg(sanitizeIdent(m.first))
|
||||
.arg(m.second);
|
||||
}
|
||||
ctx.output += QStringLiteral("};\n\n");
|
||||
ctx.visiting.remove(structId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum without members: fallback
|
||||
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
|
||||
|
||||
emitStructBody(ctx, structId);
|
||||
|
||||
@@ -232,10 +232,16 @@ struct PdbCtx {
|
||||
NodeTree tree;
|
||||
const TypeTable* tt = nullptr;
|
||||
QHash<uint32_t, uint64_t> typeCache; // typeIndex → nodeId
|
||||
QHash<QString, uint32_t> structDefByName; // struct/class definition name → typeIndex
|
||||
QHash<QString, uint32_t> unionDefByName; // union definition name → typeIndex
|
||||
bool udtDefIndexBuilt = false;
|
||||
|
||||
uint64_t importUDT(uint32_t typeIndex);
|
||||
uint64_t importEnum(uint32_t typeIndex);
|
||||
void importFieldList(uint32_t fieldListIndex, uint64_t parentId);
|
||||
void importMemberType(uint32_t typeIndex, int offset, const QString& name, uint64_t parentId);
|
||||
void buildUdtDefinitionIndex();
|
||||
uint32_t findUdtDefinitionIndex(TRK kind, const char* typeName);
|
||||
|
||||
// Resolve LF_MODIFIER chain to underlying type index
|
||||
uint32_t unwrapModifier(uint32_t typeIndex) const {
|
||||
@@ -248,6 +254,56 @@ struct PdbCtx {
|
||||
}
|
||||
};
|
||||
|
||||
void PdbCtx::buildUdtDefinitionIndex() {
|
||||
if (udtDefIndexBuilt || !tt) return;
|
||||
udtDefIndexBuilt = true;
|
||||
|
||||
for (uint32_t ti = tt->firstIndex(); ti < tt->lastIndex(); ti++) {
|
||||
const auto* rec = tt->get(ti);
|
||||
if (!rec) continue;
|
||||
|
||||
bool isUnion = false;
|
||||
bool isFwd = false;
|
||||
const char* candidateName = nullptr;
|
||||
|
||||
if (rec->header.kind == TRK::LF_UNION) {
|
||||
isUnion = true;
|
||||
isFwd = rec->data.LF_UNION.property.fwdref;
|
||||
candidateName = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
|
||||
} else if (rec->header.kind == TRK::LF_STRUCTURE || rec->header.kind == TRK::LF_CLASS) {
|
||||
isFwd = rec->data.LF_CLASS.property.fwdref;
|
||||
candidateName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFwd || !candidateName || candidateName[0] == '\0') continue;
|
||||
|
||||
QString qname = QString::fromUtf8(candidateName);
|
||||
QHash<QString, uint32_t>& lookup = isUnion ? unionDefByName : structDefByName;
|
||||
if (!lookup.contains(qname)) lookup.insert(qname, ti);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t PdbCtx::findUdtDefinitionIndex(TRK kind, const char* typeName) {
|
||||
if (!typeName || typeName[0] == '\0') return 0;
|
||||
|
||||
buildUdtDefinitionIndex();
|
||||
|
||||
const QString qname = QString::fromUtf8(typeName);
|
||||
if (kind == TRK::LF_UNION) {
|
||||
auto it = unionDefByName.constFind(qname);
|
||||
return (it != unionDefByName.cend()) ? it.value() : 0;
|
||||
}
|
||||
|
||||
if (kind == TRK::LF_STRUCTURE || kind == TRK::LF_CLASS) {
|
||||
auto it = structDefByName.constFind(qname);
|
||||
return (it != structDefByName.cend()) ? it.value() : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t PdbCtx::importUDT(uint32_t typeIndex) {
|
||||
if (typeIndex < tt->firstIndex()) return 0;
|
||||
|
||||
@@ -300,6 +356,59 @@ uint64_t PdbCtx::importUDT(uint32_t typeIndex) {
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
uint64_t PdbCtx::importEnum(uint32_t typeIndex) {
|
||||
if (typeIndex < tt->firstIndex()) return 0;
|
||||
|
||||
auto it = typeCache.find(typeIndex);
|
||||
if (it != typeCache.end()) return it.value();
|
||||
|
||||
const auto* rec = tt->get(typeIndex);
|
||||
if (!rec || rec->header.kind != TRK::LF_ENUM) return 0;
|
||||
if (rec->data.LF_ENUM.property.fwdref) return 0;
|
||||
|
||||
QString qname = rec->data.LF_ENUM.name
|
||||
? QString::fromUtf8(rec->data.LF_ENUM.name)
|
||||
: QStringLiteral("<anon>");
|
||||
|
||||
Node s;
|
||||
s.kind = NodeKind::Struct;
|
||||
s.name = qname;
|
||||
s.structTypeName = qname;
|
||||
s.classKeyword = QStringLiteral("enum");
|
||||
s.parentId = 0;
|
||||
s.collapsed = true;
|
||||
|
||||
// Extract enum members from field list
|
||||
uint32_t fieldListIndex = rec->data.LF_ENUM.field;
|
||||
const auto* flRec = tt->get(fieldListIndex);
|
||||
if (flRec && flRec->header.kind == TRK::LF_FIELDLIST) {
|
||||
auto maxSize = flRec->header.size - sizeof(uint16_t);
|
||||
for (size_t i = 0; i < maxSize; ) {
|
||||
auto* field = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(
|
||||
reinterpret_cast<const uint8_t*>(&flRec->data.LF_FIELD.list) + i);
|
||||
if (field->kind != TRK::LF_ENUMERATE) break;
|
||||
|
||||
int64_t val = static_cast<int64_t>(leafValue(
|
||||
field->data.LF_ENUMERATE.value,
|
||||
field->data.LF_ENUMERATE.lfEasy.kind));
|
||||
const char* eName = leafName(
|
||||
field->data.LF_ENUMERATE.value,
|
||||
field->data.LF_ENUMERATE.lfEasy.kind);
|
||||
if (eName)
|
||||
s.enumMembers.append({QString::fromUtf8(eName), val});
|
||||
|
||||
i += static_cast<size_t>(eName - reinterpret_cast<const char*>(field));
|
||||
i += strnlen(eName, maxSize - i - 1) + 1;
|
||||
i = (i + 3) & ~size_t(3);
|
||||
}
|
||||
}
|
||||
|
||||
int idx = tree.addNode(s);
|
||||
uint64_t nodeId = tree.nodes[idx].id;
|
||||
typeCache[typeIndex] = nodeId;
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) {
|
||||
const auto* rec = tt->get(fieldListIndex);
|
||||
if (!rec || rec->header.kind != TRK::LF_FIELDLIST) return;
|
||||
@@ -522,7 +631,6 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
||||
isFwd = pointeeRec->data.LF_CLASS.property.fwdref;
|
||||
|
||||
if (isFwd) {
|
||||
// Need to find the non-fwdref definition by name
|
||||
const char* typeName = nullptr;
|
||||
if (pointeeRec->header.kind == TRK::LF_UNION)
|
||||
typeName = leafName(pointeeRec->data.LF_UNION.data, unionLeafKind(pointeeRec->data.LF_UNION.data));
|
||||
@@ -530,28 +638,8 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
||||
typeName = leafName(pointeeRec->data.LF_CLASS.data,
|
||||
pointeeRec->data.LF_CLASS.lfEasy.kind);
|
||||
|
||||
if (typeName) {
|
||||
// Linear scan for the definition (cached after first import)
|
||||
for (uint32_t ti = tt->firstIndex(); ti < tt->lastIndex(); ti++) {
|
||||
const auto* candidate = tt->get(ti);
|
||||
if (!candidate) continue;
|
||||
if (candidate->header.kind != pointeeRec->header.kind) continue;
|
||||
bool candidateFwd;
|
||||
const char* candidateName;
|
||||
if (candidate->header.kind == TRK::LF_UNION) {
|
||||
candidateFwd = candidate->data.LF_UNION.property.fwdref;
|
||||
candidateName = leafName(candidate->data.LF_UNION.data, unionLeafKind(candidate->data.LF_UNION.data));
|
||||
} else {
|
||||
candidateFwd = candidate->data.LF_CLASS.property.fwdref;
|
||||
candidateName = leafName(candidate->data.LF_CLASS.data,
|
||||
candidate->data.LF_CLASS.lfEasy.kind);
|
||||
}
|
||||
if (!candidateFwd && candidateName && strcmp(candidateName, typeName) == 0) {
|
||||
defIndex = ti;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t resolved = findUdtDefinitionIndex(pointeeRec->header.kind, typeName);
|
||||
if (resolved != 0) defIndex = resolved;
|
||||
}
|
||||
n.refId = importUDT(defIndex);
|
||||
} else if (pointeeRec->header.kind == TRK::LF_PROCEDURE ||
|
||||
@@ -584,27 +672,8 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
||||
else
|
||||
typeName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
|
||||
|
||||
if (typeName) {
|
||||
for (uint32_t ti = tt->firstIndex(); ti < tt->lastIndex(); ti++) {
|
||||
const auto* candidate = tt->get(ti);
|
||||
if (!candidate) continue;
|
||||
if (candidate->header.kind != rec->header.kind) continue;
|
||||
bool candidateFwd;
|
||||
const char* candidateName;
|
||||
if (candidate->header.kind == TRK::LF_UNION) {
|
||||
candidateFwd = candidate->data.LF_UNION.property.fwdref;
|
||||
candidateName = leafName(candidate->data.LF_UNION.data, unionLeafKind(candidate->data.LF_UNION.data));
|
||||
} else {
|
||||
candidateFwd = candidate->data.LF_CLASS.property.fwdref;
|
||||
candidateName = leafName(candidate->data.LF_CLASS.data,
|
||||
candidate->data.LF_CLASS.lfEasy.kind);
|
||||
}
|
||||
if (!candidateFwd && candidateName && strcmp(candidateName, typeName) == 0) {
|
||||
defIndex = ti;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t resolved = findUdtDefinitionIndex(rec->header.kind, typeName);
|
||||
if (resolved != 0) defIndex = resolved;
|
||||
}
|
||||
|
||||
uint64_t refId = importUDT(defIndex);
|
||||
@@ -707,8 +776,9 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
||||
}
|
||||
|
||||
case TRK::LF_ENUM: {
|
||||
// Map enum to its underlying integer type
|
||||
// Map enum to its underlying integer type, link to enum definition
|
||||
uint32_t utype = rec->data.LF_ENUM.utype;
|
||||
uint64_t enumNodeId = importEnum(typeIndex);
|
||||
Node n;
|
||||
if (utype < tt->firstIndex()) {
|
||||
n.kind = mapPrimitiveType(utype);
|
||||
@@ -718,6 +788,7 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam
|
||||
n.name = name;
|
||||
n.parentId = parentId;
|
||||
n.offset = offset;
|
||||
n.refId = enumNodeId;
|
||||
tree.addNode(n);
|
||||
break;
|
||||
}
|
||||
@@ -823,14 +894,27 @@ QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg
|
||||
bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE ||
|
||||
rec->header.kind == TRK::LF_CLASS ||
|
||||
rec->header.kind == TRK::LF_UNION);
|
||||
if (!isUDT) continue;
|
||||
bool isEnum = (rec->header.kind == TRK::LF_ENUM);
|
||||
if (!isUDT && !isEnum) continue;
|
||||
|
||||
const char* name = nullptr;
|
||||
uint16_t fieldCount = 0;
|
||||
bool isUnion = false;
|
||||
uint64_t size = 0;
|
||||
|
||||
if (rec->header.kind == TRK::LF_UNION) {
|
||||
if (isEnum) {
|
||||
if (rec->data.LF_ENUM.property.fwdref) continue;
|
||||
fieldCount = rec->data.LF_ENUM.count;
|
||||
name = rec->data.LF_ENUM.name;
|
||||
// Size from underlying type
|
||||
uint32_t ut = rec->data.LF_ENUM.utype;
|
||||
if (ut < tt.firstIndex()) {
|
||||
NodeKind ek = mapPrimitiveType(ut);
|
||||
size = sizeForKind(ek);
|
||||
} else {
|
||||
size = 4;
|
||||
}
|
||||
} else if (rec->header.kind == TRK::LF_UNION) {
|
||||
if (rec->data.LF_UNION.property.fwdref) continue;
|
||||
isUnion = true;
|
||||
fieldCount = rec->data.LF_UNION.count;
|
||||
@@ -856,6 +940,7 @@ QVector<PdbTypeInfo> enumeratePdbTypes(const QString& pdbPath, QString* errorMsg
|
||||
info.size = size;
|
||||
info.childCount = fieldCount;
|
||||
info.isUnion = isUnion;
|
||||
info.isEnum = isEnum;
|
||||
result.append(info);
|
||||
}
|
||||
|
||||
@@ -876,7 +961,12 @@ NodeTree importPdbSelected(const QString& pdbPath,
|
||||
|
||||
int total = typeIndices.size();
|
||||
for (int i = 0; i < total; i++) {
|
||||
ctx.importUDT(typeIndices[i]);
|
||||
uint32_t ti = typeIndices[i];
|
||||
const auto* rec = pdb.typeTable->get(ti);
|
||||
if (rec && rec->header.kind == TRK::LF_ENUM)
|
||||
ctx.importEnum(ti);
|
||||
else
|
||||
ctx.importUDT(ti);
|
||||
if (progressCb && !progressCb(i + 1, total)) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("Import cancelled");
|
||||
return ctx.tree; // return partial result
|
||||
|
||||
@@ -7,10 +7,11 @@ namespace rcx {
|
||||
|
||||
struct PdbTypeInfo {
|
||||
uint32_t typeIndex; // TPI type index
|
||||
QString name; // struct/class/union name
|
||||
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).
|
||||
|
||||
@@ -371,7 +371,6 @@ NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||
auto it = classIds.find(ref.className);
|
||||
if (it != classIds.end()) {
|
||||
tree.nodes[nodeIdx].refId = it.value();
|
||||
tree.invalidateIdCache();
|
||||
resolved++;
|
||||
} else {
|
||||
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "import_source.h"
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
#include <QRegularExpression>
|
||||
#include <QDebug>
|
||||
@@ -285,13 +286,16 @@ struct ParsedField {
|
||||
int commentOffset = -1; // from // 0xNN (-1 = none)
|
||||
int bitfieldWidth = -1; // -1 = not a bitfield
|
||||
QString pointerTarget; // for Type* -> the type name
|
||||
bool isUnion = false; // union container
|
||||
QVector<ParsedField> unionMembers; // children of union
|
||||
};
|
||||
|
||||
struct ParsedStruct {
|
||||
QString name;
|
||||
QString keyword; // "struct" or "class"
|
||||
QString keyword; // "struct", "class", or "enum"
|
||||
QVector<ParsedField> fields;
|
||||
int declaredSize = -1; // from static_assert
|
||||
QVector<QPair<QString, int64_t>> enumValues; // for keyword="enum"
|
||||
};
|
||||
|
||||
struct PendingRef {
|
||||
@@ -378,8 +382,7 @@ struct Parser {
|
||||
} else if (checkIdent("typedef")) {
|
||||
parseTypedef();
|
||||
} else if (checkIdent("enum")) {
|
||||
skipToSemiOrBrace();
|
||||
if (check(TokKind::RBrace)) { advance(); match(TokKind::Semi); }
|
||||
parseEnumDef();
|
||||
} else if (peek().kind == TokKind::Hash) {
|
||||
// preprocessor (shouldn't reach here if tokenizer skipped them)
|
||||
advance();
|
||||
@@ -464,12 +467,18 @@ struct Parser {
|
||||
// Might be "struct TypeName fieldName;" - fall through to field parsing
|
||||
}
|
||||
|
||||
// Union: pick first member only
|
||||
// Union: create container with all members
|
||||
if (checkIdent("union")) {
|
||||
parseUnion(ps);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Enum definition inside struct
|
||||
if (checkIdent("enum")) {
|
||||
parseEnumDef();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Static assert inside struct
|
||||
if (checkIdent("static_assert")) {
|
||||
parseStaticAssert();
|
||||
@@ -489,33 +498,76 @@ struct Parser {
|
||||
void parseUnion(ParsedStruct& ps) {
|
||||
advance(); // skip "union"
|
||||
|
||||
// Optional union name
|
||||
// Optional union tag name (before {)
|
||||
if (check(TokKind::Ident) && peek(1).kind == TokKind::LBrace) {
|
||||
advance(); // skip union name
|
||||
advance(); // skip union tag name
|
||||
}
|
||||
|
||||
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
|
||||
|
||||
// Parse first member of union
|
||||
bool gotFirst = false;
|
||||
// Parse ALL members of the union
|
||||
ParsedField unionField;
|
||||
unionField.isUnion = true;
|
||||
|
||||
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
|
||||
if (!gotFirst) {
|
||||
ParsedField field;
|
||||
if (parseField(field)) {
|
||||
ps.fields.append(field);
|
||||
gotFirst = true;
|
||||
} else {
|
||||
advance();
|
||||
// Handle nested unions inside this union
|
||||
if (checkIdent("union")) {
|
||||
// Recurse: create a sub-union ParsedStruct temporarily,
|
||||
// then steal its fields as a nested union member
|
||||
ParsedStruct tmp;
|
||||
parseUnion(tmp);
|
||||
for (auto& f : tmp.fields)
|
||||
unionField.unionMembers.append(f);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle anonymous struct inside union: struct { ... };
|
||||
if ((checkIdent("struct") || checkIdent("class")) && peek(1).kind == TokKind::LBrace) {
|
||||
advance(); // skip "struct"
|
||||
advance(); // skip "{"
|
||||
int depth = 1;
|
||||
while (peek().kind != TokKind::Eof && depth > 0) {
|
||||
if (peek().kind == TokKind::LBrace) depth++;
|
||||
else if (peek().kind == TokKind::RBrace) depth--;
|
||||
if (depth > 0) advance();
|
||||
}
|
||||
if (check(TokKind::RBrace)) advance();
|
||||
if (check(TokKind::Ident)) advance(); // optional field name
|
||||
match(TokKind::Semi);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle nested named struct definition inside union
|
||||
if ((checkIdent("struct") || checkIdent("class")) &&
|
||||
peek(1).kind == TokKind::Ident && peek(2).kind == TokKind::LBrace) {
|
||||
parseStructOrForward();
|
||||
continue;
|
||||
}
|
||||
|
||||
ParsedField field;
|
||||
if (parseField(field)) {
|
||||
unionField.unionMembers.append(field);
|
||||
} else {
|
||||
// Skip remaining union members
|
||||
skipToSemiOrBrace();
|
||||
advance();
|
||||
}
|
||||
}
|
||||
match(TokKind::RBrace);
|
||||
// Optional field name after union close
|
||||
if (check(TokKind::Ident)) advance();
|
||||
|
||||
// Optional field name after union close: union { ... } u3;
|
||||
if (check(TokKind::Ident)) {
|
||||
unionField.name = advance().text;
|
||||
}
|
||||
match(TokKind::Semi);
|
||||
|
||||
// Determine offset from first member with a known offset
|
||||
for (const auto& m : unionField.unionMembers) {
|
||||
if (m.commentOffset >= 0) {
|
||||
unionField.commentOffset = m.commentOffset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ps.fields.append(unionField);
|
||||
}
|
||||
|
||||
bool parseField(ParsedField& field) {
|
||||
@@ -719,6 +771,90 @@ struct Parser {
|
||||
}
|
||||
match(TokKind::Semi);
|
||||
}
|
||||
|
||||
void parseEnumDef() {
|
||||
advance(); // skip "enum"
|
||||
|
||||
// Optional "class" or "struct" (enum class)
|
||||
if (checkIdent("class") || checkIdent("struct"))
|
||||
advance();
|
||||
|
||||
// Optional name
|
||||
QString name;
|
||||
if (check(TokKind::Ident) && peek(1).kind != TokKind::Semi) {
|
||||
// Could be: enum Name { ... }; or enum Name : Type { ... };
|
||||
// But NOT: enum Name; (forward decl) or enum Name field; (field usage)
|
||||
if (peek(1).kind == TokKind::LBrace || peek(1).kind == TokKind::Colon) {
|
||||
name = advance().text;
|
||||
} else {
|
||||
// Not an enum definition — revert. This might be a field like "enum Foo bar;"
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional underlying type: enum Name : uint8_t { ... }
|
||||
if (check(TokKind::Colon)) {
|
||||
advance();
|
||||
parseTypeName(); // skip underlying type
|
||||
}
|
||||
|
||||
// Forward declaration: enum Name;
|
||||
if (check(TokKind::Semi)) {
|
||||
advance();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!match(TokKind::LBrace)) { skipToSemiOrBrace(); return; }
|
||||
|
||||
ParsedStruct ps;
|
||||
ps.name = name;
|
||||
ps.keyword = QStringLiteral("enum");
|
||||
|
||||
// Parse enum members: Name [= Value], ...
|
||||
int64_t nextValue = 0;
|
||||
while (peek().kind != TokKind::RBrace && peek().kind != TokKind::Eof) {
|
||||
if (!check(TokKind::Ident)) { advance(); continue; }
|
||||
QString memberName = advance().text;
|
||||
int64_t memberValue = nextValue;
|
||||
|
||||
if (check(TokKind::Equals)) {
|
||||
advance();
|
||||
// Parse value: could be number, negative number, or expression
|
||||
bool negative = false;
|
||||
if (peek().kind == TokKind::Other && peek().text == QStringLiteral("-")) {
|
||||
negative = true;
|
||||
advance();
|
||||
}
|
||||
if (check(TokKind::Number)) {
|
||||
bool ok;
|
||||
QString numText = peek().text;
|
||||
if (numText.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
||||
memberValue = numText.mid(2).toLongLong(&ok, 16);
|
||||
else
|
||||
memberValue = numText.toLongLong(&ok);
|
||||
if (negative) memberValue = -memberValue;
|
||||
advance();
|
||||
} else {
|
||||
// Complex expression — skip to comma or brace
|
||||
while (peek().kind != TokKind::Comma &&
|
||||
peek().kind != TokKind::RBrace &&
|
||||
peek().kind != TokKind::Eof)
|
||||
advance();
|
||||
}
|
||||
}
|
||||
|
||||
ps.enumValues.append({memberName, memberValue});
|
||||
nextValue = memberValue + 1;
|
||||
|
||||
// Skip comma between members
|
||||
match(TokKind::Comma);
|
||||
}
|
||||
match(TokKind::RBrace);
|
||||
match(TokKind::Semi);
|
||||
|
||||
if (!ps.name.isEmpty())
|
||||
structs.append(ps);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Padding field detection ──
|
||||
@@ -758,6 +894,305 @@ static void emitHexPadding(NodeTree& tree, uint64_t parentId, int offset, int si
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bitfield grouping: emit a single hex node covering consecutive bitfields ──
|
||||
|
||||
static void emitBitfieldGroup(NodeTree& tree, uint64_t parentId, int offset, int totalBits) {
|
||||
int bytes = (totalBits + 7) / 8;
|
||||
// Round up to nearest power-of-2 hex node
|
||||
NodeKind hexKind;
|
||||
if (bytes <= 1) hexKind = NodeKind::Hex8;
|
||||
else if (bytes <= 2) hexKind = NodeKind::Hex16;
|
||||
else if (bytes <= 4) hexKind = NodeKind::Hex32;
|
||||
else hexKind = NodeKind::Hex64;
|
||||
Node n;
|
||||
n.kind = hexKind;
|
||||
n.parentId = parentId;
|
||||
n.offset = offset;
|
||||
tree.addNode(n);
|
||||
}
|
||||
|
||||
// ── NodeTree builder: recursive field emitter ──
|
||||
|
||||
struct BuildContext {
|
||||
NodeTree& tree;
|
||||
const QHash<QString, TypeInfo>& typeTable;
|
||||
QHash<QString, uint64_t>& classIds;
|
||||
QVector<PendingRef>& pendingRefs;
|
||||
bool useCommentOffsets;
|
||||
QSet<QString> enumNames; // enum type names (emit as UInt32 + refId)
|
||||
};
|
||||
|
||||
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
const QVector<ParsedField>& fields) {
|
||||
int computedOffset = 0;
|
||||
|
||||
for (int fi = 0; fi < fields.size(); fi++) {
|
||||
const auto& field = fields[fi];
|
||||
|
||||
// Bitfield group: consume consecutive bitfields, emit single hex node
|
||||
if (field.bitfieldWidth >= 0) {
|
||||
int groupOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
groupOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
groupOffset = computedOffset;
|
||||
int totalBits = 0;
|
||||
while (fi < fields.size() && fields[fi].bitfieldWidth >= 0) {
|
||||
totalBits += fields[fi].bitfieldWidth;
|
||||
fi++;
|
||||
}
|
||||
fi--; // compensate for outer loop increment
|
||||
if (totalBits > 0)
|
||||
emitBitfieldGroup(ctx.tree, parentId, groupOffset, totalBits);
|
||||
int bytes = (totalBits + 7) / 8;
|
||||
int nodeSize = (bytes <= 1) ? 1 : (bytes <= 2) ? 2 : (bytes <= 4) ? 4 : 8;
|
||||
computedOffset = groupOffset + nodeSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Union container field
|
||||
if (field.isUnion) {
|
||||
int unionOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
unionOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
unionOffset = computedOffset;
|
||||
|
||||
Node unionNode;
|
||||
unionNode.kind = NodeKind::Struct;
|
||||
unionNode.classKeyword = QStringLiteral("union");
|
||||
unionNode.name = field.name;
|
||||
unionNode.parentId = parentId;
|
||||
unionNode.offset = unionOffset;
|
||||
unionNode.collapsed = true;
|
||||
|
||||
int unionIdx = ctx.tree.addNode(unionNode);
|
||||
uint64_t unionId = ctx.tree.nodes[unionIdx].id;
|
||||
|
||||
// Build each union member independently so each starts at offset 0
|
||||
int absUnionOffset = baseOffset + unionOffset;
|
||||
for (const auto& member : field.unionMembers) {
|
||||
QVector<ParsedField> single;
|
||||
single.append(member);
|
||||
buildFields(ctx, unionId, absUnionOffset, single);
|
||||
}
|
||||
|
||||
// Advance computed offset past the union (max member size)
|
||||
int unionSpan = ctx.tree.structSpan(unionId);
|
||||
computedOffset = unionOffset + (unionSpan > 0 ? unionSpan : 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
int fieldOffset;
|
||||
if (ctx.useCommentOffsets && field.commentOffset >= 0)
|
||||
fieldOffset = field.commentOffset - baseOffset;
|
||||
else
|
||||
fieldOffset = computedOffset;
|
||||
|
||||
// Resolve type
|
||||
auto typeIt = ctx.typeTable.find(field.typeName);
|
||||
bool knownType = typeIt != ctx.typeTable.end();
|
||||
|
||||
// Pointer field
|
||||
if (field.isPointer) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Pointer64;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
|
||||
if (!field.pointerTarget.isEmpty() &&
|
||||
field.pointerTarget != QStringLiteral("void")) {
|
||||
ctx.pendingRefs.append({nodeId, field.pointerTarget});
|
||||
}
|
||||
|
||||
computedOffset = fieldOffset + 8;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Enum-typed field: emit as UInt32 with refId to enum definition
|
||||
if (!knownType && ctx.enumNames.contains(field.typeName)) {
|
||||
int elemSize = 4;
|
||||
NodeKind elemKind = NodeKind::UInt32;
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = elemKind;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + totalElements * elemSize;
|
||||
} else {
|
||||
Node n;
|
||||
n.kind = elemKind;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
computedOffset = fieldOffset + elemSize;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine base type info
|
||||
NodeKind baseKind = NodeKind::Hex8;
|
||||
int baseSize = 1;
|
||||
bool isStructType = false;
|
||||
|
||||
if (knownType) {
|
||||
baseKind = typeIt->kind;
|
||||
baseSize = typeIt->size;
|
||||
} else {
|
||||
isStructType = true;
|
||||
}
|
||||
|
||||
// Padding fields
|
||||
if (isPaddingName(field.name) && !field.arraySizes.isEmpty()) {
|
||||
int totalSize = baseSize;
|
||||
for (int dim : field.arraySizes) totalSize *= (dim > 0 ? dim : 1);
|
||||
emitHexPadding(ctx.tree, parentId, fieldOffset, totalSize);
|
||||
computedOffset = fieldOffset + totalSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Array fields
|
||||
if (!field.arraySizes.isEmpty() && !isStructType) {
|
||||
int firstDim = field.arraySizes.value(0, 1);
|
||||
if (firstDim <= 0) firstDim = 1;
|
||||
|
||||
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
|
||||
field.typeName == QStringLiteral("char")) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF8;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.strLen = firstDim;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + firstDim;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
|
||||
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR"))) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF16;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.strLen = firstDim;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + firstDim * 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (baseKind == NodeKind::Float && field.arraySizes.size() == 1) {
|
||||
if (firstDim == 2) {
|
||||
Node n; n.kind = NodeKind::Vec2; n.name = field.name;
|
||||
n.parentId = parentId; n.offset = fieldOffset;
|
||||
ctx.tree.addNode(n); computedOffset = fieldOffset + 8; continue;
|
||||
}
|
||||
if (firstDim == 3) {
|
||||
Node n; n.kind = NodeKind::Vec3; n.name = field.name;
|
||||
n.parentId = parentId; n.offset = fieldOffset;
|
||||
ctx.tree.addNode(n); computedOffset = fieldOffset + 12; continue;
|
||||
}
|
||||
if (firstDim == 4) {
|
||||
Node n; n.kind = NodeKind::Vec4; n.name = field.name;
|
||||
n.parentId = parentId; n.offset = fieldOffset;
|
||||
ctx.tree.addNode(n); computedOffset = fieldOffset + 16; continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (baseKind == NodeKind::Float && field.arraySizes.size() == 2 &&
|
||||
field.arraySizes[0] == 4 && field.arraySizes[1] == 4) {
|
||||
Node n; n.kind = NodeKind::Mat4x4; n.name = field.name;
|
||||
n.parentId = parentId; n.offset = fieldOffset;
|
||||
ctx.tree.addNode(n); computedOffset = fieldOffset + 64; continue;
|
||||
}
|
||||
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = baseKind;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + totalElements * baseSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Struct-type field
|
||||
if (isStructType) {
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = NodeKind::Struct;
|
||||
n.structTypeName = field.typeName;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
continue;
|
||||
}
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
n.structTypeName = field.typeName;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = ctx.tree.addNode(n);
|
||||
uint64_t nodeId = ctx.tree.nodes[nodeIdx].id;
|
||||
ctx.pendingRefs.append({nodeId, field.typeName});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Simple primitive field
|
||||
Node n;
|
||||
n.kind = baseKind;
|
||||
n.name = field.name;
|
||||
n.parentId = parentId;
|
||||
n.offset = fieldOffset;
|
||||
ctx.tree.addNode(n);
|
||||
computedOffset = fieldOffset + baseSize;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Check if any field (or union member) has a comment offset ──
|
||||
|
||||
static bool hasAnyCommentOffset(const QVector<ParsedField>& fields) {
|
||||
for (const auto& f : fields) {
|
||||
if (f.commentOffset >= 0) return true;
|
||||
if (f.isUnion && hasAnyCommentOffset(f.unionMembers)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── NodeTree builder ──
|
||||
|
||||
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
@@ -775,7 +1210,7 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
parser.parse();
|
||||
|
||||
if (parser.structs.isEmpty()) {
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No struct definitions found");
|
||||
if (errorMsg) *errorMsg = QStringLiteral("No struct or enum definitions found");
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -798,13 +1233,19 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
// Determine offset mode: if ANY field in ANY struct has a comment offset, use comment mode
|
||||
bool useCommentOffsets = false;
|
||||
for (const auto& ps : parser.structs) {
|
||||
for (const auto& f : ps.fields) {
|
||||
if (f.commentOffset >= 0) { useCommentOffsets = true; break; }
|
||||
}
|
||||
if (useCommentOffsets) break;
|
||||
if (hasAnyCommentOffset(ps.fields)) { useCommentOffsets = true; break; }
|
||||
}
|
||||
|
||||
// Build nodes for each struct
|
||||
// Collect enum type names for field-type detection
|
||||
QSet<QString> enumNames;
|
||||
for (const auto& ps : parser.structs) {
|
||||
if (ps.keyword == QStringLiteral("enum") && !ps.name.isEmpty())
|
||||
enumNames.insert(ps.name);
|
||||
}
|
||||
|
||||
BuildContext ctx{tree, typeTable, classIds, pendingRefs, useCommentOffsets, enumNames};
|
||||
|
||||
// Build nodes for each struct/enum
|
||||
for (const auto& ps : parser.structs) {
|
||||
Node structNode;
|
||||
structNode.kind = NodeKind::Struct;
|
||||
@@ -815,222 +1256,21 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
structNode.offset = 0;
|
||||
structNode.collapsed = true;
|
||||
|
||||
// Enum: store members directly on the node, no child fields
|
||||
if (ps.keyword == QStringLiteral("enum")) {
|
||||
structNode.enumMembers = ps.enumValues;
|
||||
int idx = tree.addNode(structNode);
|
||||
uint64_t nodeId = tree.nodes[idx].id;
|
||||
if (!ps.name.isEmpty())
|
||||
classIds[ps.name] = nodeId;
|
||||
continue;
|
||||
}
|
||||
|
||||
int structIdx = tree.addNode(structNode);
|
||||
uint64_t structId = tree.nodes[structIdx].id;
|
||||
classIds[ps.name] = structId;
|
||||
|
||||
int computedOffset = 0;
|
||||
|
||||
for (const auto& field : ps.fields) {
|
||||
// Skip bitfields
|
||||
if (field.bitfieldWidth >= 0) continue;
|
||||
|
||||
int fieldOffset;
|
||||
if (useCommentOffsets && field.commentOffset >= 0)
|
||||
fieldOffset = field.commentOffset;
|
||||
else
|
||||
fieldOffset = computedOffset;
|
||||
|
||||
// Resolve type
|
||||
auto typeIt = typeTable.find(field.typeName);
|
||||
bool knownType = typeIt != typeTable.end();
|
||||
|
||||
// Pointer field
|
||||
if (field.isPointer) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Pointer64;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
|
||||
// If target is not void and not a primitive, defer resolution
|
||||
if (!field.pointerTarget.isEmpty() &&
|
||||
field.pointerTarget != QStringLiteral("void")) {
|
||||
pendingRefs.append({nodeId, field.pointerTarget});
|
||||
}
|
||||
|
||||
computedOffset = fieldOffset + 8; // pointer size
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine base type info
|
||||
NodeKind baseKind = NodeKind::Hex8;
|
||||
int baseSize = 1;
|
||||
bool isStructType = false;
|
||||
|
||||
if (knownType) {
|
||||
baseKind = typeIt->kind;
|
||||
baseSize = typeIt->size;
|
||||
} else {
|
||||
// Unknown type = assume struct reference
|
||||
isStructType = true;
|
||||
}
|
||||
|
||||
// Padding fields: name-based detection
|
||||
if (isPaddingName(field.name) && !field.arraySizes.isEmpty()) {
|
||||
int totalSize = baseSize;
|
||||
for (int dim : field.arraySizes) totalSize *= (dim > 0 ? dim : 1);
|
||||
emitHexPadding(tree, structId, fieldOffset, totalSize);
|
||||
computedOffset = fieldOffset + totalSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Array fields
|
||||
if (!field.arraySizes.isEmpty() && !isStructType) {
|
||||
int firstDim = field.arraySizes.value(0, 1);
|
||||
if (firstDim <= 0) firstDim = 1;
|
||||
|
||||
// Special: char[N] -> UTF8
|
||||
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
|
||||
field.typeName == QStringLiteral("char")) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF8;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.strLen = firstDim;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + firstDim;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special: wchar_t[N] -> UTF16
|
||||
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
|
||||
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR"))) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF16;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.strLen = firstDim;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + firstDim * 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special: float[2] -> Vec2, float[3] -> Vec3, float[4] -> Vec4
|
||||
if (baseKind == NodeKind::Float && field.arraySizes.size() == 1) {
|
||||
if (firstDim == 2) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Vec2;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + 8;
|
||||
continue;
|
||||
}
|
||||
if (firstDim == 3) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Vec3;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + 12;
|
||||
continue;
|
||||
}
|
||||
if (firstDim == 4) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Vec4;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + 16;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Special: float[4][4] -> Mat4x4
|
||||
if (baseKind == NodeKind::Float && field.arraySizes.size() == 2 &&
|
||||
field.arraySizes[0] == 4 && field.arraySizes[1] == 4) {
|
||||
Node n;
|
||||
n.kind = NodeKind::Mat4x4;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + 64;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generic array
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = baseKind;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + totalElements * baseSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Struct-type field (embedded struct or array of structs)
|
||||
if (isStructType) {
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
// Array of structs
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.arrayLen = totalElements;
|
||||
n.elementKind = NodeKind::Struct;
|
||||
n.structTypeName = field.typeName;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
pendingRefs.append({nodeId, field.typeName});
|
||||
|
||||
// For computed offsets: we don't know struct size yet, use 0
|
||||
// The offset will be approximate for unknown struct sizes
|
||||
if (!useCommentOffsets) {
|
||||
// Try to estimate from same-file structs
|
||||
// Can't know size yet since we may not have parsed it
|
||||
// Just advance by 0 (will be corrected by comment offsets if present)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Embedded struct
|
||||
Node n;
|
||||
n.kind = NodeKind::Struct;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
n.structTypeName = field.typeName;
|
||||
n.collapsed = true;
|
||||
|
||||
int nodeIdx = tree.addNode(n);
|
||||
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||
pendingRefs.append({nodeId, field.typeName});
|
||||
// Don't advance computed offset for unknown struct size
|
||||
continue;
|
||||
}
|
||||
|
||||
// Simple primitive field
|
||||
Node n;
|
||||
n.kind = baseKind;
|
||||
n.name = field.name;
|
||||
n.parentId = structId;
|
||||
n.offset = fieldOffset;
|
||||
tree.addNode(n);
|
||||
computedOffset = fieldOffset + baseSize;
|
||||
}
|
||||
buildFields(ctx, structId, 0, ps.fields);
|
||||
|
||||
// Apply static_assert size: add tail padding if needed
|
||||
auto sizeIt = parser.sizeAsserts.find(ps.name);
|
||||
@@ -1056,7 +1296,6 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg) {
|
||||
auto it = classIds.find(ref.className);
|
||||
if (it != classIds.end()) {
|
||||
tree.nodes[nodeIdx].refId = it.value();
|
||||
tree.invalidateIdCache();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
303
src/main.cpp
303
src/main.cpp
@@ -251,6 +251,9 @@ public:
|
||||
// Kill the 1px frame margin Fusion reserves around QMenu contents
|
||||
if (metric == PM_MenuPanelWidth)
|
||||
return 0;
|
||||
// Kill the separator between dock widgets / central widget
|
||||
if (metric == PM_DockWidgetSeparatorExtent)
|
||||
return 0;
|
||||
return QProxyStyle::pixelMetric(metric, opt, w);
|
||||
}
|
||||
void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt,
|
||||
@@ -261,21 +264,37 @@ public:
|
||||
// Kill the status bar item frame and panel border
|
||||
if (elem == PE_FrameStatusBarItem || elem == PE_PanelStatusBar)
|
||||
return;
|
||||
// Transparent menu bar background (no CSS needed)
|
||||
if (elem == PE_PanelMenuBar)
|
||||
return;
|
||||
QProxyStyle::drawPrimitive(elem, opt, p, w);
|
||||
}
|
||||
void drawControl(ControlElement element, const QStyleOption* opt,
|
||||
QPainter* p, const QWidget* w) const override {
|
||||
// Menu bar items (File, Edit, View…) — direct paint, Fusion ignores palette
|
||||
// Suppress Fusion's CE_MenuBarEmptyArea — it fills with palette.window()
|
||||
// bypassing PE_PanelMenuBar. TitleBarWidget paints the background.
|
||||
if (element == CE_MenuBarEmptyArea)
|
||||
return;
|
||||
// Menu bar items — fully owned painting (Fusion fills full rect, hiding border)
|
||||
if (element == CE_MenuBarItem) {
|
||||
if (auto* mi = qstyleoption_cast<const QStyleOptionMenuItem*>(opt)) {
|
||||
if (mi->state & (State_Selected | State_Sunken)) {
|
||||
QStyleOptionMenuItem patched = *mi;
|
||||
patched.state &= ~(State_Selected | State_Sunken);
|
||||
patched.palette.setColor(QPalette::ButtonText,
|
||||
mi->palette.color(QPalette::Link)); // amber text only
|
||||
QProxyStyle::drawControl(element, &patched, p, w);
|
||||
return;
|
||||
}
|
||||
QRect area = mi->rect.adjusted(0, 0, 0, -1); // leave 1px for border
|
||||
bool selected = mi->state & State_Selected;
|
||||
bool sunken = mi->state & State_Sunken;
|
||||
|
||||
// Only fill background for hover/pressed — non-hovered stays
|
||||
// transparent so the parent's border line shows through.
|
||||
if (sunken)
|
||||
p->fillRect(area, mi->palette.color(QPalette::Mid).darker(130));
|
||||
else if (selected)
|
||||
p->fillRect(area, mi->palette.color(QPalette::Mid));
|
||||
|
||||
QColor fg = (selected || sunken)
|
||||
? mi->palette.color(QPalette::Link)
|
||||
: mi->palette.color(QPalette::ButtonText);
|
||||
p->setPen(fg);
|
||||
p->drawText(area, Qt::AlignCenter | Qt::TextShowMnemonic, mi->text);
|
||||
return; // never delegate to Fusion
|
||||
}
|
||||
}
|
||||
// Popup menu items — palette patch then delegate to Fusion
|
||||
@@ -285,7 +304,7 @@ public:
|
||||
&& mi->menuItemType != QStyleOptionMenuItem::Separator) {
|
||||
QStyleOptionMenuItem patched = *mi;
|
||||
patched.palette.setColor(QPalette::Highlight,
|
||||
mi->palette.color(QPalette::Mid)); // theme.border
|
||||
mi->palette.color(QPalette::Mid)); // theme.hover
|
||||
patched.palette.setColor(QPalette::HighlightedText,
|
||||
mi->palette.color(QPalette::Link)); // theme.indHoverSpan
|
||||
QProxyStyle::drawControl(element, &patched, p, w);
|
||||
@@ -293,6 +312,18 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tree view items — use theme.hover for selection instead of blue
|
||||
if (element == CE_ItemViewItem) {
|
||||
if (auto* vi = qstyleoption_cast<const QStyleOptionViewItem*>(opt)) {
|
||||
QStyleOptionViewItem patched = *vi;
|
||||
patched.palette.setColor(QPalette::Highlight,
|
||||
vi->palette.color(QPalette::Mid)); // theme.hover
|
||||
patched.palette.setColor(QPalette::HighlightedText,
|
||||
vi->palette.color(QPalette::Text));
|
||||
QProxyStyle::drawControl(element, &patched, p, w);
|
||||
return;
|
||||
}
|
||||
}
|
||||
QProxyStyle::drawControl(element, opt, p, w);
|
||||
}
|
||||
};
|
||||
@@ -408,7 +439,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
// Restore menu bar title case setting (after menus are created)
|
||||
{
|
||||
QSettings s("Reclass", "Reclass");
|
||||
m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", true).toBool());
|
||||
m_titleBar->setMenuBarTitleCase(s.value("menuBarTitleCase", false).toBool());
|
||||
if (s.value("showIcon", false).toBool())
|
||||
m_titleBar->setShowIcon(true);
|
||||
}
|
||||
@@ -549,6 +580,16 @@ void MainWindow::createMenus() {
|
||||
themeMenu->addSeparator();
|
||||
Qt5Qt6AddAction(themeMenu, "Edit Theme...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::editTheme);
|
||||
|
||||
view->addSeparator();
|
||||
auto* actCompact = view->addAction("Compact &Columns");
|
||||
actCompact->setCheckable(true);
|
||||
actCompact->setChecked(settings.value("compactColumns", true).toBool());
|
||||
connect(actCompact, &QAction::triggered, this, [this](bool checked) {
|
||||
QSettings("Reclass", "Reclass").setValue("compactColumns", checked);
|
||||
for (auto& tab : m_tabs)
|
||||
tab.ctrl->setCompactColumns(checked);
|
||||
});
|
||||
|
||||
view->addSeparator();
|
||||
view->addAction(m_workspaceDock->toggleViewAction());
|
||||
|
||||
@@ -969,6 +1010,9 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
// Create the initial split pane
|
||||
tab.panes.append(createSplitPane(tab));
|
||||
|
||||
// Apply global compact columns setting to new tab
|
||||
ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool());
|
||||
|
||||
// Give every controller the shared document list for cross-tab type visibility
|
||||
ctrl->setProjectDocuments(&m_allDocs);
|
||||
rebuildAllDocs();
|
||||
@@ -1082,6 +1126,76 @@ static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QStri
|
||||
n.offset = i * 8;
|
||||
tree.addNode(n);
|
||||
}
|
||||
|
||||
// Default project: add an example enum and a class with a union
|
||||
if (classKeyword.isEmpty()) {
|
||||
// ── Example enum: _POOL_TYPE ──
|
||||
{
|
||||
Node e;
|
||||
e.kind = NodeKind::Struct;
|
||||
e.name = QStringLiteral("_POOL_TYPE");
|
||||
e.structTypeName = QStringLiteral("_POOL_TYPE");
|
||||
e.classKeyword = QStringLiteral("enum");
|
||||
e.parentId = 0;
|
||||
e.collapsed = false;
|
||||
e.enumMembers = {
|
||||
{QStringLiteral("NonPagedPool"), 0},
|
||||
{QStringLiteral("PagedPool"), 1},
|
||||
{QStringLiteral("NonPagedPoolMustSucceed"), 2},
|
||||
{QStringLiteral("DontUseThisType"), 3},
|
||||
{QStringLiteral("NonPagedPoolCacheAligned"), 4},
|
||||
{QStringLiteral("PagedPoolCacheAligned"), 5},
|
||||
};
|
||||
tree.addNode(e);
|
||||
}
|
||||
|
||||
// ── Example class with a union: _SAMPLE_OBJECT ──
|
||||
{
|
||||
Node cls;
|
||||
cls.kind = NodeKind::Struct;
|
||||
cls.name = QStringLiteral("sample");
|
||||
cls.structTypeName = QStringLiteral("_SAMPLE_OBJECT");
|
||||
cls.classKeyword = QStringLiteral("class");
|
||||
cls.parentId = 0;
|
||||
cls.offset = 0;
|
||||
int ci = tree.addNode(cls);
|
||||
uint64_t clsId = tree.nodes[ci].id;
|
||||
|
||||
// Field: uint32_t Type at offset 0
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Type");
|
||||
n.parentId = clsId; n.offset = 0; tree.addNode(n); }
|
||||
// Field: uint32_t Size at offset 4
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Size");
|
||||
n.parentId = clsId; n.offset = 4; tree.addNode(n); }
|
||||
|
||||
// Union at offset 8
|
||||
{
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.name = QStringLiteral("Data");
|
||||
u.structTypeName = QStringLiteral("Data");
|
||||
u.classKeyword = QStringLiteral("union");
|
||||
u.parentId = clsId;
|
||||
u.offset = 8;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
// Union member: uint64_t AsLong
|
||||
{ Node n; n.kind = NodeKind::UInt64; n.name = QStringLiteral("AsLong");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
// Union member: void* AsPointer
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("AsPointer");
|
||||
n.parentId = uId; n.offset = 0; n.collapsed = true; tree.addNode(n); }
|
||||
// Union member: float[2] AsFloat2
|
||||
{ Node n; n.kind = NodeKind::Vec2; n.name = QStringLiteral("AsFloat2");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
}
|
||||
|
||||
// Field: void* Next at offset 16
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("Next");
|
||||
n.parentId = clsId; n.offset = 16; n.collapsed = true; tree.addNode(n); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::newClass() {
|
||||
@@ -1166,6 +1280,73 @@ static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) {
|
||||
n.offset = off;
|
||||
tree.addNode(n);
|
||||
}
|
||||
|
||||
// ── Example enum: _POOL_TYPE ──
|
||||
{
|
||||
Node e;
|
||||
e.kind = NodeKind::Struct;
|
||||
e.name = QStringLiteral("_POOL_TYPE");
|
||||
e.structTypeName = QStringLiteral("_POOL_TYPE");
|
||||
e.classKeyword = QStringLiteral("enum");
|
||||
e.parentId = 0;
|
||||
e.collapsed = false;
|
||||
e.enumMembers = {
|
||||
{QStringLiteral("NonPagedPool"), 0},
|
||||
{QStringLiteral("PagedPool"), 1},
|
||||
{QStringLiteral("NonPagedPoolMustSucceed"), 2},
|
||||
{QStringLiteral("DontUseThisType"), 3},
|
||||
{QStringLiteral("NonPagedPoolCacheAligned"), 4},
|
||||
{QStringLiteral("PagedPoolCacheAligned"), 5},
|
||||
};
|
||||
tree.addNode(e);
|
||||
}
|
||||
|
||||
// ── Example class with a union: _SAMPLE_OBJECT ──
|
||||
{
|
||||
Node cls;
|
||||
cls.kind = NodeKind::Struct;
|
||||
cls.name = QStringLiteral("sample");
|
||||
cls.structTypeName = QStringLiteral("_SAMPLE_OBJECT");
|
||||
cls.classKeyword = QStringLiteral("class");
|
||||
cls.parentId = 0;
|
||||
cls.offset = 0;
|
||||
int ci = tree.addNode(cls);
|
||||
uint64_t clsId = tree.nodes[ci].id;
|
||||
|
||||
// Field: uint32_t Type at offset 0
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Type");
|
||||
n.parentId = clsId; n.offset = 0; tree.addNode(n); }
|
||||
// Field: uint32_t Size at offset 4
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Size");
|
||||
n.parentId = clsId; n.offset = 4; tree.addNode(n); }
|
||||
|
||||
// Union at offset 8
|
||||
{
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.name = QStringLiteral("Data");
|
||||
u.structTypeName = QStringLiteral("Data");
|
||||
u.classKeyword = QStringLiteral("union");
|
||||
u.parentId = clsId;
|
||||
u.offset = 8;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
// Union member: uint64_t AsLong
|
||||
{ Node n; n.kind = NodeKind::UInt64; n.name = QStringLiteral("AsLong");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
// Union member: void* AsPointer
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("AsPointer");
|
||||
n.parentId = uId; n.offset = 0; n.collapsed = true; tree.addNode(n); }
|
||||
// Union member: float[2] AsFloat2
|
||||
{ Node n; n.kind = NodeKind::Vec2; n.name = QStringLiteral("AsFloat2");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
}
|
||||
|
||||
// Field: void* Next at offset 16
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("Next");
|
||||
n.parentId = clsId; n.offset = 16; n.collapsed = true; tree.addNode(n); }
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
@@ -1352,8 +1533,7 @@ void MainWindow::toggleMcp() {
|
||||
void MainWindow::applyTheme(const Theme& theme) {
|
||||
applyGlobalTheme(theme);
|
||||
|
||||
// Kill the 1px separator line between central widget and status bar
|
||||
setStyleSheet("QMainWindow::separator { height: 0px; width: 0px; }");
|
||||
// Separator killed via PM_DockWidgetSeparatorExtent in MenuBarStyle
|
||||
|
||||
// Custom title bar
|
||||
m_titleBar->applyTheme(theme);
|
||||
@@ -1406,16 +1586,26 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
if (auto* w = findChild<QWidget*>("resizeGrip"))
|
||||
static_cast<ResizeGrip*>(w)->setGripColor(theme.textFaint);
|
||||
|
||||
// Workspace tree: text color matches menu bar
|
||||
// Workspace tree: colors from theme (selection + text)
|
||||
if (m_workspaceTree) {
|
||||
QPalette tp = m_workspaceTree->palette();
|
||||
tp.setColor(QPalette::Text, theme.textDim);
|
||||
tp.setColor(QPalette::Highlight, theme.hover);
|
||||
tp.setColor(QPalette::HighlightedText, theme.text);
|
||||
m_workspaceTree->setPalette(tp);
|
||||
}
|
||||
|
||||
// Dock titlebar: restyle label + close button
|
||||
if (m_dockTitleLabel)
|
||||
m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name()));
|
||||
// Dock titlebar: restyle via palette + close button
|
||||
if (m_dockTitleLabel) {
|
||||
QPalette lp = m_dockTitleLabel->palette();
|
||||
lp.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_dockTitleLabel->setPalette(lp);
|
||||
}
|
||||
if (auto* titleBar = m_workspaceDock ? m_workspaceDock->titleBarWidget() : nullptr) {
|
||||
QPalette tbPal = titleBar->palette();
|
||||
tbPal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
titleBar->setPalette(tbPal);
|
||||
}
|
||||
if (m_dockCloseBtn)
|
||||
m_dockCloseBtn->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
||||
@@ -1855,6 +2045,7 @@ void MainWindow::importFromSource() {
|
||||
m_mdiArea->closeAllSubWindows();
|
||||
createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_workspaceDock->show();
|
||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from source").arg(classCount));
|
||||
}
|
||||
|
||||
@@ -1904,6 +2095,7 @@ void MainWindow::importPdb() {
|
||||
m_mdiArea->closeAllSubWindows();
|
||||
createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_workspaceDock->show();
|
||||
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
|
||||
.arg(classCount).arg(QFileInfo(pdbPath).fileName()));
|
||||
}
|
||||
@@ -1976,7 +2168,22 @@ QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
|
||||
|
||||
buildEmptyStruct(doc->tree, classKeyword);
|
||||
|
||||
// Inherit source from current tab (if any)
|
||||
auto* currentCtrl = activeController();
|
||||
if (currentCtrl && currentCtrl->document()->provider
|
||||
&& currentCtrl->document()->provider->isValid()) {
|
||||
doc->provider = currentCtrl->document()->provider;
|
||||
}
|
||||
|
||||
auto* sub = createTab(doc);
|
||||
|
||||
// Copy saved sources to new tab's controller
|
||||
if (currentCtrl && !currentCtrl->savedSources().isEmpty()) {
|
||||
auto& newTab = m_tabs[sub];
|
||||
newTab.ctrl->copySavedSources(currentCtrl->savedSources(),
|
||||
currentCtrl->activeSourceIndex());
|
||||
}
|
||||
|
||||
rebuildWorkspaceModel();
|
||||
return sub;
|
||||
}
|
||||
@@ -2018,6 +2225,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
||||
m_mdiArea->closeAllSubWindows();
|
||||
auto* sub = createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_workspaceDock->show();
|
||||
int classCount = 0;
|
||||
for (const auto& n : doc->tree.nodes)
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||
@@ -2038,6 +2246,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
||||
|
||||
auto* sub = createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_workspaceDock->show();
|
||||
return sub;
|
||||
}
|
||||
|
||||
@@ -2068,7 +2277,7 @@ void MainWindow::project_close(QMdiSubWindow* sub) {
|
||||
// ── Workspace Dock ──
|
||||
|
||||
void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceDock = new QDockWidget("Project Tree", this);
|
||||
m_workspaceDock = new QDockWidget("Project", this);
|
||||
m_workspaceDock->setObjectName("WorkspaceDock");
|
||||
m_workspaceDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
|
||||
m_workspaceDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
@@ -2078,17 +2287,21 @@ void MainWindow::createWorkspaceDock() {
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
|
||||
auto* titleBar = new QWidget(m_workspaceDock);
|
||||
titleBar->setAutoFillBackground(true);
|
||||
{
|
||||
QPalette tbPal = titleBar->palette();
|
||||
tbPal.setColor(QPalette::Window, t.backgroundAlt);
|
||||
titleBar->setPalette(tbPal);
|
||||
}
|
||||
auto* layout = new QHBoxLayout(titleBar);
|
||||
layout->setContentsMargins(6, 2, 2, 2);
|
||||
layout->setSpacing(0);
|
||||
|
||||
m_dockTitleLabel = new QLabel("Project Tree", titleBar);
|
||||
m_dockTitleLabel->setStyleSheet(QStringLiteral("color: %1;").arg(t.textDim.name()));
|
||||
m_dockTitleLabel = new QLabel("Project", titleBar);
|
||||
{
|
||||
QString fontName = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
|
||||
QFont f(fontName, 12);
|
||||
f.setFixedPitch(true);
|
||||
m_dockTitleLabel->setFont(f);
|
||||
QPalette lp = m_dockTitleLabel->palette();
|
||||
lp.setColor(QPalette::WindowText, t.textDim);
|
||||
m_dockTitleLabel->setPalette(lp);
|
||||
}
|
||||
layout->addWidget(m_dockTitleLabel);
|
||||
|
||||
@@ -2117,6 +2330,16 @@ void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceTree->setExpandsOnDoubleClick(false);
|
||||
m_workspaceTree->setMouseTracking(true);
|
||||
|
||||
// Override palette: selection + hover use theme colors (not default blue)
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
QPalette tp = m_workspaceTree->palette();
|
||||
tp.setColor(QPalette::Text, t.textDim);
|
||||
tp.setColor(QPalette::Highlight, t.hover);
|
||||
tp.setColor(QPalette::HighlightedText, t.text);
|
||||
m_workspaceTree->setPalette(tp);
|
||||
}
|
||||
|
||||
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_workspaceTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
QModelIndex index = m_workspaceTree->indexAt(pos);
|
||||
@@ -2280,8 +2503,16 @@ void MainWindow::populateSourceMenu() {
|
||||
{QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")},
|
||||
};
|
||||
|
||||
m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/file-binary.svg")),
|
||||
QStringLiteral("File"), this, [this]() {
|
||||
auto addSourceAction = [this](const QString& text, const QIcon& icon, auto&& slot) {
|
||||
auto* act = m_sourceMenu->addAction(icon, text);
|
||||
act->setIconVisibleInMenu(true);
|
||||
connect(act, &QAction::triggered, this, std::forward<decltype(slot)>(slot));
|
||||
return act;
|
||||
};
|
||||
|
||||
addSourceAction(QStringLiteral("File"),
|
||||
makeIcon(QStringLiteral(":/vsicons/file-binary.svg")),
|
||||
[this]() {
|
||||
if (auto* c = activeController()) c->selectSource(QStringLiteral("File"));
|
||||
});
|
||||
|
||||
@@ -2289,14 +2520,14 @@ void MainWindow::populateSourceMenu() {
|
||||
for (const auto& prov : providers) {
|
||||
QString name = prov.name;
|
||||
auto it = s_providerIcons.constFind(prov.identifier);
|
||||
QIcon icon(it != s_providerIcons.constEnd() ? *it
|
||||
: QStringLiteral(":/vsicons/extensions.svg"));
|
||||
QIcon icon = makeIcon(it != s_providerIcons.constEnd() ? *it
|
||||
: QStringLiteral(":/vsicons/extensions.svg"));
|
||||
|
||||
QString label = prov.dllFileName.isEmpty()
|
||||
? name
|
||||
: QStringLiteral("%1 (%2)").arg(name, prov.dllFileName);
|
||||
|
||||
m_sourceMenu->addAction(icon, label, this, [this, name]() {
|
||||
addSourceAction(label, icon, [this, name]() {
|
||||
if (auto* c = activeController()) c->selectSource(name);
|
||||
});
|
||||
}
|
||||
@@ -2306,18 +2537,20 @@ void MainWindow::populateSourceMenu() {
|
||||
for (int i = 0; i < ctrl->savedSources().size(); i++) {
|
||||
const auto& e = ctrl->savedSources()[i];
|
||||
auto* act = m_sourceMenu->addAction(
|
||||
QStringLiteral("%1 '%2'").arg(e.kind, e.displayName),
|
||||
this, [this, i]() {
|
||||
if (auto* c = activeController()) c->switchSource(i);
|
||||
});
|
||||
QStringLiteral("%1 '%2'").arg(e.kind, e.displayName));
|
||||
act->setCheckable(true);
|
||||
act->setChecked(i == ctrl->activeSourceIndex());
|
||||
connect(act, &QAction::triggered, this, [this, i]() {
|
||||
if (auto* c = activeController()) c->switchSource(i);
|
||||
});
|
||||
}
|
||||
m_sourceMenu->addSeparator();
|
||||
m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/clear-all.svg")),
|
||||
QStringLiteral("Clear All"), this, [this]() {
|
||||
auto* clearAct = addSourceAction(QStringLiteral("Clear All"),
|
||||
makeIcon(QStringLiteral(":/vsicons/clear-all.svg")),
|
||||
[this]() {
|
||||
if (auto* c = activeController()) c->clearSources();
|
||||
});
|
||||
Q_UNUSED(clearAct);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "theme.h"
|
||||
#include <QtGlobal>
|
||||
#include <type_traits>
|
||||
|
||||
namespace rcx {
|
||||
@@ -61,6 +62,15 @@ Theme Theme::fromJson(const QJsonObject& o) {
|
||||
t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString;
|
||||
if (!t.indHeatHot.isValid())
|
||||
t.indHeatHot = t.markerPtr;
|
||||
|
||||
// Ensure hover is visually distinct from background
|
||||
if (t.hover.isValid() && t.background.isValid()) {
|
||||
int dist = qAbs(t.hover.red() - t.background.red())
|
||||
+ qAbs(t.hover.green() - t.background.green())
|
||||
+ qAbs(t.hover.blue() - t.background.blue());
|
||||
if (dist < 20)
|
||||
t.hover = t.background.lighter(130);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,14 +76,16 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
|
||||
QStringLiteral("QLabel { color: %1; font-size: 12px; font-weight: bold; }")
|
||||
.arg(theme.textDim.name()));
|
||||
|
||||
// Menu bar styling — transparent background, themed text
|
||||
m_menuBar->setStyleSheet(
|
||||
QStringLiteral(
|
||||
"QMenuBar { background: transparent; border: none; }"
|
||||
"QMenuBar::item { background: transparent; color: %1; padding: 8px 8px 4px 8px; }"
|
||||
"QMenuBar::item:selected { background: %2; }"
|
||||
"QMenuBar::item:pressed { background: %2; }")
|
||||
.arg(theme.textDim.name(), theme.hover.name()));
|
||||
// Menu bar palette — hover/bg handled by MenuBarStyle QProxyStyle.
|
||||
// Set Window + Button to background so Fusion never paints a foreign color.
|
||||
{
|
||||
QPalette mbPal = m_menuBar->palette();
|
||||
mbPal.setColor(QPalette::Window, theme.background);
|
||||
mbPal.setColor(QPalette::Button, theme.background);
|
||||
mbPal.setColor(QPalette::ButtonText, theme.textDim);
|
||||
m_menuBar->setPalette(mbPal);
|
||||
m_menuBar->setAutoFillBackground(false);
|
||||
}
|
||||
|
||||
// Chrome buttons
|
||||
QString btnStyle = QStringLiteral(
|
||||
|
||||
@@ -34,7 +34,7 @@ private:
|
||||
QToolButton* m_btnClose = nullptr;
|
||||
|
||||
Theme m_theme;
|
||||
bool m_titleCase = true;
|
||||
bool m_titleCase = false;
|
||||
|
||||
QToolButton* makeChromeButton(const QString& iconPath);
|
||||
void toggleMaximize();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QJsonDocument>
|
||||
#include <QFile>
|
||||
#include "core.h"
|
||||
|
||||
using namespace rcx;
|
||||
@@ -1984,6 +1986,371 @@ private slots:
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// Union tests
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
|
||||
void testUnionHeaderShowsKeyword() {
|
||||
// Union (Struct with classKeyword="union") should display "union" in header
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Union container
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.classKeyword = "union";
|
||||
u.name = "u1";
|
||||
u.parentId = rootId;
|
||||
u.offset = 0;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
// Two members at offset 0
|
||||
Node m1;
|
||||
m1.kind = NodeKind::UInt32;
|
||||
m1.name = "asInt";
|
||||
m1.parentId = uId;
|
||||
m1.offset = 0;
|
||||
tree.addNode(m1);
|
||||
|
||||
Node m2;
|
||||
m2.kind = NodeKind::Float;
|
||||
m2.name = "asFloat";
|
||||
m2.parentId = uId;
|
||||
m2.offset = 0;
|
||||
tree.addNode(m2);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
QStringList lines = result.text.split('\n');
|
||||
|
||||
// Find the union header line
|
||||
int headerLine = -1;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].lineKind == LineKind::Header &&
|
||||
result.meta[i].nodeKind == NodeKind::Struct &&
|
||||
result.meta[i].depth == 1) {
|
||||
headerLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY(headerLine >= 0);
|
||||
QVERIFY2(lines[headerLine].contains("union"),
|
||||
qPrintable("Union header should contain 'union': " + lines[headerLine]));
|
||||
|
||||
// Both members should be rendered at depth 2
|
||||
int memberCount = 0;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth == 2)
|
||||
memberCount++;
|
||||
}
|
||||
QCOMPARE(memberCount, 2);
|
||||
|
||||
// Both members share the same offset text (both at 0000)
|
||||
QVector<int> memberLines;
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
if (result.meta[i].lineKind == LineKind::Field && result.meta[i].depth == 2)
|
||||
memberLines.append(i);
|
||||
}
|
||||
QCOMPARE(memberLines.size(), 2);
|
||||
QCOMPARE(result.meta[memberLines[0]].offsetText,
|
||||
result.meta[memberLines[1]].offsetText);
|
||||
}
|
||||
|
||||
void testUnionCollapsed() {
|
||||
// Collapsed union should hide children
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.classKeyword = "union";
|
||||
u.name = "u1";
|
||||
u.parentId = rootId;
|
||||
u.offset = 0;
|
||||
u.collapsed = true;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
Node m;
|
||||
m.kind = NodeKind::UInt64;
|
||||
m.name = "val";
|
||||
m.parentId = uId;
|
||||
m.offset = 0;
|
||||
tree.addNode(m);
|
||||
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov);
|
||||
|
||||
// No field lines at depth 2
|
||||
int deepFields = 0;
|
||||
for (const auto& lm : result.meta) {
|
||||
if (lm.lineKind == LineKind::Field && lm.depth >= 2)
|
||||
deepFields++;
|
||||
}
|
||||
QCOMPARE(deepFields, 0);
|
||||
}
|
||||
|
||||
void testUnionStructSpan() {
|
||||
// structSpan of a union = max(child offset + size), not sum
|
||||
NodeTree tree;
|
||||
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.classKeyword = "union";
|
||||
u.name = "U";
|
||||
u.parentId = 0;
|
||||
u.offset = 0;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
// 2-byte member
|
||||
Node m1;
|
||||
m1.kind = NodeKind::UInt16;
|
||||
m1.name = "small";
|
||||
m1.parentId = uId;
|
||||
m1.offset = 0;
|
||||
tree.addNode(m1);
|
||||
|
||||
// 8-byte member
|
||||
Node m2;
|
||||
m2.kind = NodeKind::UInt64;
|
||||
m2.name = "big";
|
||||
m2.parentId = uId;
|
||||
m2.offset = 0;
|
||||
tree.addNode(m2);
|
||||
|
||||
// structSpan = max(0+2, 0+8) = 8
|
||||
QCOMPARE(tree.structSpan(uId), 8);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// Enum compose tests
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
|
||||
void testEnumDisplaysMembers() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node e;
|
||||
e.kind = NodeKind::Struct;
|
||||
e.classKeyword = "enum";
|
||||
e.name = "Color";
|
||||
e.structTypeName = "Color";
|
||||
e.parentId = rootId;
|
||||
e.offset = 0;
|
||||
e.collapsed = false;
|
||||
e.enumMembers = {{"Red", 0}, {"Green", 1}, {"Blue", 2}};
|
||||
tree.addNode(e);
|
||||
|
||||
NullProvider prov;
|
||||
auto result = compose(tree, prov);
|
||||
|
||||
// Should have enum members in the text
|
||||
QVERIFY(result.text.contains("Red"));
|
||||
QVERIFY(result.text.contains("Green"));
|
||||
QVERIFY(result.text.contains("Blue"));
|
||||
QVERIFY(result.text.contains("= 0"));
|
||||
QVERIFY(result.text.contains("= 2"));
|
||||
// Header should contain "enum"
|
||||
QVERIFY(result.text.contains("enum"));
|
||||
}
|
||||
|
||||
void testEnumCollapsed() {
|
||||
NodeTree tree;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Root";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
Node e;
|
||||
e.kind = NodeKind::Struct;
|
||||
e.classKeyword = "enum";
|
||||
e.name = "Flags";
|
||||
e.structTypeName = "Flags";
|
||||
e.parentId = rootId;
|
||||
e.offset = 0;
|
||||
e.collapsed = true;
|
||||
e.enumMembers = {{"A", 0}, {"B", 1}};
|
||||
tree.addNode(e);
|
||||
|
||||
NullProvider prov;
|
||||
auto result = compose(tree, prov);
|
||||
|
||||
// Collapsed: members should NOT appear
|
||||
QVERIFY(!result.text.contains("= 0"));
|
||||
QVERIFY(!result.text.contains("= 1"));
|
||||
// But header should still show
|
||||
QVERIFY(result.text.contains("enum"));
|
||||
QVERIFY(result.text.contains("Flags"));
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
// Compact columns: load EPROCESS.rcx and compare output
|
||||
// ═════════════════════════════════════════════════════════════
|
||||
|
||||
void testCompactColumnsEprocess() {
|
||||
// Load the EPROCESS example .rcx
|
||||
// Try multiple paths: build dir examples, or source dir
|
||||
QString rcxPath;
|
||||
QStringList candidates = {
|
||||
QCoreApplication::applicationDirPath() + "/examples/EPROCESS.rcx",
|
||||
QCoreApplication::applicationDirPath() + "/../src/examples/EPROCESS.rcx",
|
||||
};
|
||||
for (const auto& c : candidates) {
|
||||
if (QFile::exists(c)) { rcxPath = c; break; }
|
||||
}
|
||||
if (rcxPath.isEmpty())
|
||||
QSKIP("EPROCESS.rcx not found");
|
||||
QFile file(rcxPath);
|
||||
QVERIFY2(file.open(QIODevice::ReadOnly),
|
||||
qPrintable("Cannot open " + rcxPath));
|
||||
QJsonDocument jdoc = QJsonDocument::fromJson(file.readAll());
|
||||
NodeTree tree = NodeTree::fromJson(jdoc.object());
|
||||
NullProvider prov;
|
||||
|
||||
// Compose WITHOUT compact (default)
|
||||
ComposeResult normal = compose(tree, prov, 0, false);
|
||||
// Compose WITH compact
|
||||
ComposeResult compact = compose(tree, prov, 0, true);
|
||||
|
||||
// Compact typeW should be capped at kCompactTypeW (22)
|
||||
QVERIFY2(compact.layout.typeW <= kCompactTypeW,
|
||||
qPrintable(QString("compact typeW=%1, expected <= %2")
|
||||
.arg(compact.layout.typeW).arg(kCompactTypeW)));
|
||||
|
||||
// Normal typeW should be wider (the _EPROCESS has long type names)
|
||||
QVERIFY2(normal.layout.typeW > compact.layout.typeW,
|
||||
qPrintable(QString("normal typeW=%1 should exceed compact typeW=%2")
|
||||
.arg(normal.layout.typeW).arg(compact.layout.typeW)));
|
||||
|
||||
// Print side-by-side sample for visual inspection
|
||||
QStringList normalLines = normal.text.split('\n');
|
||||
QStringList compactLines = compact.text.split('\n');
|
||||
qDebug() << "\n=== EPROCESS compact columns comparison ===";
|
||||
qDebug() << "Normal typeW:" << normal.layout.typeW
|
||||
<< " Compact typeW:" << compact.layout.typeW;
|
||||
qDebug() << "Normal lines:" << normalLines.size()
|
||||
<< " Compact lines:" << compactLines.size();
|
||||
|
||||
// Dump full output to files for visual diffing
|
||||
{
|
||||
QFile nf(QCoreApplication::applicationDirPath() + "/../eprocess_normal.txt");
|
||||
nf.open(QIODevice::WriteOnly);
|
||||
nf.write(normal.text.toUtf8());
|
||||
}
|
||||
{
|
||||
QFile cf(QCoreApplication::applicationDirPath() + "/../eprocess_compact.txt");
|
||||
cf.open(QIODevice::WriteOnly);
|
||||
cf.write(compact.text.toUtf8());
|
||||
}
|
||||
qDebug() << "Wrote eprocess_normal.txt and eprocess_compact.txt";
|
||||
|
||||
// Show first 50 lines of each for quick inspection
|
||||
qDebug() << "\n--- NORMAL (first 50 lines) ---";
|
||||
for (int i = 0; i < qMin(50, normalLines.size()); ++i)
|
||||
qDebug().noquote() << normalLines[i];
|
||||
|
||||
qDebug() << "\n--- COMPACT (first 50 lines) ---";
|
||||
for (int i = 0; i < qMin(50, compactLines.size()); ++i)
|
||||
qDebug().noquote() << compactLines[i];
|
||||
|
||||
// Overflow types should print in full (no truncation)
|
||||
bool foundFull = false;
|
||||
for (const QString& l : compactLines) {
|
||||
if (l.contains("_PS_DYNAMIC_ENFORCED_ADDRESS_RANGES")) {
|
||||
foundFull = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(foundFull,
|
||||
"Long type _PS_DYNAMIC_ENFORCED_ADDRESS_RANGES should print in full (no truncation)");
|
||||
}
|
||||
|
||||
void testMmpfnRcxLoadsAndComposes() {
|
||||
// Load the MMPFN.rcx example file and verify it composes without errors
|
||||
// Try several paths to find the .rcx file
|
||||
QString rcxPath;
|
||||
for (const auto& p : {
|
||||
QStringLiteral("../src/examples/MMPFN.rcx"),
|
||||
QStringLiteral("../../src/examples/MMPFN.rcx"),
|
||||
QStringLiteral("src/examples/MMPFN.rcx")}) {
|
||||
if (QFile::exists(p)) { rcxPath = p; break; }
|
||||
}
|
||||
if (rcxPath.isEmpty()) {
|
||||
QSKIP("MMPFN.rcx not found (run from build dir)");
|
||||
}
|
||||
QFile f(rcxPath);
|
||||
QVERIFY2(f.open(QIODevice::ReadOnly), "Cannot open MMPFN.rcx");
|
||||
QJsonDocument jdoc = QJsonDocument::fromJson(f.readAll());
|
||||
QVERIFY(jdoc.isObject());
|
||||
NodeTree tree = NodeTree::fromJson(jdoc.object());
|
||||
|
||||
QVERIFY2(tree.nodes.size() >= 60, "Expected at least 60 nodes");
|
||||
|
||||
// Check key top-level types exist
|
||||
bool hasMmpfn = false, hasListEntry = false, hasMmpte = false;
|
||||
for (const auto& n : tree.nodes) {
|
||||
if (n.parentId == 0 && n.structTypeName == "_MMPFN") hasMmpfn = true;
|
||||
if (n.parentId == 0 && n.structTypeName == "_LIST_ENTRY") hasListEntry = true;
|
||||
if (n.parentId == 0 && n.structTypeName == "_MMPTE") hasMmpte = true;
|
||||
}
|
||||
QVERIFY2(hasMmpfn, "Missing _MMPFN top-level type");
|
||||
QVERIFY2(hasListEntry, "Missing _LIST_ENTRY top-level type");
|
||||
QVERIFY2(hasMmpte, "Missing _MMPTE top-level type");
|
||||
|
||||
// Compose and verify output
|
||||
NullProvider prov;
|
||||
ComposeResult result = compose(tree, prov, 0, false);
|
||||
QStringList lines = result.text.split('\n');
|
||||
QVERIFY2(lines.size() > 10, "Expected non-trivial compose output");
|
||||
|
||||
// Print first 30 lines for manual inspection
|
||||
qDebug() << "=== MMPFN compose output ===";
|
||||
for (int i = 0; i < qMin(30, lines.size()); ++i)
|
||||
qDebug().noquote() << lines[i];
|
||||
qDebug() << "... total lines:" << lines.size();
|
||||
|
||||
// Verify _MMPFN header appears in output
|
||||
bool foundMmpfn = false;
|
||||
for (const auto& l : lines) {
|
||||
if (l.contains("_MMPFN")) { foundMmpfn = true; break; }
|
||||
}
|
||||
QVERIFY2(foundMmpfn, "Compose output should contain _MMPFN");
|
||||
|
||||
// Verify no M_CYCLE markers on any lines (all self-ref pointers are collapsed)
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
bool hasCycle = (result.meta[i].markerMask & (1u << M_CYCLE)) != 0;
|
||||
QVERIFY2(!hasCycle,
|
||||
qPrintable(QString("Unexpected cycle marker on line %1").arg(i)));
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestCompose)
|
||||
|
||||
@@ -49,7 +49,9 @@ private slots:
|
||||
void forwardDeclaration();
|
||||
|
||||
// Union handling
|
||||
void unionPickFirst();
|
||||
void unionContainer();
|
||||
void unionWithCommentOffsets();
|
||||
void namedUnion();
|
||||
|
||||
// Padding fields
|
||||
void paddingFieldExpansion();
|
||||
@@ -69,11 +71,19 @@ private slots:
|
||||
|
||||
// Edge cases
|
||||
void bitfieldSkipped();
|
||||
void bitfieldWithOffsetsEmitsHex();
|
||||
void hexArraySizes();
|
||||
void windowsStylePEB();
|
||||
void classKeyword();
|
||||
void inheritanceSkipped();
|
||||
|
||||
// Enum tests
|
||||
void enumBasic();
|
||||
void enumAutoValues();
|
||||
void enumHexValues();
|
||||
void enumInStruct();
|
||||
void enumClass();
|
||||
|
||||
// Round-trip test (requires generator.h)
|
||||
void basicRoundTrip();
|
||||
};
|
||||
@@ -575,7 +585,7 @@ void TestImportSource::forwardDeclaration() {
|
||||
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||
}
|
||||
|
||||
void TestImportSource::unionPickFirst() {
|
||||
void TestImportSource::unionContainer() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct WithUnion {\n"
|
||||
" union {\n"
|
||||
@@ -586,12 +596,85 @@ void TestImportSource::unionPickFirst() {
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// Should have 2 fields: asFloat (first union member) + after
|
||||
// Should have 2 direct children: union container + after
|
||||
QCOMPARE(kids.size(), 2);
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("asFloat"));
|
||||
|
||||
// First child is the union container
|
||||
const auto& unionNode = tree.nodes[kids[0]];
|
||||
QCOMPARE(unionNode.kind, NodeKind::Struct);
|
||||
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
|
||||
QCOMPARE(unionNode.offset, 0);
|
||||
|
||||
// Union has 2 children, both at offset 0
|
||||
auto unionKids = childrenOf(tree, unionNode.id);
|
||||
QCOMPARE(unionKids.size(), 2);
|
||||
QCOMPARE(tree.nodes[unionKids[0]].kind, NodeKind::Float);
|
||||
QCOMPARE(tree.nodes[unionKids[0]].name, QStringLiteral("asFloat"));
|
||||
QCOMPARE(tree.nodes[unionKids[0]].offset, 0);
|
||||
QCOMPARE(tree.nodes[unionKids[1]].kind, NodeKind::UInt32);
|
||||
QCOMPARE(tree.nodes[unionKids[1]].name, QStringLiteral("asInt"));
|
||||
QCOMPARE(tree.nodes[unionKids[1]].offset, 0);
|
||||
|
||||
// structSpan of union = max member size = 4
|
||||
QCOMPARE(tree.structSpan(unionNode.id), 4);
|
||||
|
||||
// after field follows the union at offset 4
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
|
||||
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
}
|
||||
|
||||
void TestImportSource::unionWithCommentOffsets() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct S {\n"
|
||||
" uint64_t a; // 0x0\n"
|
||||
" union {\n"
|
||||
" uint32_t x; // 0x8\n"
|
||||
" float y; // 0x8\n"
|
||||
" };\n"
|
||||
" uint32_t b; // 0xC\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 3); // a + union + b
|
||||
|
||||
// Union at offset 0x8
|
||||
const auto& unionNode = tree.nodes[kids[1]];
|
||||
QCOMPARE(unionNode.kind, NodeKind::Struct);
|
||||
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
|
||||
QCOMPARE(unionNode.offset, 0x8);
|
||||
|
||||
// Union members at offset 0 (relative to union)
|
||||
auto unionKids = childrenOf(tree, unionNode.id);
|
||||
QCOMPARE(unionKids.size(), 2);
|
||||
QCOMPARE(tree.nodes[unionKids[0]].offset, 0);
|
||||
QCOMPARE(tree.nodes[unionKids[1]].offset, 0);
|
||||
|
||||
// b at 0xC
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 0xC);
|
||||
}
|
||||
|
||||
void TestImportSource::namedUnion() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct S {\n"
|
||||
" union {\n"
|
||||
" uint16_t shortVal;\n"
|
||||
" uint64_t longVal;\n"
|
||||
" } u3;\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
QCOMPARE(kids.size(), 1);
|
||||
|
||||
const auto& unionNode = tree.nodes[kids[0]];
|
||||
QCOMPARE(unionNode.kind, NodeKind::Struct);
|
||||
QCOMPARE(unionNode.classKeyword, QStringLiteral("union"));
|
||||
QCOMPARE(unionNode.name, QStringLiteral("u3"));
|
||||
|
||||
auto unionKids = childrenOf(tree, unionNode.id);
|
||||
QCOMPARE(unionKids.size(), 2);
|
||||
// structSpan = max(2, 8) = 8
|
||||
QCOMPARE(tree.structSpan(unionNode.id), 8);
|
||||
}
|
||||
|
||||
void TestImportSource::paddingFieldExpansion() {
|
||||
@@ -697,6 +780,7 @@ void TestImportSource::structPrefixOnType() {
|
||||
}
|
||||
|
||||
void TestImportSource::bitfieldSkipped() {
|
||||
// Bitfields emit a hex placeholder covering the group
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct BF {\n"
|
||||
" uint32_t normal;\n"
|
||||
@@ -706,10 +790,38 @@ void TestImportSource::bitfieldSkipped() {
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// Bitfields should be skipped, only normal + after
|
||||
QCOMPARE(kids.size(), 2);
|
||||
// normal + Hex16 (16 bits → 2 bytes) + after
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex16);
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 6);
|
||||
}
|
||||
|
||||
void TestImportSource::bitfieldWithOffsetsEmitsHex() {
|
||||
NodeTree tree = importFromSource(QStringLiteral(
|
||||
"struct BF2 {\n"
|
||||
" uint32_t normal; // 0x0\n"
|
||||
" ULONGLONG Valid : 1; // 0x4\n"
|
||||
" ULONGLONG Dirty : 1; // 0x4\n"
|
||||
" ULONGLONG PageFrameNumber : 36; // 0x4\n"
|
||||
" ULONGLONG Reserved : 26; // 0x4\n"
|
||||
" uint32_t after; // 0xC\n"
|
||||
"};\n"
|
||||
));
|
||||
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||
// normal + hex64 (bitfield group: 64 bits) + after = 3
|
||||
QCOMPARE(kids.size(), 3);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||
// Bitfield group emitted as Hex64 at offset 4
|
||||
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex64);
|
||||
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||
// after at 0xC
|
||||
QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after"));
|
||||
QCOMPARE(tree.nodes[kids[2]].offset, 0xC);
|
||||
}
|
||||
|
||||
void TestImportSource::hexArraySizes() {
|
||||
@@ -842,5 +954,78 @@ void TestImportSource::basicRoundTrip() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Enum tests ──
|
||||
|
||||
void TestImportSource::enumBasic() {
|
||||
auto tree = importFromSource(QStringLiteral(
|
||||
"enum Color { Red = 0, Green = 1, Blue = 2 };"));
|
||||
QCOMPARE(countRoots(tree), 1);
|
||||
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("enum"));
|
||||
QCOMPARE(tree.nodes[0].structTypeName, QStringLiteral("Color"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers.size(), 3);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].first, QStringLiteral("Red"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0LL);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[1].first, QStringLiteral("Green"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers[1].second, 1LL);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[2].first, QStringLiteral("Blue"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers[2].second, 2LL);
|
||||
}
|
||||
|
||||
void TestImportSource::enumAutoValues() {
|
||||
auto tree = importFromSource(QStringLiteral(
|
||||
"enum Flags { A, B, C };"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers.size(), 3);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0LL);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[1].second, 1LL);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[2].second, 2LL);
|
||||
}
|
||||
|
||||
void TestImportSource::enumHexValues() {
|
||||
auto tree = importFromSource(QStringLiteral(
|
||||
"enum { X = 0x10, Y = 0x20 };"));
|
||||
// Anonymous enum has no name — parser skips it (unnamed enums are not added)
|
||||
// Actually, let's use a named enum with hex values
|
||||
tree = importFromSource(QStringLiteral(
|
||||
"enum Hex { X = 0x10, Y = 0x20 };"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers.size(), 2);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].second, 0x10LL);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[1].second, 0x20LL);
|
||||
}
|
||||
|
||||
void TestImportSource::enumInStruct() {
|
||||
auto tree = importFromSource(QStringLiteral(
|
||||
"enum PoolType { NonPaged = 0, Paged = 1 };\n"
|
||||
"struct Foo {\n"
|
||||
" PoolType pool; //0x0\n"
|
||||
" uint32_t size; //0x4\n"
|
||||
"};"));
|
||||
// Should have 2 roots: PoolType enum + Foo struct
|
||||
QCOMPARE(countRoots(tree), 2);
|
||||
|
||||
// Find Foo struct
|
||||
int fooIdx = -1;
|
||||
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||
if (tree.nodes[i].name == QStringLiteral("Foo")) { fooIdx = i; break; }
|
||||
}
|
||||
QVERIFY(fooIdx >= 0);
|
||||
auto kids = childrenOf(tree, tree.nodes[fooIdx].id);
|
||||
QCOMPARE(kids.size(), 2);
|
||||
// First child should be UInt32 (enum mapped to int) with refId to PoolType
|
||||
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
|
||||
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("pool"));
|
||||
QVERIFY(tree.nodes[kids[0]].refId != 0); // linked to enum definition
|
||||
}
|
||||
|
||||
void TestImportSource::enumClass() {
|
||||
auto tree = importFromSource(QStringLiteral(
|
||||
"enum class Scope : uint8_t { A = 1, B = 2 };"));
|
||||
QCOMPARE(countRoots(tree), 1);
|
||||
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("enum"));
|
||||
QCOMPARE(tree.nodes[0].structTypeName, QStringLiteral("Scope"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers.size(), 2);
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].first, QStringLiteral("A"));
|
||||
QCOMPARE(tree.nodes[0].enumMembers[0].second, 1LL);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestImportSource)
|
||||
#include "test_import_source.moc"
|
||||
|
||||
@@ -256,8 +256,9 @@ private slots:
|
||||
{
|
||||
WinDbgMemoryProvider prov(m_connString);
|
||||
QVERIFY(prov.isValid());
|
||||
QVERIFY2(prov.base() != 0, "Should have a non-zero base from first module");
|
||||
qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16);
|
||||
// WinDbg provider no longer auto-selects a module base — it returns 0
|
||||
// so the controller doesn't override the user's chosen base address.
|
||||
QCOMPARE(prov.base(), (uint64_t)0);
|
||||
}
|
||||
|
||||
// ── Read: MZ header on main thread ──
|
||||
@@ -446,6 +447,139 @@ private slots:
|
||||
QCOMPARE(raw->Name(), std::string("WinDbg Memory"));
|
||||
delete raw;
|
||||
}
|
||||
|
||||
// ── Kernel session tests ──
|
||||
// Requires a WinDbg instance with a kernel dump loaded and
|
||||
// .server tcp:port=5055 running. Skipped automatically if
|
||||
// no server is available. Override with WINDBG_KERNEL_CONN env var.
|
||||
|
||||
void provider_kernel_connect()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available (set WINDBG_KERNEL_CONN)");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY2(prov.isValid(), "Should connect to kernel debug server");
|
||||
QCOMPARE(prov.kind(), QStringLiteral("WinDbg"));
|
||||
|
||||
qDebug() << "Kernel provider name:" << prov.name();
|
||||
qDebug() << "Kernel provider base:" << QString("0x%1").arg(prov.base(), 0, 16);
|
||||
qDebug() << "Kernel provider isLive:" << prov.isLive();
|
||||
|
||||
// Name should not be an arbitrary user-mode DLL
|
||||
QVERIFY2(!prov.name().contains("WS2_32", Qt::CaseInsensitive),
|
||||
qPrintable("Name should not be 'WS2_32', got: " + prov.name()));
|
||||
}
|
||||
|
||||
void provider_kernel_read_base()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
// Provider no longer auto-selects a base. Use a known kernel address
|
||||
// from env, or skip.
|
||||
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||
if (addrStr.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
|
||||
|
||||
bool ok = false;
|
||||
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
||||
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
|
||||
|
||||
uint8_t buf[16] = {};
|
||||
ok = prov.read(addr, buf, 16);
|
||||
QVERIFY2(ok, "Should read from kernel address");
|
||||
|
||||
bool allZero = true;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
if (buf[i] != 0) { allZero = false; break; }
|
||||
}
|
||||
QVERIFY2(!allZero, "Kernel read returned all zeros");
|
||||
}
|
||||
|
||||
void provider_kernel_read_high_address()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
// Use env var for a specific kernel address (e.g. _EPROCESS),
|
||||
// otherwise fall back to the provider's base.
|
||||
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||
uint64_t addr = 0;
|
||||
if (!addrStr.isEmpty()) {
|
||||
bool ok = false;
|
||||
addr = addrStr.toULongLong(&ok, 16);
|
||||
if (!ok) addr = 0;
|
||||
}
|
||||
if (addr == 0) addr = prov.base();
|
||||
|
||||
uint8_t buf[64] = {};
|
||||
bool ok = prov.read(addr, buf, 64);
|
||||
QVERIFY2(ok, qPrintable(QString("Should read kernel addr 0x%1")
|
||||
.arg(addr, 0, 16)));
|
||||
|
||||
bool allZero = true;
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
if (buf[i] != 0) { allZero = false; break; }
|
||||
}
|
||||
QVERIFY2(!allZero, "Kernel high-address read returned all zeros");
|
||||
|
||||
qDebug() << "Read 64 bytes at" << QString("0x%1").arg(addr, 0, 16)
|
||||
<< "first 8:" << QString("%1 %2 %3 %4 %5 %6 %7 %8")
|
||||
.arg(buf[0], 2, 16, QChar('0'))
|
||||
.arg(buf[1], 2, 16, QChar('0'))
|
||||
.arg(buf[2], 2, 16, QChar('0'))
|
||||
.arg(buf[3], 2, 16, QChar('0'))
|
||||
.arg(buf[4], 2, 16, QChar('0'))
|
||||
.arg(buf[5], 2, 16, QChar('0'))
|
||||
.arg(buf[6], 2, 16, QChar('0'))
|
||||
.arg(buf[7], 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
void provider_kernel_read_backgroundThread()
|
||||
{
|
||||
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||
"tcp:Port=5055,Server=localhost");
|
||||
if (!canConnect(kernelConn))
|
||||
QSKIP("No kernel debug server available");
|
||||
|
||||
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||
if (addrStr.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
|
||||
|
||||
bool ok = false;
|
||||
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
||||
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
|
||||
|
||||
WinDbgMemoryProvider prov(kernelConn);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
// Simulate the controller's async refresh pattern
|
||||
QFuture<QByteArray> future = QtConcurrent::run([&prov, addr]() -> QByteArray {
|
||||
return prov.readBytes(addr, 4096);
|
||||
});
|
||||
future.waitForFinished();
|
||||
QByteArray data = future.result();
|
||||
|
||||
QCOMPARE(data.size(), 4096);
|
||||
bool allZero = true;
|
||||
for (int i = 0; i < data.size(); ++i) {
|
||||
if (data[i] != '\0') { allZero = false; break; }
|
||||
}
|
||||
QVERIFY2(!allZero, "Kernel background read returned all zeros");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestWinDbgProvider)
|
||||
|
||||
159
tools/test_hover.py
Normal file
159
tools/test_hover.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Structural hover test: validate that all themes produce visible hover colors
|
||||
and that the QProxyStyle code handles the required control elements.
|
||||
|
||||
No pixel sampling — checks theme JSON values and source code patterns.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def hex_to_rgb(h):
|
||||
h = h.lstrip('#')
|
||||
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
|
||||
|
||||
|
||||
def color_dist(c1, c2):
|
||||
return sum(abs(a - b) for a, b in zip(c1, c2))
|
||||
|
||||
|
||||
def lighter_130(rgb):
|
||||
"""Approximate Qt's QColor::lighter(130) for dark grays."""
|
||||
r, g, b = rgb
|
||||
return (min(255, int(r * 1.3) + 1),
|
||||
min(255, int(g * 1.3) + 1),
|
||||
min(255, int(b * 1.3) + 1))
|
||||
|
||||
|
||||
def load_themes():
|
||||
themes = {}
|
||||
theme_dir = os.path.join(os.path.dirname(__file__),
|
||||
'..', 'src', 'themes', 'defaults')
|
||||
if not os.path.isdir(theme_dir):
|
||||
return themes
|
||||
for name in os.listdir(theme_dir):
|
||||
if name.endswith('.json'):
|
||||
with open(os.path.join(theme_dir, name)) as f:
|
||||
themes[name] = json.load(f)
|
||||
return themes
|
||||
|
||||
|
||||
def test_hover_visibility(themes):
|
||||
"""Every theme must have hover visually distinct from background.
|
||||
If raw values are identical, Theme::fromJson applies lighter(130)."""
|
||||
ok = True
|
||||
for name, data in sorted(themes.items()):
|
||||
bg = hex_to_rgb(data['background'])
|
||||
hover = hex_to_rgb(data['hover'])
|
||||
dist = color_dist(bg, hover)
|
||||
|
||||
if dist < 20:
|
||||
# fromJson will fix this — verify the fix produces sufficient contrast
|
||||
fixed = lighter_130(bg)
|
||||
fixed_dist = color_dist(bg, fixed)
|
||||
if fixed_dist < 15:
|
||||
print(f" FAIL: {name}: hover==bg and lighter(130) still too close "
|
||||
f"(dist={fixed_dist})")
|
||||
ok = False
|
||||
else:
|
||||
print(f" OK: {name}: hover==bg, fromJson fixup -> "
|
||||
f"dist {dist}->{fixed_dist}")
|
||||
else:
|
||||
print(f" OK: {name}: hover distinct (dist={dist})")
|
||||
return ok
|
||||
|
||||
|
||||
def test_proxystyle_handlers():
|
||||
"""Verify MenuBarStyle handles CE_MenuBarItem, CE_MenuItem, CE_MenuBarEmptyArea."""
|
||||
src = os.path.join(os.path.dirname(__file__), '..', 'src', 'main.cpp')
|
||||
with open(src) as f:
|
||||
code = f.read()
|
||||
|
||||
required = {
|
||||
'CE_MenuBarItem': r'element\s*==\s*CE_MenuBarItem',
|
||||
'CE_MenuItem': r'element\s*==\s*CE_MenuItem',
|
||||
'CE_MenuBarEmptyArea': r'element\s*==\s*CE_MenuBarEmptyArea',
|
||||
'State_Selected': r'State_Selected',
|
||||
'QPalette::Mid': r'QPalette::Mid',
|
||||
}
|
||||
|
||||
ok = True
|
||||
for label, pattern in required.items():
|
||||
if re.search(pattern, code):
|
||||
print(f" OK: MenuBarStyle handles {label}")
|
||||
else:
|
||||
print(f" FAIL: MenuBarStyle missing {label}")
|
||||
ok = False
|
||||
return ok
|
||||
|
||||
|
||||
def test_no_menubar_css():
|
||||
"""Verify no CSS stylesheet is set on QMenuBar (would bypass QProxyStyle)."""
|
||||
src_dir = os.path.join(os.path.dirname(__file__), '..', 'src')
|
||||
ok = True
|
||||
for root, _, files in os.walk(src_dir):
|
||||
for fname in files:
|
||||
if not fname.endswith('.cpp'):
|
||||
continue
|
||||
path = os.path.join(root, fname)
|
||||
with open(path, encoding='utf-8', errors='replace') as f:
|
||||
for i, line in enumerate(f, 1):
|
||||
# Check for menuBar/m_menuBar stylesheet calls
|
||||
if ('menuBar' in line or 'm_menuBar' in line) and \
|
||||
'setStyleSheet' in line:
|
||||
print(f" FAIL: CSS on QMenuBar at {fname}:{i}: "
|
||||
f"{line.strip()}")
|
||||
ok = False
|
||||
if ok:
|
||||
print(" OK: No CSS on QMenuBar")
|
||||
return ok
|
||||
|
||||
|
||||
def test_hover_fixup_in_fromjson():
|
||||
"""Verify Theme::fromJson applies the hover fixup."""
|
||||
src = os.path.join(os.path.dirname(__file__),
|
||||
'..', 'src', 'themes', 'theme.cpp')
|
||||
with open(src) as f:
|
||||
code = f.read()
|
||||
|
||||
if 'lighter(130)' in code and 't.hover' in code:
|
||||
print(" OK: Theme::fromJson has hover fixup")
|
||||
return True
|
||||
else:
|
||||
print(" FAIL: Theme::fromJson missing hover fixup")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
themes = load_themes()
|
||||
if not themes:
|
||||
print("FAIL: No theme files found")
|
||||
return 1
|
||||
|
||||
all_ok = True
|
||||
|
||||
print("--- Test 1: Hover visibility across themes ---")
|
||||
all_ok &= test_hover_visibility(themes)
|
||||
|
||||
print("\n--- Test 2: QProxyStyle handles required elements ---")
|
||||
all_ok &= test_proxystyle_handlers()
|
||||
|
||||
print("\n--- Test 3: No CSS on QMenuBar ---")
|
||||
all_ok &= test_no_menubar_css()
|
||||
|
||||
print("\n--- Test 4: Theme::fromJson hover fixup ---")
|
||||
all_ok &= test_hover_fixup_in_fromjson()
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
if all_ok:
|
||||
print("ALL HOVER TESTS PASSED")
|
||||
return 0
|
||||
else:
|
||||
print("SOME HOVER TESTS FAILED")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user