Compare commits

...
56 Commits
Author SHA1 Message Date
pancake 49b4b19c21 Release 6.2.6 2026-11-06 02:29:25 +01:00
pancake 068520f923 Use ubuntu-24.04 as the minimum CI runner 2026-11-06 02:09:52 +01:00
pancake f723ee3dee Simplify the indent-check CI job and use r2 debs in the meson job 2026-11-06 02:06:09 +01:00
pancake ce1f9181cf Use r2 6.2.6 from git 2026-11-05 23:57:50 +01:00
pancake a043f93e72 Light theme have a faded cyan now 2026-11-05 11:40:54 +01:00
pancake c0cb4a5123 Fix build with radare2 ABI 168 basic block vectors 2026-10-30 18:51:50 +01:00
pancake db64c517a4 Fix ghost function lines when scrolling disassembly 2026-10-27 16:16:03 +01:00
pancake 9602d98307 Destroy QApplication normally after printing the version 2026-10-18 05:37:20 +02:00
pancake d85739b5aa Use a plain widget type alias for Qt 6.12 uic compatibility 2026-10-18 05:37:20 +02:00
pancake 1828006c2b Preserve 64-bit section sizes when scaling address maps
Avoid narrowing large section sizes to int when choosing the minimum size and computing relative heights. Handle a zero minimum before dividing.
2026-10-15 15:44:43 +02:00
pancake aff259387b Prevent division by zero in narrow color picker controls
Keep the checkerboard period at least two pixels so its half-width remains a valid divisor in the color preview and alpha bar.
2026-10-15 15:44:43 +02:00
pancake fe80cff908 Prevent overflow when scaling the full address scrollbar range
Compute the ceiling division without adding to the 64-bit span, which can wrap and produce a zero slider unit.
2026-10-15 15:44:42 +02:00
pancake ae183f2c43 Add consistent keyboard navigation to code views
Add Space folding and matching-brace navigation to the decompiler. Use Enter and keypad Enter to follow references and links in both code views, including jumps from an instruction address.

Enable horizontal disassembly navigation with arrows and H/L, and move Link type to Shift+L to avoid a shortcut conflict.
2026-10-15 12:25:08 +02:00
pancake 9ae80e4d62 Add colorful icons to the decompiler context menu 2026-10-15 11:46:25 +02:00
pancake e920c6aba8 Add brace block folding to the decompiler 2026-10-15 11:15:39 +02:00
pancake 5875f989d8 Add per-view seek history controls and route navigation to the active view 2026-10-15 10:33:22 +02:00
pancake d95daf392f Keep independent back and forward history for unsynchronized views 2026-10-15 10:17:35 +02:00
pancake 6c4997ac29 Add consistent decompiler hover and symbol navigation 2026-10-15 10:16:32 +02:00
pancake a8d239febd Add flag shortcuts, selection sizes and colors 2026-10-14 23:47:45 +02:00
pancake 6b52aa0be1 Fix comments and cursor navigation in the decompiler ## 2026-10-14 16:46:15 +02:00
pancake 19c3c07b99 Remove function annotations when saving empty text 2026-10-14 16:25:55 +02:00
pancake ef8aba25e8 Add address and disassembly toggles to the decompiler ## 2026-10-14 15:33:01 +02:00
pancake c2f042643a Highlight the active dock frame
Outline the selected frame and its tab bar with the theme highlight color. Follow focus, tab selection, keyboard navigation, and layout changes without consuming mouse input or changing panel geometry.

Validation: make -C tests/panelmaximize check (17 passed); visually checked tiled and expanded layouts.
2026-10-14 00:04:09 +02:00
pancake baf1afaca0 Cycle dock frames with Cmd/Super+J and K
Add configurable shortcuts to focus the next or previous frame in screen order with wraparound, preserving selected tabs and the last focused control. Keep expanded mode when navigating and skip hidden or detached panels. Add regression coverage for navigation, selected tabs, and closed frames.

Validation: make -C tests/panelmaximize check (17 passed).
2026-10-13 23:53:26 +02:00
pancake de8f2b476e Add Cmd/Super number shortcuts for panel tabs
Select the first five visible tabs of the active panel group in displayed order, retaining maximization when switching. Ignore offscreen Qt tab bars and cover tiled and maximized panels, reordered tabs, and unavailable tab numbers with regression tests.

Validation: make -C tests/panelmaximize check (14 passed).
2026-10-13 23:38:43 +02:00
pancake bc6c362bd5 Preserve dock tab selection across layout changes
Leave Qt's native tabData identities intact and use them to resolve docks instead of matching titles. Add regression coverage for layout changes, repeated maximization, and panels with duplicate titles.

Validation: make -C tests/panelmaximize check (11 passed).
2026-10-13 23:27:21 +02:00
pancake bff22b12bd Meta+Enter zooms the panel like in dwm/r2panels 2026-10-13 23:11:34 +02:00
pancake 63f2bd57cd Select small flag ranges in the hexdump on click 2026-10-13 19:31:32 +02:00
pancake 5518aaa29d Keep hexdump types and pins when reopening projects 2026-10-13 19:07:01 +02:00
pancake 73856c6c32 Add a recursive structure tree and pins to the hexdump ## 2026-10-13 19:06:51 +02:00
pancake 018c210777 Fix empty hexdump fetches allocating excessive memory 2026-10-13 19:06:22 +02:00
pancake 59a47845f0 Move Flags from Bin to Data in the View menu 2026-10-13 18:27:01 +02:00
pancake 11b530a526 Add global variable editing and navigation widget ## 2026-10-12 18:37:49 +02:00
pancake f6b792020a Add filesystem browsing and code navigation tests 2026-10-08 19:15:27 +02:00
pancake c50dc3313f Add source file browsing and clickable address previews ## 2026-10-08 19:15:00 +02:00
pancake 0e5a49c331 Fix make run without make install in macos 2026-10-07 15:52:48 +02:00
pancake 74c12685a0 Show options for the decai decompiler 2026-10-07 15:46:13 +02:00
pancake 93af6edfe9 Toggle decompiler with TAB, toggle graph with SPACE 2026-10-07 15:21:55 +02:00
pancake 977578a91e Basic block colors in minigraph and hexdump too 2026-10-07 07:59:44 +02:00
pancake e980b1cb48 Release 6.2.4 2026-10-06 00:08:21 +02:00
pancake b5edef36bc Add hack to workaround r2 6.2.4 r_cmd.h msvc issue 2026-10-05 22:28:23 +02:00
pancake 1208624c77 MakeFmt 2026-10-01 17:20:00 +02:00
pancake eff435fc29 Bounded scrollbar by default 2026-10-01 15:39:46 +02:00
pancake af1f536b0f Hightlight background of the actionable items on hover 2026-10-01 15:26:50 +02:00
pancake 6affec227d Pointing hand cursor for the actionable items in disasm too 2026-10-01 15:11:13 +02:00
pancake e0a120ad08 Implemented the pointing-hand cursor over flags in HexWidget 2026-10-01 14:33:47 +02:00
pancake 3383cc1d92 Fix disasm glitch 2026-09-30 16:35:21 +02:00
pancake 7eade2df0c Pinch to zoom in the data analysis 2026-09-30 09:43:21 +02:00
pancake bd1105f45d Introduce the hexpat widget 2026-09-29 21:40:24 +02:00
pancake a7532b4d80 Introduce the advanced DataAnalysis widget 2026-09-29 21:30:37 +02:00
pancake 11240a1a0f Optimize list filtering and type combobox 2026-09-28 12:55:31 +02:00
pancake ddb7bdb428 Better tree collapsing behaviours in listings 2026-09-28 12:36:32 +02:00
pancake b912778e62 Better demangling support on listings 2026-09-28 12:20:14 +02:00
pancake dc1fde07f5 Tree/list and shared filter for functions, symbols, imports, exports and flags 2026-09-28 11:56:59 +02:00
pancake 1e089d9c37 Introduce the ExceptionBar for disasm and graph navigation 2026-09-28 11:13:48 +02:00
pancake e0e9690fd5 Add support for collapsing sparse gaps like pxs do in r2 2026-09-28 06:21:59 +02:00
158 changed files with 13507 additions and 945 deletions

No files matched your search

+25 -48
View File
@@ -1,8 +1,8 @@
name: iaito CI
env:
R2V: 6.2.2
R2GHIDRA_VERSION: 6.2.2
R2V: 6.2.6
R2GHIDRA_VERSION: 6.2.6
QT_MAC_INSTALL_VERSION: 6.11.*
QT_WIN_INSTALL_VERSION: 6.10.*
@@ -16,7 +16,7 @@ on:
jobs:
acr-linux-r2git:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
@@ -35,6 +35,8 @@ jobs:
run: |
./configure --prefix=/usr
make -j4
- name: command-line checks
run: python3 tests/commandline/test_commandline.py build/iaito
- name: packaging
run: make -j -C dist/debian
indent-check:
@@ -42,24 +44,14 @@ jobs:
continue-on-error: true
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: 3.12.x
- name: Install indent-check dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgraphviz-dev mesa-common-dev ninja-build libqt5svg5-dev qttools5-dev qttools5-dev-tools qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools
- name: install r2
run: |
wget -q https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2_${{env.R2V}}_amd64.deb
wget -q https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-dev_${{env.R2V}}_amd64.deb
sudo dpkg -i *.deb
- name: configure iaito
run: ./configure --prefix=/usr
persist-credentials: false
- name: install clang-format
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends clang-format-20
- name: make indent
run: make indent && git diff --exit-code
run: make indent-host && git diff --exit-code
acr-linux:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
@@ -82,6 +74,8 @@ jobs:
run: ./configure --prefix=/usr --with-qmake6
- name: build iaito
run: make -j4
- name: command-line checks
run: python3 tests/commandline/test_commandline.py build/iaito
- name: packaging
run: make -C dist/debian
- uses: actions/upload-artifact@v7
@@ -89,7 +83,7 @@ jobs:
name: iaito-amd64.deb
path: dist/debian/*/*.deb
acr-linux-nodebugger:
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
@@ -112,6 +106,8 @@ jobs:
run: ./configure --prefix=/usr --with-qmake6 --without-debugger
- name: build iaito
run: make -j4
- name: command-line checks
run: python3 tests/commandline/test_commandline.py build/iaito
acr-macos:
strategy:
fail-fast: false
@@ -196,46 +192,25 @@ jobs:
name: iaito-macos-${{ matrix.arch }}
path: dist/macos/iaito.zip
meson:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04]
python-version: [3.12.x]
system-deps: [true]
cc-override: [default]
cxx-override: [default]
# Prevent one job from pausing the rest
fail-fast: false
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
submodules: false # 'recursive' 'true' or 'false'
persist-credentials: false
- name: apt dependencies
if: contains(matrix.os, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libgraphviz-dev mesa-common-dev ninja-build libqt5svg5-dev qttools5-dev qttools5-dev-tools qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools
- name: install r2
run: git clone --depth=1 https://github.com/radareorg/radare2 r2git && CFLAGS=-O0 r2git/sys/install.sh
run: |
wget -q https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2_${{env.R2V}}_amd64.deb
wget -q https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-dev_${{env.R2V}}_amd64.deb
sudo dpkg -i *.deb
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: install dependencies
if: contains(matrix.os, 'macos')
run: pip3 install meson ninja
- name: install Qt
if: contains(matrix.os, 'macos')
uses: jurplel/install-qt-action@v4
with:
version: ${{env.QT_MAC_INSTALL_VERSION}}
- name: macos build
if: contains(matrix.os, 'macos')
run: |
meson setup -Dwith_qt6=true build src
ninja -C build -j4
python-version: 3.12.x
- name: meson+ninja ubuntu
if: contains(matrix.os, 'ubuntu')
run: |
pip install meson
meson setup build src
@@ -264,6 +239,8 @@ jobs:
curl -L -o r2.zip https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-${{env.R2V}}-w64.zip
7z x r2.zip
mv radare2-${{env.R2V}}-w64 radare2
# XXX workaround for r2-6.2.4 headers not being msvc c++ friendly (fixed in r2 master)
sed -i 's/(RStrs) { NULL, NULL }/r_strs_new (NULL, NULL)/' radare2/include/libr/r_cmd.h
echo "%CD%\radare2\bin" >> $GITHUB_PATH
- name: configure
shell: cmd
@@ -294,7 +271,7 @@ jobs:
- acr-linux
- acr-macos
- w64-meson
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v7
@@ -414,7 +391,7 @@ jobs:
needs:
- macos-sign
- check_release
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v7
+1 -1
View File
@@ -13,7 +13,7 @@ name: Semgrep
jobs:
semgrep:
name: Scan
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
container:
+3
View File
@@ -3,6 +3,9 @@ AStyle*
./config.mk
config.mk
# Generated configuration header (qmake / Meson)
IaitoConfig.h
# OSX files
*.DS_Store
.AppleDouble
+5 -1
View File
@@ -1,4 +1,4 @@
include config.mk
-include config.mk
ifeq ($(shell uname),Darwin)
BIN=build/iaito.app/Contents/MacOS/iaito
@@ -128,7 +128,11 @@ endif
user-uninstall:
$(MAKE) uninstall DESTDIR=/ PREFIX=${HOME}/.local MANDIR=${HOME}/.local/share/man
ifneq ($(shell uname),Darwin)
run: install-desktop-user
endif
run:
rarun2 libpath=$(shell r2 -H R2_LIBDIR) program=$(BIN)
install-desktop-user:
+3
View File
@@ -83,6 +83,9 @@ make
make run
```
On macOS, `make run` launches the app from `build/iaito.app` without installing it.
Registering the `iaito://` URI handler is optional; see [deep links](doc/deeplinks.md#macos).
To install the app in your home:
```sh
Vendored
+5 -5
View File
@@ -129,12 +129,12 @@ done
: ${INSTALL_PROGRAM_STRIP:=${INSTALL} -m 755 -s}
: ${INSTALL_MAN:=${INSTALL} -m 444}
: ${INSTALL_LIB:=${INSTALL} -m 755 -c}
PKGNAME='iaito' ; VERSION='6.2.2' ; VERSION_MAJOR=6; VERSION_MINOR=2; VERSION_PATCH=2; VERSION_NUMBER=60202; CONTACT_MAIL="[email protected]" ; CONTACT_NAME="pancake" ; CONTACT="pancake <[email protected]>" ;
PKGNAME='iaito' ; VERSION='6.2.6' ; VERSION_MAJOR=6; VERSION_MINOR=2; VERSION_PATCH=6; VERSION_NUMBER=60206; CONTACT_MAIL="[email protected]" ; CONTACT_NAME="pancake" ; CONTACT="pancake <[email protected]>" ;
}
show_usage() {
cat <<EOF2
'configure' configures iaito-6.2.2 to adapt to many kinds of systems.
'configure' configures iaito-6.2.6 to adapt to many kinds of systems.
Usage: ./configure [OPTION]... [VAR=VALUE]...
@@ -212,10 +212,10 @@ ocho() {
show_version() {
if [ "$QUIET" = 1 ]; then
echo "6.2.2"
echo "6.2.6"
exit 0
fi
echo "iaito-6.2.2 configuration script done with acr v2.2.8.
echo "iaito-6.2.6 configuration script done with acr v2.2.8.
The 'Free Software Foundation' message is only for autodetection.
Originally written by pancake <nopcode.org>."
exit 0
@@ -244,7 +244,7 @@ case $flag in
show_version ; ;;
-r|--r|--report)
echo "PKGNAME: iaito"
echo "VERSION: 6.2.2"
echo "VERSION: 6.2.6"
echo "AUTHOR: pancake"
echo "EMAIL: [email protected]"
echo "LANGS: c++"
+1 -1
View File
@@ -1,5 +1,5 @@
PKGNAME iaito
VERSION 6.2.2
VERSION 6.2.6
CONTACT pancake ; [email protected]
LANG_CXX!
+50
View File
@@ -0,0 +1,50 @@
# Data Analysis
Open **View → Data → Analysis**. The dock is hidden in the default layout and when
first added to an older saved layout. Hidden docks do not sample data. **Zoom** is
also available under **View → Data**.
- **Algorithms** enables multiple byte metrics. Hover over a named legend entry
for its definition; click it to edit its color or visibility. Clicking the legend
does not remove the series.
- **Options → Resolution (buckets)** selects 32, 64, 128, 256, 512, 1024, 2048 or
4096 buckets. Lower values give coarser graphs and less work. The default is 1024.
- **Options** also controls lines, filled lines or vertical bars, separate tracks,
greyscale/rainbow/theme colors, custom colors, and structural markers.
- Click the graph to seek. Drag to select bytes. Wheel or `+`/`-` zooms;
Shift+wheel or the arrow keys pans. `Home` fits all data, and `Esc` clears selection.
- The bottom entropy overview shows the complete address span. Drag its window
to pan, drag its edges to resize the range, or double-click to fit all data.
- **Range** fits the current section, segment, map, block or selection.
**Edit range** accepts radare2 expressions and inclusive endpoints.
- **Follow** keeps the current seek address visible. Offset synchronization and
the global address-selection synchronization setting control integration with
disassembly and hexdump widgets.
- **Find marker** searches sections, segments, maps, flags (including search hits)
and comments. The graph context menu provides comments, flags and “Show in”.
Hover details appear after the pointer rests for 650 ms.
- Hexdump selections, sections and segments have direct analysis actions.
Other addressable widgets can use **Show in → Data Analysis**.
The graph uses the current radare2 address space: virtual addresses with `io.va`
enabled, otherwise physical offsets in the active descriptor. Gaps and failed reads
do not contribute invented bytes. Each bucket reads at most 4096 bytes from centered
windows in readable spans. Tooltips, the status line and exported CSV report how
many bytes were actually read. Zoom in for complete coverage of a region. Entropy
is measured per bucket, so very small buckets have a lower attainable entropy.
Multiple overlaid series use a normalized percentage scale. Entropy's native scale
is 0–8 bits/byte, unique bytes 0–256, and mean byte 0–255. A single series or separate
tracks uses its native scale. CSV exports normalized values plus exact bucket bounds
and coverage. Display settings are saved with the layout.
Other widgets can call `MainWindow::showDataAnalysis(start, end)` or
`DataAnalysisWidget::showRange(start, end)` with **inclusive** endpoints. Internally,
`DataAnalysis::Range` and graph signals use half-open ranges.
Run the focused tests with:
```sh
make -C tests/dataanalysis -j8 > /dev/null
QT_QPA_PLATFORM=offscreen tests/dataanalysis/build/test_dataanalysis
```
+7
View File
@@ -210,6 +210,13 @@ The app bundle declares the scheme in `Info.plist` via `CFBundleURLTypes`
the bundle is installed; `scripts/deeplink/register.sh` forces a refresh with
`lsregister -f <iaito.app>`. The handler is dropped when the bundle is removed.
`make run` launches the build without registering the URI handler. To optionally
register the local bundle without installing it, run:
```sh
sh scripts/deeplink/register.sh "$PWD/build/iaito.app"
```
### Windows
`scripts/deeplink/register.bat` writes the `HKCU\Software\Classes\iaito` keys
+30
View File
@@ -0,0 +1,30 @@
# Structures in the hexdump
Open the hexdump sidebar's **Structures** tab, choose an r2 type, enter an address
or expression, and press **Inspect**. **At cursor** uses the first selected byte
or the current cursor. You can also right-click the hexdump and choose **Inspect
structure here**; a type already linked to that address is selected automatically.
The tree shows each field's declared type, value, address, and size. Expand nested
structures, unions, arrays, and pointers to explore them. Selecting a field selects
its bytes in the hexdump. Field colors match the byte highlights. The inspected
structure stays anchored while you navigate, and **Refresh** preserves expanded
fields and the selected row while rereading memory and type definitions.
Types come from r2's type database, including imported C definitions and typedefs.
The tree uses stored member offsets and the current `asm.bits` and `cfg.bigendian`
settings. Large arrays are divided into expandable groups. Unmapped pointers and
recursive references stop expansion; invalid or overflowing layouts are reported.
Array expressions such as `uint16_t[16]` can be inspected directly.
**Pin type** creates an r2 type link (`tl`) at the inspected address. The pinned
address list also includes existing type links and command annotations (`Cr`) from
the current r2 session. Define a named typedef before pinning an array expression.
**Pin command…** stores a command at the address using the selected byte range or
the chosen type's size. Double-click a pin to navigate to its address. Select a
command pin and press **Run command** to display its output. **Unpin** removes only
the selected type link or command annotation. Pins use r2's existing project data
and are saved with the project. iaito also writes `iaito-memory-pins.json` inside the
project directory to preserve types and pins omitted by some r2 binary project versions;
keep that companion file when copying a project.
+32 -4
View File
@@ -4,7 +4,7 @@ TARGET = iaito
IAITO_VERSION_MAJOR = 6
IAITO_VERSION_MINOR = 2
IAITO_VERSION_PATCH = 2
IAITO_VERSION_PATCH = 6
CONFIG += c++20
QMAKE_CXXFLAGS += -std=c++20
@@ -164,6 +164,8 @@ IAITO_R2GHIDRA_STATIC {
QMAKE_SUBSTITUTES += IaitoConfig.h.in
SOURCES += \
common/ExceptionInfo.cpp \
widgets/ExceptionBar.cpp \
Main.cpp \
core/Iaito.cpp \
dialogs/EditStringDialog.cpp \
@@ -204,6 +206,7 @@ SOURCES += \
widgets/ExportsWidget.cpp \
widgets/FlagsWidget.cpp \
widgets/FunctionsWidget.cpp \
widgets/GlobalVariablesWidget.cpp \
widgets/ImportsWidget.cpp \
widgets/CodeEditor.cpp \
widgets/Omnibar.cpp \
@@ -230,6 +233,7 @@ SOURCES += \
common/SvgIconEngine.cpp \
common/SyntaxHighlighter.cpp \
widgets/DecompilerWidget.cpp \
widgets/DecompilerTextEdit.cpp \
widgets/VisualNavbar.cpp \
widgets/GraphView.cpp \
dialogs/settings/SettingsDialog.cpp \
@@ -255,6 +259,7 @@ SOURCES += \
widgets/BinariesWidget.cpp \
widgets/FilesWidget.cpp \
widgets/FilesystemWidget.cpp \
common/SourceLineReference.cpp \
widgets/GraphWidget.cpp \
widgets/OverviewWidget.cpp \
common/JsonTreeItem.cpp \
@@ -314,6 +319,7 @@ SOURCES += \
common/HighDpiPixmap.cpp \
widgets/GraphGridLayout.cpp \
widgets/HexWidget.cpp \
widgets/HexPatWidget.cpp \
widgets/AddressScrollBar.cpp \
widgets/R2AIWidget.cpp \
widgets/R2McpWidget.cpp \
@@ -333,6 +339,8 @@ SOURCES += \
common/R2retdecDecompiler.cpp \
menus/AddressableItemContextMenu.cpp \
common/AddressableItemModel.cpp \
common/NameListModel.cpp \
widgets/NameListView.cpp \
widgets/ListDockWidget.cpp \
dialogs/MultitypeFileSaveDialog.cpp \
widgets/BoolToggleDelegate.cpp \
@@ -355,12 +363,18 @@ SOURCES += \
dialogs/AssemblerDialog.cpp \
dialogs/ScriptManagerDialog.cpp \
dialogs/ScriptManagerWidget.cpp \
widgets/ZoomWidget.cpp
widgets/ZoomWidget.cpp \
common/DataAnalysis.cpp \
common/TypeLayout.cpp \
widgets/DataAnalysisView.cpp \
widgets/DataAnalysisWidget.cpp
GRAPHVIZ_SOURCES = \
widgets/GraphvizLayout.cpp
HEADERS += \
common/ExceptionInfo.h \
widgets/ExceptionBar.h \
common/R2Shims.h \
core/Iaito.h \
core/IaitoCommon.h \
@@ -408,6 +422,7 @@ HEADERS += \
widgets/ExportsWidget.h \
widgets/FlagsWidget.h \
widgets/FunctionsWidget.h \
widgets/GlobalVariablesWidget.h \
widgets/ImportsWidget.h \
widgets/Omnibar.h \
widgets/RelocsWidget.h \
@@ -421,6 +436,7 @@ HEADERS += \
widgets/CodeInfoWidget.h \
menus/DisassemblyContextMenu.h \
menus/DecompilerContextMenu.h \
menus/ContextMenuIcons.h \
menus/ColorPickerMenu.h \
menus/PinPickerMenu.h \
widgets/DisassemblyWidget.h \
@@ -434,6 +450,7 @@ HEADERS += \
common/SvgIconEngine.h \
common/SyntaxHighlighter.h \
widgets/DecompilerWidget.h \
widgets/DecompilerTextEdit.h \
widgets/VisualNavbar.h \
widgets/GraphView.h \
dialogs/settings/SettingsDialog.h \
@@ -459,6 +476,7 @@ HEADERS += \
widgets/BinariesWidget.h \
widgets/FilesWidget.h \
widgets/FilesystemWidget.h \
common/SourceLineReference.h \
widgets/GraphWidget.h \
widgets/OverviewWidget.h \
common/JsonTreeItem.h \
@@ -526,6 +544,8 @@ HEADERS += \
widgets/GraphLayout.h \
widgets/GraphGridLayout.h \
widgets/HexWidget.h \
widgets/HexPatWidget.h \
common/SparseHexLayout.h \
widgets/AddressScrollBar.h \
widgets/R2AIWidget.h \
widgets/R2McpWidget.h \
@@ -542,6 +562,8 @@ HEADERS += \
common/R2retdecDecompiler.h \
menus/AddressableItemContextMenu.h \
common/AddressableItemModel.h \
common/NameListModel.h \
widgets/NameListView.h \
widgets/ListDockWidget.h \
widgets/AddressableItemList.h \
dialogs/MultitypeFileSaveDialog.h \
@@ -567,11 +589,17 @@ HEADERS += \
dialogs/AssemblerDialog.h \
dialogs/ScriptManagerDialog.h \
dialogs/ScriptManagerWidget.h \
widgets/ZoomWidget.h
widgets/ZoomWidget.h \
common/DataAnalysis.h \
common/TypeLayout.h \
widgets/DataAnalysisView.h \
widgets/DataAnalysisWidget.h
GRAPHVIZ_HEADERS = widgets/GraphvizLayout.h
FORMS += \
widgets/GlobalVariablesWidget.ui \
widgets/DataAnalysisWidget.ui \
dialogs/AboutDialog.ui \
dialogs/EditStringDialog.ui \
dialogs/Base64EnDecodedWriteDialog.ui \
@@ -595,6 +623,7 @@ FORMS += \
widgets/FlagsWidget.ui \
widgets/StringsWidget.ui \
widgets/HexdumpWidget.ui \
widgets/HexPatWidget.ui \
dialogs/SaveProjectDialog.ui \
dialogs/settings/SettingsDialog.ui \
dialogs/settings/AppearanceOptionsWidget.ui \
@@ -627,7 +656,6 @@ FORMS += \
dialogs/SetToDataDialog.ui \
dialogs/EditVariablesDialog.ui \
widgets/IaitoTreeView.ui \
widgets/ComboQuickFilterView.ui \
dialogs/HexdumpRangeDialog.ui \
dialogs/WelcomeDialog.ui \
dialogs/EditMethodDialog.ui \
+16 -6
View File
@@ -172,8 +172,7 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
#endif
#endif
if (!parseCommandLineOptions()) {
QCoreApplication::exit();
// std::exit(1);
return;
}
if (!versionCheck()) {
@@ -338,9 +337,11 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
IaitoApplication::~IaitoApplication()
{
SamplesDB::waitForPending();
Plugins()->destroyPlugins();
delete mainWindow;
if (mainWindow) {
SamplesDB::waitForPending();
Plugins()->destroyPlugins();
delete mainWindow;
}
}
void IaitoApplication::launchNewInstance(const QStringList &args)
@@ -535,7 +536,16 @@ bool IaitoApplication::parseCommandLineOptions()
cmd_parser.addOption(debugOption);
#endif
cmd_parser.process(*this);
// process() exits without destroying QApplication for --version.
const bool parsed = cmd_parser.parse(arguments());
if (parsed && cmd_parser.isSet("version")) {
printf("%s %s\n", qPrintable(applicationName()), qPrintable(applicationVersion()));
exitCode = 0;
return false;
}
if (!parsed || cmd_parser.isSet("help") || cmd_parser.isSet("help-all")) {
cmd_parser.process(*this);
}
IaitoCommandLineOptions opts;
opts.args = cmd_parser.positionalArguments();
+3 -1
View File
@@ -30,6 +30,7 @@ public:
~IaitoApplication();
MainWindow *getMainWindow() { return mainWindow; }
int getExitCode() const { return exitCode; }
void launchNewInstance(const QStringList &args = {});
@@ -51,7 +52,8 @@ private:
private:
bool m_FileAlreadyDropped;
MainWindow *mainWindow;
MainWindow *mainWindow = nullptr;
int exitCode = 1;
IaitoCommandLineOptions clOptions;
};
+3
View File
@@ -154,6 +154,9 @@ int main(int argc, char *argv[])
#endif
IaitoApplication a(argc, argv);
if (!a.getMainWindow()) {
return a.getExitCode();
}
return a.exec();
}
+46 -4
View File
@@ -12,17 +12,20 @@ AddressableFilterProxyModel::AddressableFilterProxyModel(
: AddressableItemModel<QSortFilterProxyModel>(parent)
{
setSourceModel(sourceModel);
addressableSourceModel = sourceModel;
}
RVA AddressableFilterProxyModel::address(const QModelIndex &index) const
{
return addressableSourceModel->address(this->mapToSource(index));
const auto source = mapToSource(index);
return addressableSourceModel && source.isValid() ? addressableSourceModel->address(source)
: RVA_INVALID;
}
QString AddressableFilterProxyModel::name(const QModelIndex &index) const
{
return addressableSourceModel->name(this->mapToSource(index));
const auto source = mapToSource(index);
return addressableSourceModel && source.isValid() ? addressableSourceModel->name(source)
: QString();
}
void AddressableFilterProxyModel::setSourceModel(QAbstractItemModel *)
@@ -32,6 +35,45 @@ void AddressableFilterProxyModel::setSourceModel(QAbstractItemModel *)
void AddressableFilterProxyModel::setSourceModel(AddressableItemModelI *sourceModel)
{
ParentClass::setSourceModel(sourceModel->asItemModel());
addressableSourceModel = sourceModel;
ParentClass::setSourceModel(sourceModel ? sourceModel->asItemModel() : nullptr);
}
void AddressableFilterProxyModel::setTypeFilterColumn(int column)
{
if (typeColumn != column) {
emit filterAboutToChange();
typeColumn = column;
invalidateFilter();
emit filterChanged();
}
}
void AddressableFilterProxyModel::setTypeFilter(const QVariant &type)
{
if (selectedType != type) {
emit filterAboutToChange();
selectedType = type;
invalidateFilter();
emit filterChanged();
}
}
void AddressableFilterProxyModel::setFilterWildcard(const QString &pattern)
{
emit filterAboutToChange();
ParentClass::setFilterWildcard(pattern);
emit filterChanged();
}
bool AddressableFilterProxyModel::filterAcceptsType(int row, const QModelIndex &parent) const
{
return typeColumn < 0 || !selectedType.isValid()
|| sourceModel()->index(row, typeColumn, parent).data().toString()
== selectedType.toString();
}
bool AddressableFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
return filterAcceptsType(row, parent) && ParentClass::filterAcceptsRow(row, parent);
}
+19 -1
View File
@@ -43,6 +43,8 @@ public:
class IAITO_EXPORT AddressableFilterProxyModel : public AddressableItemModel<QSortFilterProxyModel>
{
Q_OBJECT
using ParentClass = AddressableItemModel<QSortFilterProxyModel>;
public:
@@ -51,10 +53,26 @@ public:
RVA address(const QModelIndex &index) const override;
QString name(const QModelIndex &) const override;
void setSourceModel(AddressableItemModelI *sourceModel);
void setTypeFilterColumn(int column);
void setTypeFilter(const QVariant &type);
QVariant typeFilter() const { return selectedType; }
public slots:
void setFilterWildcard(const QString &pattern);
signals:
void filterAboutToChange();
void filterChanged();
protected:
bool filterAcceptsRow(int row, const QModelIndex &parent) const override;
bool filterAcceptsType(int row, const QModelIndex &parent) const;
private:
void setSourceModel(QAbstractItemModel *sourceModel) override; // Don't use this directly
AddressableItemModelI *addressableSourceModel;
AddressableItemModelI *addressableSourceModel = nullptr;
int typeColumn = -1;
QVariant selectedType;
};
#endif // ADDRESSABLEITEMMODEL_H
+15 -4
View File
@@ -687,14 +687,15 @@ void Configuration::setVisualNavbarResolution(int divisor)
Configuration::MemoryScrollBarMode Configuration::getMemoryScrollBarMode() const
{
const int mode
= s.value("memoryScrollBarMode", static_cast<int>(MemoryScrollBarMode::Hidden)).toInt();
= s.value("memoryScrollBarMode", static_cast<int>(MemoryScrollBarMode::Bounded)).toInt();
switch (mode) {
case static_cast<int>(MemoryScrollBarMode::Spring):
return MemoryScrollBarMode::Spring;
case static_cast<int>(MemoryScrollBarMode::Bounded):
return MemoryScrollBarMode::Bounded;
default:
case static_cast<int>(MemoryScrollBarMode::Hidden):
return MemoryScrollBarMode::Hidden;
case static_cast<int>(MemoryScrollBarMode::Bounded):
default:
return MemoryScrollBarMode::Bounded;
}
}
@@ -1163,6 +1164,16 @@ void Configuration::setDecompilerShowAddresses(bool enabled)
s.setValue("decompilerShowAddresses", enabled);
}
bool Configuration::getDecompilerShowDisassembly()
{
return s.value("decompilerShowDisassembly", false).toBool();
}
void Configuration::setDecompilerShowDisassembly(bool enabled)
{
s.setValue("decompilerShowDisassembly", enabled);
}
bool Configuration::getBitmapTransparentState()
{
return s.value("bitmapGraphExportTransparency", false).value<bool>();
+2
View File
@@ -236,6 +236,8 @@ public:
void setDecompilerRunInBackground(bool enabled);
bool getDecompilerShowAddresses();
void setDecompilerShowAddresses(bool enabled);
bool getDecompilerShowDisassembly();
void setDecompilerShowDisassembly(bool enabled);
bool getAddressRangeSelectionSyncEnabled() const;
void setAddressRangeSelectionSyncEnabled(bool enabled);
+122
View File
@@ -0,0 +1,122 @@
#include "DataAnalysis.h"
#include <algorithm>
#include <cmath>
namespace DataAnalysis {
Range sizedRange(RVA start, RVA size)
{
return {start, start + qMin(size, RVA_MAX - start)};
}
Range intersect(Range a, Range b)
{
const RVA start = qMax(a.start, b.start);
return {start, qMax(start, qMin(a.end, b.end))};
}
Range bucket(Range range, int index, int count)
{
if (!range.valid() || count <= 0 || index < 0 || index >= count) {
return {};
}
// Avoid both floating point address rounding and multiplication overflow.
auto boundary = [range, count](RVA i) {
return range.start + (range.size() / count) * i + (range.size() % count) * i / count;
};
return {boundary(index), boundary(index + 1)};
}
Range centered(Range domain, RVA address, RVA size)
{
if (!domain.valid()) {
return {};
}
size = qBound<RVA>(1, size, domain.size());
address = qBound(domain.start, address, domain.end - 1);
const RVA start = address - qMin(address - domain.start, size / 2);
const RVA clamped = qMin(start, domain.end - size);
return {clamped, clamped + size};
}
QVector<Range> mergeRanges(QVector<Range> ranges)
{
std::sort(ranges.begin(), ranges.end(), [](Range a, Range b) { return a.start < b.start; });
QVector<Range> result;
for (auto range : ranges) {
if (!range.valid()) {
continue;
}
if (result.isEmpty() || result.last().end < range.start) {
result.append(range);
} else {
result.last().end = qMax(result.last().end, range.end);
}
}
return result;
}
Sample sample(
Range range,
const QVector<Range> &readable,
const std::function<QByteArray(RVA, int)> &read,
int budget)
{
Sample result;
result.range = range;
if (!range.valid() || budget <= 0) {
return result;
}
QVector<Range> spans;
for (auto candidate : readable) {
const Range span = intersect(range, candidate);
if (span.valid()) {
spans.append(span);
result.covered += span.size();
}
}
std::array<int, 256> histogram{};
RVA remaining = result.covered;
for (auto span : spans) {
if (budget <= 0) {
break;
}
// Allocate the budget proportionally among disjoint readable spans.
const int share = qMax(1, int((long double) span.size() / remaining * budget));
const int length = int(qMin<RVA>(span.size(), qMin(budget, share)));
const RVA address = span.start + (span.size() - length) / 2;
const QByteArray data = read(address, length).left(length);
for (unsigned char byte : data) {
++histogram[byte];
}
result.bytes += data.size();
budget -= length;
remaining -= span.size();
}
if (!result.bytes) {
return result;
}
auto &v = result.values;
for (int byte = 0; byte < 256; ++byte) {
const int n = histogram[byte];
if (!n) {
continue;
}
const double p = double(n) / result.bytes;
v[Entropy] -= p * std::log2(p) / 8.0;
v[Printable] += (byte >= 32 && byte <= 126) ? p : 0;
v[Zero] += byte == 0 ? p : 0;
v[FF] += byte == 255 ? p : 0;
v[Unique] += 1.0 / 256;
v[Mean] += p * byte / 255;
v[Alpha] += ((byte >= 'a' && byte <= 'z') || (byte >= 'A' && byte <= 'Z')) ? p : 0;
v[Digit] += (byte >= '0' && byte <= '9') ? p : 0;
v[Space] += (byte == ' ' || (byte >= 9 && byte <= 13)) ? p : 0;
v[Control] += (byte < 32 || byte == 127) ? p : 0;
v[High] += byte >= 128 ? p : 0;
}
return result;
}
} // namespace DataAnalysis
+64
View File
@@ -0,0 +1,64 @@
#ifndef DATAANALYSIS_H
#define DATAANALYSIS_H
#include "core/IaitoCommon.h"
#include <array>
#include <functional>
#include <QByteArray>
#include <QVector>
namespace DataAnalysis {
// Half-open ranges, including when the exclusive end is RVA_MAX.
struct Range
{
RVA start = 0;
RVA end = 0;
bool valid() const { return start < end; }
RVA size() const { return end - start; }
bool contains(RVA address) const { return start <= address && address < end; }
bool operator==(const Range &other) const { return start == other.start && end == other.end; }
bool operator!=(const Range &other) const { return !(*this == other); }
};
enum Metric {
Entropy,
Printable,
Zero,
FF,
Unique,
Mean,
Alpha,
Digit,
Space,
Control,
High,
Count
};
struct Sample
{
Range range;
RVA covered = 0;
int bytes = 0;
std::array<double, Count> values{}; // Normalized to [0, 1]; entropy / 8.
};
Range sizedRange(RVA start, RVA size);
Range intersect(Range a, Range b);
Range bucket(Range range, int index, int count);
Range centered(Range domain, RVA address, RVA size);
QVector<Range> mergeRanges(QVector<Range> ranges);
// Bounded, deterministic sampling. Reads only known readable spans; missing bytes
// never contribute fabricated 0xff data. Large buckets use centered windows.
Sample sample(
Range range,
const QVector<Range> &readable,
const std::function<QByteArray(RVA, int)> &read,
int budget = 4096);
} // namespace DataAnalysis
#endif
+1 -1
View File
@@ -337,7 +337,7 @@ QString linkAt(const QString &line, int posInLine)
auto it = re.globalMatch(line);
while (it.hasNext()) {
const QRegularExpressionMatch m = it.next();
if (posInLine >= m.capturedStart() && posInLine <= m.capturedEnd()) {
if (posInLine >= m.capturedStart() && posInLine < m.capturedEnd()) {
return m.captured();
}
}
+124
View File
@@ -0,0 +1,124 @@
#include "ExceptionInfo.h"
#include <cmath>
#include <QCoreApplication>
#include <QJsonObject>
#include <QVariant>
static QString exceptionTr(const char *text)
{
return QCoreApplication::translate("ExceptionInfo", text);
}
static quint64 exceptionAddress(const QJsonValue &value)
{
bool ok = false;
quint64 result = ExceptionRegion::Invalid;
if (value.isString()) {
result = value.toString().toULongLong(&ok, 0);
} else if (
value.isDouble() && value.toDouble() >= 0
&& std::floor(value.toDouble()) == value.toDouble()) {
result = value.toVariant().toULongLong(&ok);
}
return ok ? result : ExceptionRegion::Invalid;
}
bool ExceptionRegion::protects(quint64 address, quint64 size) const
{
// Subtraction avoids overflowing a block's end near the address-space limit.
return size && address < to && (address >= from || from - address < size);
}
bool ExceptionRegion::touches(quint64 address, quint64 size) const
{
return protects(address, size)
|| (handler != Invalid && handler >= address && handler - address < size);
}
QString ExceptionRegion::label() const
{
if (kind == QStringLiteral("cleanup")) {
return exceptionTr("Cleanup / finally");
}
const QString name = kind == QStringLiteral("filter") ? exceptionTr("Filter")
: exceptionTr("Catch");
return QStringLiteral("%1 (%2)").arg(
name,
catchAll ? exceptionTr("all exceptions")
: type.isEmpty() ? exceptionTr("unknown type")
: type);
}
QString ExceptionRegion::description() const
{
QString text = exceptionTr("#%1 · %2\nProtected: [0x%3, 0x%4)\nHandler: 0x%5")
.arg(index)
.arg(label())
.arg(from, 0, 16)
.arg(to, 0, 16)
.arg(handler, 0, 16);
if (source != Invalid) {
text += exceptionTr("\nMetadata source: 0x%1").arg(source, 0, 16);
}
if (filter && filter != Invalid) {
text += exceptionTr("\nFilter value / address: 0x%1").arg(filter, 0, 16);
}
if (typeFilter) {
text += exceptionTr("\nType filter: %1").arg(typeFilter);
}
return text;
}
QColor ExceptionRegion::color() const
{
// Stable across views and independent of metadata order; shared ranges share a color.
const quint64 hash = (from >> 2) ^ (to >> 1) ^ from;
return QColor::fromHsv(int(hash % 12) * 30, 145, 210);
}
QList<ExceptionRegion> ExceptionRegion::parse(const QJsonArray &array)
{
QList<ExceptionRegion> result;
for (int i = 0; i < array.size(); ++i) {
const auto object = array.at(i).toObject();
ExceptionRegion region;
region.from = exceptionAddress(object.value(QStringLiteral("from")));
region.to = exceptionAddress(object.value(QStringLiteral("to")));
region.handler = exceptionAddress(object.value(QStringLiteral("handler")));
if (region.from == Invalid || region.to == Invalid || region.from >= region.to
|| region.handler == Invalid) {
continue;
}
region.index = exceptionAddress(object.value(QStringLiteral("index")));
if (region.index == Invalid) {
region.index = quint64(i);
}
region.source = exceptionAddress(object.value(QStringLiteral("source")));
region.filter = exceptionAddress(object.value(QStringLiteral("filter")));
region.kind = object.value(QStringLiteral("kind")).toString();
region.type = object.value(QStringLiteral("type")).toString();
region.typeFilter = object.value(QStringLiteral("typeFilter")).toVariant().toLongLong();
region.catchAll = object.value(QStringLiteral("catchAll")).toBool();
result.append(region);
}
return result;
}
QList<ExceptionRegion> ExceptionRegion::forBlocks(
const QList<ExceptionRegion> &regions, const QJsonArray &blocks)
{
QList<ExceptionRegion> result;
for (const auto &region : regions) {
for (const auto &value : blocks) {
const auto block = value.toObject();
const auto address = exceptionAddress(block.value(QStringLiteral("addr")));
const auto size = exceptionAddress(block.value(QStringLiteral("size")));
if (address != Invalid && size != Invalid && region.touches(address, size)) {
result.append(region);
break;
}
}
}
return result;
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef EXCEPTIONINFO_H
#define EXCEPTIONINFO_H
#include <limits>
#include <QColor>
#include <QJsonArray>
#include <QList>
#include <QString>
// Addresses in iwj use half-open ranges: [from, to).
struct ExceptionRegion
{
static constexpr quint64 Invalid = std::numeric_limits<quint64>::max();
quint64 index = 0;
quint64 source = Invalid;
quint64 from = Invalid;
quint64 to = Invalid;
quint64 handler = Invalid;
quint64 filter = 0;
QString kind;
QString type;
qint64 typeFilter = 0;
bool catchAll = false;
bool contains(quint64 address) const { return from <= address && address < to; }
bool protects(quint64 address, quint64 size) const;
bool touches(quint64 address, quint64 size) const;
QString label() const;
QString description() const;
QColor color() const;
static QList<ExceptionRegion> parse(const QJsonArray &array);
static QList<ExceptionRegion> forBlocks(
const QList<ExceptionRegion> &regions, const QJsonArray &blocks);
};
#endif
+74 -13
View File
@@ -1,11 +1,9 @@
#include "IaitoSeekable.h"
#include "core/MainWindow.h"
#include <QPlainTextEdit>
IaitoSeekable::IaitoSeekable(QObject *parent)
: QObject(parent)
{
widgetOffset = Core()->getOffset();
connect(Core(), &IaitoCore::seekChanged, this, &IaitoSeekable::onCoreSeekChanged);
}
@@ -13,35 +11,98 @@ IaitoSeekable::~IaitoSeekable() {}
void IaitoSeekable::setSynchronization(bool sync)
{
if (synchronized == sync) {
return;
}
const RVA address = Core()->getOffset();
synchronized = sync;
onCoreSeekChanged(Core()->getOffset());
seekHistory = {address};
historyIndex = 0;
updateSeek(address);
emit syncChanged();
emit historyChanged();
}
void IaitoSeekable::onCoreSeekChanged(RVA addr)
{
if (synchronized && widgetOffset != addr) {
updateSeek(addr, true);
if (synchronized) {
updateSeek(addr);
emit historyChanged();
}
}
void IaitoSeekable::updateSeek(RVA addr, bool localOnly)
void IaitoSeekable::seek(RVA addr)
{
previousOffset = widgetOffset;
widgetOffset = addr;
if (synchronized && !localOnly) {
Core()->seek(addr);
if (addr == RVA_INVALID) {
return;
}
if (synchronized) {
Core()->seek(addr);
return;
}
if (addr == widgetOffset) {
return;
}
seekHistory.resize(historyIndex + 1);
seekHistory.append(addr);
++historyIndex;
updateSeek(addr);
emit historyChanged();
}
void IaitoSeekable::updateSeek(RVA addr)
{
if (addr == RVA_INVALID || addr == widgetOffset) {
return;
}
widgetOffset = addr;
emit seekableSeekChanged(addr);
}
bool IaitoSeekable::canSeekPrev() const
{
if (synchronized) {
return Core()->core()->io->undo.undos > 0;
}
return historyIndex > 0;
}
bool IaitoSeekable::canSeekNext() const
{
if (synchronized) {
return Core()->core()->io->undo.redos > 0;
}
return historyIndex + 1 < seekHistory.size();
}
QVector<RVA> IaitoSeekable::localHistory(bool forward) const
{
QVector<RVA> result;
if (!synchronized) {
for (int i = historyIndex; i >= 0 && i < seekHistory.size(); i += forward ? 1 : -1) {
result.append(seekHistory.at(i));
}
}
return result;
}
void IaitoSeekable::seekPrev()
{
if (synchronized) {
Core()->seekPrev();
} else {
this->seek(previousOffset);
} else if (canSeekPrev()) {
updateSeek(seekHistory.at(--historyIndex));
emit historyChanged();
}
}
void IaitoSeekable::seekNext()
{
if (synchronized) {
Core()->seekNext();
} else if (canSeekNext()) {
updateSeek(seekHistory.at(++historyIndex));
emit historyChanged();
}
}
+11 -9
View File
@@ -1,6 +1,7 @@
#pragma once
#include "core/Iaito.h"
#include <QVector>
class MainWindow;
@@ -20,7 +21,7 @@ public:
* In any case, IaitoSeekable::seekableSeekChanged is emitted.
* @param addr the location to seek at.
*/
void seek(RVA addr) { updateSeek(addr, false); }
void seek(RVA addr);
/**
* @brief setSynchronization sets
@@ -44,6 +45,10 @@ public:
*/
bool isSynchronized();
bool canSeekPrev() const;
bool canSeekNext() const;
QVector<RVA> localHistory(bool forward) const;
/**
* @brief seekToReference will seek to the function or the object which is
* referenced in a given offset
@@ -56,6 +61,7 @@ public slots:
* @brief seekPrev seeks to last location.
*/
void seekPrev();
void seekNext();
/**
* @brief toggleSyncWithCore toggles
@@ -75,11 +81,8 @@ private:
*/
RVA widgetOffset = RVA_INVALID;
/**
* @brief previousOffset last seek location.
* @todo maybe use an actual history?
*/
RVA previousOffset = RVA_INVALID;
QVector<RVA> seekHistory;
int historyIndex = 0;
/**
* @brief synchronized tells with the seekable's offset is
@@ -89,12 +92,11 @@ private:
/**
* @brief internal method for changing the seek
* @param localOnly whether the seek should be updated globally if
* synchronized
*/
void updateSeek(RVA addr, bool localOnly);
void updateSeek(RVA addr);
signals:
void seekableSeekChanged(RVA addr);
void syncChanged();
void historyChanged();
};
+436
View File
@@ -0,0 +1,436 @@
#include "NameListModel.h"
#include <QStringList>
namespace {
QString compactName(const QString &name, const QString &previous)
{
int prefix = 0;
const int limit = qMin(name.size(), previous.size());
while (prefix < limit && name.at(prefix) == previous.at(prefix)) {
++prefix;
}
if (prefix > 0 && prefix < name.size() && name.at(prefix).isLowSurrogate()) {
--prefix;
}
return prefix > 2 && prefix < name.size() ? QStringLiteral("▶ ") + name.mid(prefix) : name;
}
QStringList nameParts(const QString &name)
{
QStringList parts;
int start = 0;
int templates = 0;
int parentheses = 0;
int brackets = 0;
for (int i = 0; i < name.size(); ++i) {
const QChar c = name.at(i);
// Operators and their signatures belong to the leaf, including operator< and operator().
if (!templates && !parentheses && i == start && name.mid(i, 8) == QLatin1String("operator")
&& (i + 8 == name.size()
|| (!name.at(i + 8).isLetterOrNumber() && name.at(i + 8) != QLatin1Char('_')))) {
break;
}
if (c == QLatin1Char('<')) {
++templates;
} else if (c == QLatin1Char('>') && templates) {
--templates;
} else if (c == QLatin1Char('(')) {
++parentheses;
} else if (c == QLatin1Char(')') && parentheses) {
--parentheses;
} else if (c == QLatin1Char('[')) {
++brackets;
} else if (c == QLatin1Char(']') && brackets) {
--brackets;
}
if (templates || parentheses || brackets) {
continue;
}
int separator = 0;
if (c == QLatin1Char(':') && i + 1 < name.size() && name.at(i + 1) == c) {
separator = 2;
} else if (c == QLatin1Char('-') && i + 1 < name.size() && name.at(i + 1) == QLatin1Char('>')) {
separator = 2;
} else if (c == QLatin1Char('.') || c == QLatin1Char('/') || c == QLatin1Char('$')) {
separator = 1;
}
if (separator) {
i += separator - 1;
if (i + 1 < name.size()) {
parts.append(name.mid(start, i + 1 - start));
start = i + 1;
}
}
}
parts.append(name.mid(start));
return parts;
}
} // namespace
NameListModel::NameListModel(AddressableItemModelI *source, int nameColumn, QObject *parent)
: AddressableItemModel<QAbstractProxyModel>(parent)
, nameColumn(nameColumn)
{
setSourceModel(source->asItemModel());
}
NameListModel::~NameListModel() = default;
void NameListModel::setSourceModel(QAbstractItemModel *source)
{
beginRebuild();
if (sourceModel()) {
disconnect(sourceModel(), nullptr, this, nullptr);
}
QAbstractProxyModel::setSourceModel(source);
sourceProvider = dynamic_cast<AddressableItemModelI *>(source);
if (source) {
if (auto filter = qobject_cast<AddressableFilterProxyModel *>(source)) {
// QSortFilterProxyModel can remove thousands of disjoint row ranges for one query.
// Rebuild the presentation once, after the entire filter operation has finished.
connect(filter, &AddressableFilterProxyModel::filterAboutToChange, this, [this] {
++filterChangeDepth;
beginRebuild();
});
connect(filter, &AddressableFilterProxyModel::filterChanged, this, [this] {
--filterChangeDepth;
endRebuild();
});
}
connect(source, &QAbstractItemModel::modelAboutToBeReset, this, &NameListModel::beginRebuild);
connect(source, &QAbstractItemModel::modelReset, this, &NameListModel::endRebuild);
connect(
source, &QAbstractItemModel::layoutAboutToBeChanged, this, &NameListModel::beginRebuild);
connect(source, &QAbstractItemModel::layoutChanged, this, &NameListModel::endRebuild);
connect(
source, &QAbstractItemModel::rowsAboutToBeInserted, this, &NameListModel::beginRebuild);
connect(source, &QAbstractItemModel::rowsInserted, this, &NameListModel::endRebuild);
connect(source, &QAbstractItemModel::rowsAboutToBeRemoved, this, &NameListModel::beginRebuild);
connect(source, &QAbstractItemModel::rowsRemoved, this, &NameListModel::endRebuild);
connect(source, &QAbstractItemModel::rowsAboutToBeMoved, this, &NameListModel::beginRebuild);
connect(source, &QAbstractItemModel::rowsMoved, this, &NameListModel::endRebuild);
connect(source, &QAbstractItemModel::dataChanged, this, &NameListModel::sourceDataChanged);
connect(
source, &QAbstractItemModel::headerDataChanged, this, &NameListModel::headerDataChanged);
connect(source, &QObject::destroyed, this, [this] {
sourceProvider = nullptr;
beginRebuild();
endRebuild();
});
}
endRebuild();
}
NameListModel::Node *NameListModel::node(const QModelIndex &index) const
{
return index.isValid() ? static_cast<Node *>(index.internalPointer()) : nullptr;
}
QModelIndex NameListModel::nodeIndex(Node *item, int column) const
{
return item && item != &root ? createIndex(item->row, column, item) : QModelIndex();
}
QModelIndex NameListModel::index(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || column < 0 || column >= columnCount() || (parent.isValid() && parent.column())) {
return {};
}
const Node *item = parent.isValid() ? node(parent) : &root;
return row < int(item->children.size()) ? nodeIndex(item->children[row].get(), column)
: QModelIndex();
}
QModelIndex NameListModel::parent(const QModelIndex &index) const
{
auto item = node(index);
return item ? nodeIndex(item->parent) : QModelIndex();
}
int NameListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid() && parent.column()) {
return 0;
}
const Node *item = parent.isValid() ? node(parent) : &root;
return int(item->children.size());
}
int NameListModel::columnCount(const QModelIndex &) const
{
return sourceProvider ? sourceModel()->columnCount() : 0;
}
bool NameListModel::hasChildren(const QModelIndex &parent) const
{
return rowCount(parent) > 0;
}
QModelIndex NameListModel::mapToSource(const QModelIndex &index) const
{
auto item = node(index);
return sourceProvider && item && item->source.isValid()
? item->source.sibling(item->source.row(), index.column())
: QModelIndex();
}
QModelIndex NameListModel::mapFromSource(const QModelIndex &index) const
{
return index.isValid()
? nodeIndex(sourceNodes.value(index.sibling(index.row(), 0)), index.column())
: QModelIndex();
}
QVariant NameListModel::data(const QModelIndex &index, int role) const
{
auto item = node(index);
if (!item) {
return {};
}
if (!item->source.isValid()) {
if (role == GroupPathRole) {
return item->groupPath;
}
if (index.column() == nameColumn) {
if (role == Qt::DisplayRole) {
return item->label;
}
if (role == Qt::ToolTipRole) {
return QStringLiteral("<qt>%1</qt>").arg(item->groupPath.toHtmlEscaped());
}
}
return {};
}
if (index.column() == nameColumn && item->named) {
if (role == Qt::ToolTipRole) {
const auto tooltip = mapToSource(index).data(NameToolTipRole);
if (tooltip.isValid()) {
return tooltip;
}
return QStringLiteral("<qt>%1</qt>").arg(item->fullName.toHtmlEscaped());
}
if (role == Qt::DisplayRole) {
QString label = item->label;
if (compact && item->row > 0) {
auto previous = item->parent->children[item->row - 1].get();
if (previous->named) {
label = compactName(label, previous->label);
}
}
const QString original = mapToSource(index).data().toString();
// Keep decorations represented as text, such as a function's pin emoji.
if (original.endsWith(item->fullName)) {
return original.left(original.size() - item->fullName.size()) + label;
}
return label;
}
}
return mapToSource(index).data(role);
}
Qt::ItemFlags NameListModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
auto source = mapToSource(index);
return source.isValid() ? source.flags() : Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
QVariant NameListModel::headerData(int section, Qt::Orientation orientation, int role) const
{
return sourceProvider ? sourceModel()->headerData(section, orientation, role) : QVariant();
}
void NameListModel::sort(int column, Qt::SortOrder order)
{
if (sourceProvider) {
sourceModel()->sort(column, order);
}
}
RVA NameListModel::address(const QModelIndex &index) const
{
const auto source = mapToSource(index);
return sourceProvider && source.isValid() ? sourceProvider->address(source) : RVA_INVALID;
}
QString NameListModel::name(const QModelIndex &index) const
{
const auto source = mapToSource(index);
return sourceProvider && source.isValid() ? sourceProvider->name(source) : QString();
}
void NameListModel::beginRebuild()
{
if (!resetting) {
resetting = true;
beginResetModel();
}
}
void NameListModel::endRebuild()
{
if (resetting && !filterChangeDepth) {
rebuild();
resetting = false;
endResetModel();
}
}
void NameListModel::appendSource(
Node *parent, const QModelIndex &source, const QString &label, bool named)
{
auto item = std::make_unique<Node>();
item->parent = parent;
item->row = int(parent->children.size());
item->source = source;
item->label = label;
item->named = named;
if (named) {
item->fullName = sourceProvider->name(source);
}
auto ptr = item.get();
sourceNodes.insert(source, ptr);
parent->children.push_back(std::move(item));
// Preserve existing class/member relationships and unnamed function detail rows.
if (sourceModel()->hasChildren(source)) {
for (int row = 0; row < sourceModel()->rowCount(source); ++row) {
auto child = sourceModel()->index(row, 0, source);
QString childName = sourceProvider->name(child);
const bool childNamed = !childName.isEmpty() && childName != ptr->fullName;
if (childNamed) {
for (const QString &separator :
{QStringLiteral("::"),
QStringLiteral("."),
QStringLiteral("/"),
QStringLiteral("$"),
QStringLiteral("->")}) {
const QString prefix = ptr->fullName + separator;
if (childName.startsWith(prefix)) {
childName.remove(0, prefix.size());
break;
}
}
}
appendSource(ptr, child, childName, childNamed);
}
}
}
void NameListModel::rebuild()
{
sourceNodes.clear();
groups.clear();
root.children.clear();
if (!sourceProvider) {
return;
}
for (int row = 0; row < sourceModel()->rowCount(); ++row) {
auto source = sourceModel()->index(row, 0);
const QString fullName = sourceProvider->name(source);
Node *parent = &root;
QString label = fullName;
if (hierarchy) {
const auto parts = nameParts(fullName);
QString path;
for (int part = 0; part + 1 < parts.size(); ++part) {
path += parts[part];
auto group = groups.value(path);
if (!group) {
auto item = std::make_unique<Node>();
item->parent = parent;
item->row = int(parent->children.size());
item->label = parts[part];
item->groupPath = path;
group = item.get();
parent->children.push_back(std::move(item));
groups.insert(path, group);
}
parent = group;
}
label = parts.last();
}
appendSource(parent, source, label, true);
}
collapseSingleGroups(&root);
}
void NameListModel::collapseSingleGroups(Node *parent)
{
for (auto &child : parent->children) {
if (child->source.isValid()) {
continue; // Class/member relationships belong to the source model.
}
collapseSingleGroups(child.get());
if (child->children.size() == 1 && child->children.front()->source.isValid()) {
auto group = std::move(child);
child = std::move(group->children.front());
child->label.prepend(group->label);
child->parent = parent;
child->row = group->row;
groups.remove(group->groupPath);
}
}
}
void NameListModel::sourceDataChanged(const QModelIndex &first, const QModelIndex &last)
{
if (resetting) {
return;
}
for (int row = first.row(); row <= last.row(); ++row) {
auto source = sourceModel()->index(row, 0, first.parent());
auto item = sourceNodes.value(source);
if (item && item->named && item->fullName != sourceProvider->name(source)) {
beginRebuild();
endRebuild();
return;
}
}
for (int row = first.row(); row <= last.row(); ++row) {
auto source = sourceModel()->index(row, 0, first.parent());
auto item = sourceNodes.value(source);
if (item) {
emit dataChanged(nodeIndex(item, first.column()), nodeIndex(item, last.column()));
}
}
}
void NameListModel::namesChanged(Node *parent)
{
if (!parent->children.empty()) {
emit dataChanged(
nodeIndex(parent->children.front().get(), nameColumn),
nodeIndex(parent->children.back().get(), nameColumn),
{Qt::DisplayRole});
for (const auto &child : parent->children) {
namesChanged(child.get());
}
}
}
void NameListModel::setCompact(bool enabled)
{
if (compact != enabled) {
compact = enabled;
namesChanged(&root);
}
}
void NameListModel::setHierarchy(bool enabled)
{
if (hierarchy != enabled) {
beginRebuild();
hierarchy = enabled;
endRebuild();
}
}
QModelIndex NameListModel::groupIndex(const QString &path) const
{
return nodeIndex(groups.value(path));
}
+79
View File
@@ -0,0 +1,79 @@
#ifndef NAMELISTMODEL_H
#define NAMELISTMODEL_H
#include "AddressableItemModel.h"
#include <memory>
#include <vector>
#include <QAbstractProxyModel>
#include <QHash>
// Presentation only: all addresses, descriptions and edits belong to the source model.
class IAITO_EXPORT NameListModel : public AddressableItemModel<QAbstractProxyModel>
{
public:
enum {
GroupPathRole = Qt::UserRole + 0x1000,
// A complete, HTML-escaped name tooltip supplied by models with alternate spellings.
NameToolTipRole
};
NameListModel(AddressableItemModelI *source, int nameColumn, QObject *parent = nullptr);
~NameListModel() override;
void setSourceModel(QAbstractItemModel *source) override;
QModelIndex mapToSource(const QModelIndex &index) const override;
QModelIndex mapFromSource(const QModelIndex &index) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = {}) const override;
int columnCount(const QModelIndex &parent = {}) const override;
bool hasChildren(const QModelIndex &parent = {}) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(
int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
RVA address(const QModelIndex &index) const override;
QString name(const QModelIndex &index) const override;
void setCompact(bool enabled);
void setHierarchy(bool enabled);
bool isHierarchy() const { return hierarchy; }
QModelIndex groupIndex(const QString &path) const;
private:
struct Node
{
Node *parent = nullptr;
int row = 0;
QModelIndex source;
QString label;
QString fullName;
bool named = false;
QString groupPath;
std::vector<std::unique_ptr<Node>> children;
};
AddressableItemModelI *sourceProvider = nullptr;
int nameColumn;
bool compact = false;
bool hierarchy = false;
bool resetting = false;
int filterChangeDepth = 0;
Node root;
QHash<QModelIndex, Node *> sourceNodes;
QHash<QString, Node *> groups;
Node *node(const QModelIndex &index) const;
QModelIndex nodeIndex(Node *node, int column = 0) const;
void beginRebuild();
void endRebuild();
void rebuild();
void collapseSingleGroups(Node *parent);
void appendSource(Node *parent, const QModelIndex &source, const QString &label, bool named);
void sourceDataChanged(const QModelIndex &first, const QModelIndex &last);
void namesChanged(Node *parent);
};
#endif
+15 -1
View File
@@ -2,6 +2,7 @@
#include "R2DecaiDecompiler.h"
#include "Iaito.h"
#include <QByteArray>
#include <QJsonArray>
#include <QJsonObject>
@@ -48,7 +49,20 @@ static QStringList parseDecaiEvalLines(const QString &output)
QStringList R2DecaiDecompiler::listOptions()
{
QString output = Core()->cmd("decai -e");
// r2js console.log writes to stdout instead of the command's output buffer.
const QByteArray script = R"((function() {
var log = console.log;
var lines = [];
console.log = function() { lines.push(Array.prototype.join.call(arguments, " ")); };
try {
var output = r2.cmd("decai -e");
if (output) { lines.push(output); }
} finally {
console.log = log;
}
r2.log(lines.join("\n"));
})())";
QString output = Core()->cmd(("js base64:" + script.toBase64()).constData());
return parseDecaiEvalLines(output);
}
+136 -3
View File
@@ -106,6 +106,61 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Close tab"),
{},
QKeySequence::Close});
#ifdef Q_OS_MACOS
// Qt maps the Command key to CTRL on macOS.
const auto panelModifier = Qt::CTRL;
#else
const auto panelModifier = Qt::META;
#endif
t.append(
{"global.maximizePanel",
ShortcutScope::Global,
QT_TR_NOOP("Toggle panel maximization"),
{QKeySequence(panelModifier | Qt::Key_Return),
QKeySequence(panelModifier | Qt::Key_Enter)},
Unknown});
t.append(
{"global.selectPanelTab1",
ShortcutScope::Global,
QT_TR_NOOP("Select panel tab 1"),
{QKeySequence(panelModifier | Qt::Key_1)},
Unknown});
t.append(
{"global.selectPanelTab2",
ShortcutScope::Global,
QT_TR_NOOP("Select panel tab 2"),
{QKeySequence(panelModifier | Qt::Key_2)},
Unknown});
t.append(
{"global.selectPanelTab3",
ShortcutScope::Global,
QT_TR_NOOP("Select panel tab 3"),
{QKeySequence(panelModifier | Qt::Key_3)},
Unknown});
t.append(
{"global.selectPanelTab4",
ShortcutScope::Global,
QT_TR_NOOP("Select panel tab 4"),
{QKeySequence(panelModifier | Qt::Key_4)},
Unknown});
t.append(
{"global.selectPanelTab5",
ShortcutScope::Global,
QT_TR_NOOP("Select panel tab 5"),
{QKeySequence(panelModifier | Qt::Key_5)},
Unknown});
t.append(
{"global.focusNextFrame",
ShortcutScope::Global,
QT_TR_NOOP("Focus next frame"),
{QKeySequence(panelModifier | Qt::Key_J)},
Unknown});
t.append(
{"global.focusPreviousFrame",
ShortcutScope::Global,
QT_TR_NOOP("Focus previous frame"),
{QKeySequence(panelModifier | Qt::Key_K)},
Unknown});
t.append(
{"global.back",
ShortcutScope::Global,
@@ -254,6 +309,12 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Set to data (resize)"),
{QKeySequence(Qt::Key_Asterisk)},
Unknown});
t.append(
{"disasm.flag",
ShortcutScope::Disassembly,
QT_TR_NOOP("Add / edit flag"),
{QKeySequence(Qt::Key_F)},
Unknown});
t.append(
{"disasm.rename",
ShortcutScope::Disassembly,
@@ -276,7 +337,7 @@ const QVector<ShortcutDescriptor> &catalog()
{"disasm.linkType",
ShortcutScope::Disassembly,
QT_TR_NOOP("Link type"),
{QKeySequence(Qt::Key_L)},
{QKeySequence(Qt::SHIFT | Qt::Key_L)},
Unknown});
t.append(
{"disasm.addBreakpoint",
@@ -308,12 +369,30 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Switch to graph view"),
{QKeySequence(Qt::Key_Space)},
Unknown});
t.append(
{"disasm.switchToDecompiler",
ShortcutScope::Disassembly,
QT_TR_NOOP("Switch to decompiler view"),
{QKeySequence(Qt::Key_Tab)},
Unknown});
t.append(
{"disasm.seekPrev",
ShortcutScope::Disassembly,
QT_TR_NOOP("Seek to previous"),
{QKeySequence(Qt::Key_Escape)},
Unknown});
t.append(
{"disasm.cursorLeft",
ShortcutScope::Disassembly,
QT_TR_NOOP("Move cursor left"),
{QKeySequence(Qt::Key_H), QKeySequence(Qt::Key_Left)},
Unknown});
t.append(
{"disasm.cursorRight",
ShortcutScope::Disassembly,
QT_TR_NOOP("Move cursor right"),
{QKeySequence(Qt::Key_L), QKeySequence(Qt::Key_Right)},
Unknown});
t.append(
{"disasm.cursorDown",
ShortcutScope::Disassembly,
@@ -341,8 +420,8 @@ const QVector<ShortcutDescriptor> &catalog()
t.append(
{"disasm.jumpToOffset",
ShortcutScope::Disassembly,
QT_TR_NOOP("Jump to offset under cursor"),
{QKeySequence(Qt::Key_Return)},
QT_TR_NOOP("Follow reference under cursor"),
{QKeySequence(Qt::Key_Return), QKeySequence(Qt::Key_Enter)},
Unknown});
t.append(
@@ -351,6 +430,30 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Copy"),
{},
QKeySequence::Copy});
t.append(
{"decompiler.cursorLeft",
ShortcutScope::Decompiler,
QT_TR_NOOP("Move cursor left"),
{QKeySequence(Qt::Key_H)},
Unknown});
t.append(
{"decompiler.cursorDown",
ShortcutScope::Decompiler,
QT_TR_NOOP("Move cursor down"),
{QKeySequence(Qt::Key_J)},
Unknown});
t.append(
{"decompiler.cursorUp",
ShortcutScope::Decompiler,
QT_TR_NOOP("Move cursor up"),
{QKeySequence(Qt::Key_K)},
Unknown});
t.append(
{"decompiler.cursorRight",
ShortcutScope::Decompiler,
QT_TR_NOOP("Move cursor right"),
{QKeySequence(Qt::Key_L)},
Unknown});
t.append(
{"decompiler.comment",
ShortcutScope::Decompiler,
@@ -381,6 +484,24 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Edit variables"),
{QKeySequence(Qt::Key_Y)},
Unknown});
t.append(
{"decompiler.followReference",
ShortcutScope::Decompiler,
QT_TR_NOOP("Follow reference under cursor"),
{QKeySequence(Qt::Key_Return), QKeySequence(Qt::Key_Enter)},
Unknown});
t.append(
{"decompiler.matchBrace",
ShortcutScope::Decompiler,
QT_TR_NOOP("Jump to matching brace"),
{QKeySequence(Qt::Key_Percent), QKeySequence(Qt::SHIFT | Qt::Key_Percent)},
Unknown});
t.append(
{"decompiler.toggleFold",
ShortcutScope::Decompiler,
QT_TR_NOOP("Collapse/expand block"),
{QKeySequence(Qt::Key_Space)},
Unknown});
t.append(
{"decompiler.toggleBreakpoint",
ShortcutScope::Decompiler,
@@ -399,6 +520,12 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Seek to previous"),
{QKeySequence(Qt::Key_Escape)},
Unknown});
t.append(
{"decompiler.switchToDisassembly",
ShortcutScope::Decompiler,
QT_TR_NOOP("Switch to disassembly view"),
{QKeySequence(Qt::Key_Tab)},
Unknown});
t.append(
{"addressable.copyAddress",
@@ -492,6 +619,12 @@ const QVector<ShortcutDescriptor> &catalog()
{QKeySequence(Qt::SHIFT | Qt::Key_L)},
Unknown});
t.append({"hex.copy", ShortcutScope::Hexdump, QT_TR_NOOP("Copy"), {}, QKeySequence::Copy});
t.append(
{"hex.flag",
ShortcutScope::Hexdump,
QT_TR_NOOP("Add / edit flag"),
{QKeySequence(Qt::Key_F)},
Unknown});
t.append(
{"hex.copyAddress",
ShortcutScope::Hexdump,
+30
View File
@@ -0,0 +1,30 @@
#include "SourceLineReference.h"
QList<SourceLineReference> SourceLineReference::parse(const QString &contents)
{
QList<SourceLineReference> result;
for (const QString &row : contents.split(QLatin1Char('\n'))) {
const int firstTab = row.indexOf(QLatin1Char('\t'));
const int secondTab = row.indexOf(QLatin1Char('\t'), firstTab + 1);
if (firstTab <= 0 || secondTab < 0) {
continue;
}
const QString addressText = row.left(firstTab);
if (!addressText.startsWith(QStringLiteral("0x"))) {
continue;
}
bool addressOk = false;
bool lineOk = false;
const quint64 address = addressText.mid(2).toULongLong(&addressOk, 16);
const int line = row.mid(firstTab + 1, secondTab - firstTab - 1).toInt(&lineOk);
if (!addressOk || !lineOk || line < 0) {
continue;
}
QString code = row.mid(secondTab + 1);
if (code.endsWith(QLatin1Char('\r'))) {
code.chop(1);
}
result.append({address, line, code});
}
return result;
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <QList>
#include <QString>
struct SourceLineReference
{
quint64 address;
int line;
QString code;
// fs.r2's /cl files contain address, line number and source text, separated by tabs.
static QList<SourceLineReference> parse(const QString &contents);
};
+88
View File
@@ -0,0 +1,88 @@
#ifndef SPARSEHEXLAYOUT_H
#define SPARSEHEXLAYOUT_H
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <vector>
namespace SparseHexLayout {
// Offsets are relative to a bounded read buffer, never absolute addresses.
struct Row
{
int offset;
int size;
int fill = -1;
bool continues = false;
};
inline int uniformPrefix(const unsigned char *bytes, int size)
{
const uint64_t pattern = bytes[0] ? UINT64_MAX : 0;
int i = 0;
while (size - i >= int(sizeof(pattern))) {
uint64_t word;
std::memcpy(&word, bytes + i, sizeof(word));
if (word != pattern) {
break;
}
i += sizeof(word);
}
while (i < size && bytes[i] == bytes[0]) {
++i;
}
return i;
}
inline std::vector<Row> build(
const unsigned char *bytes, int size, int rowSize, int itemSize, int maxRows)
{
std::vector<Row> rows;
if (size <= 0 || rowSize <= 0 || itemSize <= 0 || maxRows <= 0) {
return rows;
}
rows.reserve(std::min(maxRows, size / rowSize + 2));
int begin = 0;
int pos = 0;
while (pos < size && int(rows.size()) < maxRows) {
int step = itemSize;
if (bytes[pos] == 0 || bytes[pos] == 0xff) {
const int run = uniformPrefix(bytes + pos, size - pos);
if (run > rowSize * 2) {
if (pos > begin) {
rows.push_back({begin, pos - begin});
if (int(rows.size()) == maxRows) {
return rows;
}
}
// Keep multi-byte items intact, including at the end of a read.
const int folded = run - run % itemSize;
rows.push_back({pos, folded, bytes[pos], pos + run == size});
begin = pos = pos + folded;
continue;
}
// Consume short runs once too; rescanning every suffix is quadratic
// when the user selects a large number of columns.
step = std::max(itemSize, run - run % itemSize);
}
step = std::min(step, size - pos);
while (step > 0 && int(rows.size()) < maxRows) {
const int count = std::min(step, rowSize - (pos - begin));
pos += count;
step -= count;
if (pos - begin == rowSize) {
rows.push_back({begin, rowSize});
begin = pos;
}
}
}
if (pos > begin && int(rows.size()) < maxRows) {
rows.push_back({begin, pos - begin});
}
return rows;
}
} // namespace SparseHexLayout
#endif // SPARSEHEXLAYOUT_H
+253
View File
@@ -0,0 +1,253 @@
#include "TypeLayout.h"
#include <algorithm>
#include <cstring>
#include <limits>
#include <QRegularExpression>
void TypeLayout::load(const QString &text, int pointerByteSize)
{
database.clear();
sizes.clear();
pointerSize = qBound(1, pointerByteSize, 8);
for (const QString &line : text.split(QLatin1Char('\n'))) {
const int separator = line.indexOf(QLatin1Char('='));
if (separator > 0) {
database.insert(line.left(separator), line.mid(separator + 1));
}
}
}
QStringList TypeLayout::names() const
{
QStringList result;
for (auto it = database.cbegin(); it != database.cend(); ++it) {
if (it.value() == QLatin1String("struct") || it.value() == QLatin1String("union")
|| it.value() == QLatin1String("typedef") || it.value() == QLatin1String("type")
|| it.value() == QLatin1String("enum")) {
result.append(it.key());
}
}
result.sort(Qt::CaseInsensitive);
return result;
}
QMap<quint64, QString> TypeLayout::links() const
{
QMap<quint64, QString> result;
for (auto it = database.cbegin(); it != database.cend(); ++it) {
if (it.key().startsWith(QLatin1String("link."))) {
bool ok = false;
const quint64 address = it.key().mid(5).toULongLong(&ok, 16);
if (ok) {
result.insert(address, it.value());
}
}
}
return result;
}
QString TypeLayout::resolve(QString type) const
{
static const QRegularExpression qualifiers(QStringLiteral("\\b(const|volatile|restrict)\\b"));
static const QRegularExpression tag(QStringLiteral("^(struct|union|enum)\\s+"));
QStringList visited;
for (int i = 0; i < 32; ++i) {
type.remove(qualifiers);
type = type.simplified();
type.remove(tag);
if (visited.contains(type)) {
return QString();
}
visited.append(type);
if (database.value(type) != QLatin1String("typedef")) {
return type;
}
type = database.value(QStringLiteral("typedef.") + type);
}
return QString();
}
bool TypeLayout::array(const QString &type, QString *element, quint64 *count)
{
static const QRegularExpression expression(QStringLiteral("^(.*?)\\[(\\d+)\\](.*)$"));
const auto match = expression.match(type);
if (!match.hasMatch()) {
return false;
}
bool ok = false;
*count = match.captured(2).toULongLong(&ok);
*element = (match.captured(1) + match.captured(3)).trimmed();
return ok;
}
QString TypeLayout::kind(const QString &type) const
{
const QString resolved = resolve(type);
if (resolved.endsWith(QLatin1Char('*'))) {
return QStringLiteral("pointer");
}
QString element;
quint64 count;
if (array(resolved, &element, &count)) {
return QStringLiteral("array");
}
return database.value(resolved);
}
QVector<TypeLayout::Field> TypeLayout::fields(const QString &type) const
{
return fields(type, {resolve(type)});
}
QVector<TypeLayout::Field> TypeLayout::fields(const QString &type, const QStringList &ancestors) const
{
const QString resolved = resolve(type);
const QString category = kind(resolved);
QVector<Field> result;
if (category != QLatin1String("struct") && category != QLatin1String("union")) {
return result;
}
const QString prefix = category + QLatin1Char('.') + resolved;
const QStringList members = database.value(prefix).split(QLatin1Char(','), Qt::SkipEmptyParts);
bool allZero = true;
for (const QString &name : members) {
const QStringList details
= database.value(prefix + QLatin1Char('.') + name).split(QLatin1Char(','));
if (details.size() < 3) {
continue;
}
bool ok = false;
const quint64 offset = details.at(1).toULongLong(&ok, 0);
if (!ok) {
continue;
}
const quint64 count = details.at(2).toULongLong(&ok, 0);
if (!ok) {
continue;
}
result.append({name, details.first(), offset, qMax<quint64>(1, count)});
allZero &= offset == 0;
}
// Some bundled r2 definitions omit offsets entirely. Their members are packed.
if (allZero && category == QLatin1String("struct") && result.size() > 1) {
quint64 offset = 0;
for (Field &field : result) {
field.offset = offset;
const quint64 bytes = size(field.type, ancestors);
if (bytes && field.count <= (std::numeric_limits<quint64>::max() - offset) / bytes) {
offset += bytes * field.count;
}
}
}
return result;
}
quint64 TypeLayout::size(const QString &type) const
{
const auto cached = sizes.constFind(type);
if (cached != sizes.cend()) {
return cached.value();
}
const quint64 result = size(type, {});
sizes.insert(type, result);
return result;
}
quint64 TypeLayout::size(const QString &type, QStringList ancestors) const
{
const QString resolved = resolve(type);
if (resolved.isEmpty() || ancestors.size() >= 32 || ancestors.contains(resolved)) {
return 0;
}
ancestors.append(resolved);
const QString category = kind(resolved);
if (category == QLatin1String("pointer")) {
return pointerSize;
}
QString element;
quint64 count;
if (array(resolved, &element, &count)) {
const quint64 stride = size(element, ancestors);
return stride && count <= std::numeric_limits<quint64>::max() / stride ? stride * count : 0;
}
if (category == QLatin1String("enum")) {
return 4;
}
if (category == QLatin1String("struct") || category == QLatin1String("union")) {
quint64 end = 0;
for (const Field &field : fields(resolved, ancestors)) {
const quint64 stride = size(field.type, ancestors);
if (!stride
|| field.count > (std::numeric_limits<quint64>::max() - field.offset) / stride) {
return 0;
}
end = qMax(end, field.offset + stride * field.count);
}
return end;
}
bool ok = false;
const qint64 bits = database.value(QStringLiteral("type.") + resolved + QStringLiteral(".size"))
.toLongLong(&ok);
return ok && bits > 0 ? quint64(bits) / 8 : 0;
}
quint64 TypeLayout::unsignedValue(const QByteArray &bytes, bool bigEndian)
{
quint64 result = 0;
for (int i = 0; i < qMin(8, int(bytes.size())); ++i) {
const int shift = (bigEndian ? bytes.size() - i - 1 : i) * 8;
if (shift < 64) {
result |= quint64(static_cast<unsigned char>(bytes.at(i))) << shift;
}
}
return result;
}
QString TypeLayout::value(const QString &type, const QByteArray &bytes, bool bigEndian) const
{
const QString resolved = resolve(type);
const quint64 number = unsignedValue(bytes, bigEndian);
if (kind(resolved) == QLatin1String("pointer")) {
return QStringLiteral("0x%1").arg(number, 0, 16);
}
if (resolved == QLatin1String("float") && bytes.size() == 4) {
const quint32 raw = quint32(number);
float value;
memcpy(&value, &raw, sizeof(value));
return QString::number(value, 'g', 8);
}
if (resolved == QLatin1String("double") && bytes.size() == 8) {
double value;
memcpy(&value, &number, sizeof(value));
return QString::number(value, 'g', 16);
}
if (bytes.size() > 8) {
return QString::fromLatin1(bytes.toHex(' '));
}
const bool isUnsigned = resolved.startsWith(QLatin1String("uint"))
|| resolved.startsWith(QLatin1String("unsigned"));
quint64 extended = number;
if (!isUnsigned && !bytes.isEmpty() && bytes.size() < 8
&& (number & (quint64(1) << (bytes.size() * 8 - 1)))) {
extended |= std::numeric_limits<quint64>::max() << (bytes.size() * 8);
}
qint64 signedNumber;
memcpy(&signedNumber, &extended, sizeof(signedNumber));
QString result = isUnsigned ? QString::number(number) : QString::number(signedNumber);
if (kind(resolved) == QLatin1String("enum")) {
const QString prefix = QStringLiteral("enum.") + resolved + QLatin1Char('.');
for (auto it = database.cbegin(); it != database.cend(); ++it) {
bool ok = false;
const qint64 candidate = it.value().toLongLong(&ok, 0);
if (it.key().startsWith(prefix) && ok && candidate == signedNumber) {
result += QStringLiteral(" (%1)").arg(it.key().mid(prefix.size()));
break;
}
}
}
if (resolved == QLatin1String("char") && number >= 32 && number < 127) {
result += QStringLiteral(" '%1'").arg(QChar(ushort(number)));
}
return result + QStringLiteral(" (0x%1)").arg(number, 0, 16);
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef TYPELAYOUT_H
#define TYPELAYOUT_H
#include <QByteArray>
#include <QHash>
#include <QMap>
#include <QStringList>
#include <QVector>
// A snapshot of r2's type database. Keep stored offsets: pf formats and tsj can
// lose padding, and JSON numbers cannot represent every 64-bit type-link address.
class TypeLayout
{
public:
struct Field
{
QString name;
QString type;
quint64 offset = 0;
quint64 count = 1;
};
void load(const QString &database, int pointerSize);
QStringList names() const;
QMap<quint64, QString> links() const;
QString resolve(QString type) const;
QString kind(const QString &type) const;
QVector<Field> fields(const QString &type) const;
quint64 size(const QString &type) const;
QString value(const QString &type, const QByteArray &bytes, bool bigEndian) const;
static quint64 unsignedValue(const QByteArray &bytes, bool bigEndian);
static bool array(const QString &type, QString *element, quint64 *count);
private:
QHash<QString, QString> database;
int pointerSize = 8;
mutable QHash<QString, quint64> sizes;
QVector<Field> fields(const QString &type, const QStringList &ancestors) const;
quint64 size(const QString &type, QStringList ancestors) const;
};
#endif // TYPELAYOUT_H
+371 -50
View File
@@ -22,6 +22,7 @@
#include <QPoint>
#include <QRect>
#include <QScopedValueRollback>
#include <QSet>
#include <QSizeGrip>
#include <QTabBar>
#include <QTimer>
@@ -43,6 +44,28 @@ static const char *dockHandleResizeGripProperty = "iaitoDockHandleResizeGrip";
static const char *dockTabCloseButtonIndexProperty = "iaitoDockTabCloseButtonIndex";
static const char *dockTabBarConfiguredProperty = "iaitoDockTabBarConfigured";
// Four opaque strips outline the active frame without changing its layout.
// Keeping them narrow avoids translucent child compositing artifacts on macOS.
class DockFocusBorder : public QWidget
{
public:
explicit DockFocusBorder(QWidget *parent)
: QWidget(parent)
{
setObjectName(QStringLiteral("activeFrameBorder"));
setAttribute(Qt::WA_TransparentForMouseEvents);
setAttribute(Qt::WA_OpaquePaintEvent);
setFocusPolicy(Qt::NoFocus);
}
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.fillRect(rect(), palette().highlight());
}
};
// Translucent highlight, in accent color, of the region a dragged panel would
// occupy if dropped now. Transparent to mouse input so it never interferes with
// the drag. Has two modes (see updateDropOverlay):
@@ -206,12 +229,6 @@ QString panelDisplayName(const IaitoDockWidget *dock)
return name;
}
QString normalizedDockTabText(QString text)
{
text.remove(QLatin1Char('&'));
return text.trimmed();
}
QPoint mouseEventGlobalPos(const QMouseEvent *event)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
@@ -321,26 +338,54 @@ DockManager::DockManager(MainWindow *mainWindow)
, backend(new QtDockBackend(mainWindow))
{
dockWidgets.reserve(20);
connect(mainWindow, &QMainWindow::tabifiedDockWidgetActivated, this, [this](QDockWidget *dock) {
auto *panel = qobject_cast<IaitoDockWidget *>(dock);
if (dockWidgets.contains(panel)) {
setActiveDock(panel);
}
});
}
DockManager::~DockManager()
{
for (auto border : activeFrameBorders) {
delete border;
}
delete dropOverlay;
delete backend;
}
void DockManager::addWidget(IaitoDockWidget *widget)
{
restoreMaximizedPanel();
dockWidgets.push_back(widget);
connect(widget, &QDockWidget::visibilityChanged, this, [this](bool) {
connect(widget, &QDockWidget::visibilityChanged, this, [this, widget](bool) {
if (!dockRelayoutInProgress) {
const bool hidden = widget->isHidden();
if (!maximizedLayoutState.isEmpty()
&& ((widget == maximizedDock && hidden)
|| (widget != maximizedDock && !hidden && !widget->isFloating()))) {
// Closing the expanded panel or opening another panel leaves
// maximization while preserving the requested visibility.
restoreMaximizedPanel();
widget->setVisible(!hidden);
if (!hidden) {
widget->raise();
}
}
emit layoutMutated();
}
requestUpdateActiveFrame();
});
connect(widget, &QDockWidget::topLevelChanged, this, [this](bool) {
connect(widget, &QDockWidget::topLevelChanged, this, [this, widget](bool floating) {
if (!dockRelayoutInProgress) {
if (!maximizedLayoutState.isEmpty()) {
restoreMaximizedPanel();
widget->setFloating(floating);
}
emit layoutMutated();
}
requestUpdateActiveFrame();
});
requestUpdateTabBars();
emit panelAdded(widget);
@@ -349,8 +394,12 @@ void DockManager::addWidget(IaitoDockWidget *widget)
void DockManager::removeWidget(IaitoDockWidget *widget)
{
restoreMaximizedPanel();
dockWidgets.removeAll(widget);
pluginDocks.removeAll(widget);
if (activeDock == widget) {
setActiveDock(nullptr);
}
requestUpdateTabBars();
emit panelRemoved(widget);
emit layoutMutated();
@@ -490,14 +539,78 @@ void DockManager::updateDockTabBars()
for (auto *dockWidget : dockWidgets) {
configureDockWidget(dockWidget);
}
QSet<IaitoDockWidget *> claimed;
for (auto *tabBar : backend->dockTabBars()) {
if (updateEmptyDockTabBar(tabBar)) {
continue;
}
if (configureDockTabBar(tabBar)) {
stampTabBar(tabBar, claimed);
configureDockTabBar(tabBar);
}
updateActiveFrame();
}
void DockManager::setActiveDock(IaitoDockWidget *dock)
{
if (activeDock == dock) {
return;
}
activeDock = dock;
requestUpdateActiveFrame();
}
void DockManager::requestUpdateActiveFrame()
{
if (activeFrameUpdateScheduled) {
return;
}
activeFrameUpdateScheduled = true;
QTimer::singleShot(0, this, [this]() {
activeFrameUpdateScheduled = false;
updateActiveFrame();
});
}
void DockManager::updateActiveFrame()
{
auto *dock = activeDock.data();
if (!dock || dock->visibleRegion().isEmpty()) {
for (auto border : activeFrameBorders) {
if (border) {
border->hide();
}
}
return;
}
QWidget *owner = dock->window();
QRect frame(dock->mapTo(owner, QPoint()), dock->size());
for (auto *tabBar : backend->dockTabBars()) {
if (isMainDockTabBar(tabBar) && tabBar->window() == owner
&& dockForDockTab(tabBar, tabBar->currentIndex()) == dock) {
frame = frame.united(QRect(tabBar->mapTo(owner, QPoint()), tabBar->size()));
break;
}
}
const int width = 2;
const QRect edges[] = {
QRect(frame.left(), frame.top(), frame.width(), width),
QRect(frame.left(), frame.bottom() - width + 1, frame.width(), width),
QRect(frame.left(), frame.top(), width, frame.height()),
QRect(frame.right() - width + 1, frame.top(), width, frame.height()),
};
while (activeFrameBorders.size() < 4) {
activeFrameBorders.append(nullptr);
}
for (int i = 0; i < 4; ++i) {
auto &border = activeFrameBorders[i];
if (!border) {
border = new DockFocusBorder(owner);
} else if (border->parentWidget() != owner) {
border->setParent(owner);
}
border->setGeometry(edges[i]);
border->show();
border->raise();
}
}
@@ -568,7 +681,7 @@ bool DockManager::configureDockTabBar(QTabBar *tabBar)
bool DockManager::isMainDockTabBar(QTabBar *tabBar) const
{
if (!tabBar || tabBar->count() == 0 || !tabBar->isVisible()) {
if (!tabBar || tabBar->count() == 0 || tabBar->visibleRegion().isEmpty()) {
return false;
}
@@ -580,30 +693,6 @@ bool DockManager::isMainDockTabBar(QTabBar *tabBar) const
return true;
}
void DockManager::stampTabBar(QTabBar *tabBar, QSet<IaitoDockWidget *> &claimed)
{
// Qt labels each tab with the dock's window title. Resolve every tab to its
// dock once (claiming across all bars so duplicate titles map bijectively)
// and record the dock pointer in tabData, so later lookups are exact instead
// of relying on title-string matching.
for (int i = 0; i < tabBar->count(); ++i) {
const QString want = normalizedDockTabText(tabBar->tabText(i));
if (want.isEmpty()) {
continue;
}
for (auto *dock : dockWidgets) {
if (claimed.contains(dock)) {
continue;
}
if (normalizedDockTabText(panelDisplayName(dock)) == want) {
tabBar->setTabData(i, QVariant::fromValue<QObject *>(dock));
claimed.insert(dock);
break;
}
}
}
}
void DockManager::updateDockTabCloseButtons(QTabBar *tabBar, int hoveredIndex)
{
if (!tabBar) {
@@ -732,6 +821,7 @@ void DockManager::showDockContextMenu(QWidget *parent, IaitoDockWidget *dock, co
void DockManager::placeDockTab(IaitoDockWidget *dock, DockDropKind kind)
{
restoreMaximizedPanel();
IaitoDockWidget *targetDock = dockSplitReference(dock);
if (!targetDock) {
return;
@@ -761,6 +851,7 @@ void DockManager::detachDockTab(IaitoDockWidget *dock, const QPoint &globalPos)
return;
}
restoreMaximizedPanel();
backend->removeDock(dock);
dock->setParent(mainWindow);
backend->setDockFloating(dock, true);
@@ -1332,27 +1423,236 @@ bool DockManager::closeCurrentTab()
return false;
}
bool DockManager::selectCurrentPanelTab(int index)
{
if (index < 0 || dockTabDragActive) {
return false;
}
// Expanded panels temporarily hide their sibling tabs. Restore the group
// to resolve its visual order, then expand the newly selected panel again.
const bool wasMaximized = !maximizedLayoutState.isEmpty();
restoreMaximizedPanel();
QPointer<IaitoDockWidget> dock = activeDock;
if (!dock || dock->isHidden()) {
dock = dockForWidget(QApplication::focusWidget());
}
if (wasMaximized) {
flushNativeDockLayout();
}
IaitoDockWidget *target = nullptr;
if (dock && !dock->isHidden()) {
for (auto *tabBar : backend->dockTabBars()) {
if (!isMainDockTabBar(tabBar) || index >= tabBar->count()) {
continue;
}
for (int i = 0; i < tabBar->count(); ++i) {
if (dockForDockTab(tabBar, i) == dock) {
target = dockForDockTab(tabBar, index);
break;
}
}
if (target) {
target->raiseMemoryWidget();
setActiveDock(target);
break;
}
}
}
if (wasMaximized) {
toggleCurrentPanelMaximized();
}
return target != nullptr;
}
bool DockManager::focusNextFrame(bool forward)
{
if (dockTabDragActive) {
return false;
}
const bool wasMaximized = !maximizedLayoutState.isEmpty();
restoreMaximizedPanel();
QPointer<IaitoDockWidget> current = activeDock;
if (!current || current->isHidden()) {
current = dockForWidget(QApplication::focusWidget());
}
if (wasMaximized) {
flushNativeDockLayout();
}
struct Frame
{
IaitoDockWidget *panel;
QRect rect;
};
QList<Frame> frames;
for (auto *dock : dockWidgets) {
// Inactive tabs are parked offscreen by Qt. Only the selected panel
// represents a frame; floating windows are outside the main layout.
if (dock->isFloating() || dock->parentWidget() != mainWindow || !dock->isEnabled()
|| dock->visibleRegion().isEmpty()) {
continue;
}
QRect rect = dock->geometry();
for (auto *tabBar : backend->dockTabBars()) {
if (isMainDockTabBar(tabBar) && dockForDockTab(tabBar, tabBar->currentIndex()) == dock) {
// Include the tab bar so tabbed and standalone frames in the
// same row sort together regardless of their content offset.
rect = rect.united(tabBar->geometry());
break;
}
}
frames.append({dock, rect});
}
std::stable_sort(frames.begin(), frames.end(), [](const Frame &a, const Frame &b) {
return a.rect.top() == b.rect.top() ? a.rect.left() < b.rect.left()
: a.rect.top() < b.rect.top();
});
QPointer<IaitoDockWidget> target;
QPointer<QWidget> focus;
if (!frames.isEmpty()) {
int index = -1;
for (int i = 0; i < frames.size(); ++i) {
if (frames.at(i).panel == current) {
index = i;
break;
}
}
const int next = index < 0 ? (forward ? 0 : frames.size() - 1)
: (index + (forward ? 1 : frames.size() - 1)) % frames.size();
target = frames.at(next).panel;
focus = target->focusWidget();
target->raiseMemoryWidget();
setActiveDock(target);
}
if (wasMaximized) {
toggleCurrentPanelMaximized();
}
if (target) {
target->activateWindow();
if (focus && focus->isEnabled() && focus->isVisibleTo(target)) {
focus->setFocus(Qt::ShortcutFocusReason);
if (maximizedDock == target) {
maximizedFocusWidget = focus;
}
}
}
return target != nullptr;
}
IaitoDockWidget *DockManager::dockForWidget(QWidget *widget) const
{
for (; widget; widget = widget->parentWidget()) {
if (auto *tabBar = qobject_cast<QTabBar *>(widget)) {
if (isMainDockTabBar(tabBar)) {
return dockForDockTab(tabBar, tabBar->currentIndex());
}
}
if (auto *dock = qobject_cast<IaitoDockWidget *>(widget)) {
if (dockWidgets.contains(dock)) {
return dock;
}
}
}
return nullptr;
}
QByteArray DockManager::saveState() const
{
// Maximization is temporary: saved layouts retain the tiled arrangement.
return maximizedLayoutState.isEmpty() ? backend->saveState() : maximizedLayoutState;
}
bool DockManager::toggleCurrentPanelMaximized()
{
if (!maximizedLayoutState.isEmpty()) {
restoreMaximizedPanel();
return true;
}
if (dockTabDragActive) {
return false;
}
IaitoDockWidget *dock = activeDock;
if (!dock || dock->isHidden()) {
dock = dockForWidget(QApplication::focusWidget());
}
if (!dock || dock->isHidden()) {
return false;
}
QScopedValueRollback<bool> relayoutGuard(dockRelayoutInProgress, true);
maximizedLayoutState = backend->saveState();
maximizedDock = dock;
QWidget *focus = QApplication::focusWidget();
maximizedFocusWidget = focus && (focus == dock || dock->isAncestorOf(focus)) ? focus : nullptr;
maximizedDockMaximumSize = dock->maximumSize();
dock->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
const bool wasAnimated = mainWindow->isAnimated();
mainWindow->setAnimated(false);
if (dock->isFloating()) {
backend->setDockFloating(dock, false);
}
if (backend->areaOf(dock) == Qt::NoDockWidgetArea) {
backend->addDock(dock, Qt::TopDockWidgetArea);
}
for (auto *other : dockWidgets) {
if (other != dock && !other->isFloating()) {
other->hide();
}
}
dock->raiseMemoryWidget();
if (maximizedFocusWidget) {
maximizedFocusWidget->setFocus(Qt::ShortcutFocusReason);
}
setActiveDock(dock);
mainWindow->setAnimated(wasAnimated);
requestUpdateTabBars();
return true;
}
void DockManager::restoreMaximizedPanel()
{
if (maximizedLayoutState.isEmpty()) {
return;
}
QScopedValueRollback<bool> relayoutGuard(dockRelayoutInProgress, true);
const QByteArray state = maximizedLayoutState;
const QPointer<IaitoDockWidget> dock = maximizedDock;
const QPointer<QWidget> focus = maximizedFocusWidget;
maximizedLayoutState.clear();
maximizedDock.clear();
maximizedFocusWidget.clear();
if (dock) {
dock->setMaximumSize(maximizedDockMaximumSize);
}
const bool wasAnimated = mainWindow->isAnimated();
mainWindow->setAnimated(false);
backend->restoreState(state);
mainWindow->setAnimated(wasAnimated);
if (dock && !dock->isHidden()) {
dock->raiseMemoryWidget();
if (focus) {
focus->setFocus(Qt::ShortcutFocusReason);
}
setActiveDock(dock);
}
requestUpdateTabBars();
}
IaitoDockWidget *DockManager::dockForDockTab(QTabBar *tabBar, int index) const
{
if (!tabBar || index < 0 || index >= tabBar->count()) {
return nullptr;
}
if (QObject *object = tabBar->tabData(index).value<QObject *>()) {
if (auto *dock = qobject_cast<IaitoDockWidget *>(object)) {
if (dockWidgets.contains(dock)) {
return dock;
}
}
}
const QString requestedText = normalizedDockTabText(tabBar->tabText(index));
if (requestedText.isEmpty()) {
return nullptr;
}
// QMainWindow owns tabData: Qt 5/6 store the dock's QWidget address as a
// quintptr and use it to preserve the active tab during layout changes and
// saveState(). Read it without overwriting it or guessing from tab titles.
const quintptr dockId = tabBar->tabData(index).value<quintptr>();
for (auto *dock : dockWidgets) {
if (dock && normalizedDockTabText(panelDisplayName(dock)) == requestedText) {
if (reinterpret_cast<quintptr>(static_cast<QWidget *>(dock)) == dockId) {
return dock;
}
}
@@ -1449,6 +1749,7 @@ void DockManager::startDockWidgetDrag(
return;
}
restoreMaximizedPanel();
dockTabDragActive = true;
dockDragOffset = offset;
updateDropOverlay(globalPos);
@@ -1744,6 +2045,24 @@ void DockManager::clearDockDrag()
bool DockManager::handleEvent(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::FocusIn || event->type() == QEvent::MouseButtonPress) {
if (auto *dock = dockForWidget(qobject_cast<QWidget *>(watched))) {
setActiveDock(dock);
}
}
if (watched == activeDock || watched == mainWindow) {
switch (event->type()) {
case QEvent::Move:
case QEvent::Resize:
case QEvent::Show:
case QEvent::Hide:
case QEvent::LayoutRequest:
requestUpdateActiveFrame();
break;
default:
break;
}
}
if (dockTabDragActive) {
if (event->type() == QEvent::KeyPress
&& static_cast<QKeyEvent *>(event)->key() == Qt::Key_Escape) {
@@ -1820,6 +2139,7 @@ bool DockManager::handleEvent(QObject *watched, QEvent *event)
clearDockDrag();
dockDragTabBar = tabBar;
dockDragWidget = dockForDockTab(tabBar, index);
setActiveDock(dockDragWidget);
dockDragStartGlobalPos = mouseEventGlobalPos(mouseEvent);
}
break;
@@ -1857,6 +2177,7 @@ bool DockManager::handleEvent(QObject *watched, QEvent *event)
case QEvent::Show:
case QEvent::Resize:
case QEvent::LayoutRequest: {
requestUpdateActiveFrame();
QPointer<QTabBar> guardedTabBar(tabBar);
QTimer::singleShot(0, this, [this, guardedTabBar]() {
if (guardedTabBar) {
+19 -2
View File
@@ -3,13 +3,14 @@
#include <functional>
#include <QByteArray>
#include <QList>
#include <QMap>
#include <QObject>
#include <QPoint>
#include <QPointer>
#include <QRect>
#include <QSet>
#include <QSize>
#include <QString>
#include <QStringList>
@@ -71,6 +72,11 @@ public:
void updateDockTabBars();
void requestUpdateTabBars();
bool closeCurrentTab();
bool selectCurrentPanelTab(int index);
bool focusNextFrame(bool forward);
bool toggleCurrentPanelMaximized();
void restoreMaximizedPanel();
QByteArray saveState() const;
bool handleEvent(QObject *watched, QEvent *event);
@@ -82,11 +88,14 @@ signals:
void layoutMutated();
private:
void setActiveDock(IaitoDockWidget *dock);
void requestUpdateActiveFrame();
void updateActiveFrame();
IaitoDockWidget *dockForWidget(QWidget *widget) const;
void configureDockWidget(QDockWidget *dock);
bool updateEmptyDockTabBar(QTabBar *tabBar);
bool configureDockTabBar(QTabBar *tabBar);
bool isMainDockTabBar(QTabBar *tabBar) const;
void stampTabBar(QTabBar *tabBar, QSet<IaitoDockWidget *> &claimed);
void updateDockTabCloseButtons(QTabBar *tabBar, int hoveredIndex = -1);
QToolButton *dockTabCloseButton(QTabBar *tabBar);
void closeDockTab(QTabBar *tabBar, int index);
@@ -122,6 +131,14 @@ private:
QList<IaitoDockWidget *> pluginDocks;
QMap<QString, std::function<IaitoDockWidget *(MainWindow *)>> widgetTypeToConstructorMap;
QPointer<IaitoDockWidget> activeDock;
QList<QPointer<QWidget>> activeFrameBorders;
bool activeFrameUpdateScheduled = false;
QPointer<IaitoDockWidget> maximizedDock;
QPointer<QWidget> maximizedFocusWidget;
QByteArray maximizedLayoutState;
QSize maximizedDockMaximumSize;
QPointer<QTabBar> dockDragTabBar;
QPointer<QWidget> dockDragHandle;
QPointer<IaitoDockWidget> dockDragWidget;
+290 -6
View File
@@ -21,6 +21,7 @@
#include "common/R2Shims.h"
#include "common/R2Task.h"
#include "common/TempConfig.h"
#include "common/TypeLayout.h"
#include "core/Iaito.h"
#include "plugins/PluginManager.h"
@@ -1320,6 +1321,24 @@ RVA IaitoCore::getOffset()
return ADDRESS_OF(core_);
}
QString IaitoCore::getCommandAnnotation(RVA address, RVA *size)
{
CORE_LOCK();
if (size) {
*size = 0;
}
RAnalMetaItem *item = r_meta_get_at(core->anal, address, R_META_TYPE_RUN, size);
return item && item->str ? QString::fromUtf8(item->str) : QString();
}
void IaitoCore::setCommandAnnotation(RVA address, RVA size, const QString &command)
{
CORE_LOCK();
// Store the command verbatim without passing it through the r2 command parser.
r_meta_set(core->anal, R_META_TYPE_RUN, address, size, command.toUtf8().constData());
emit refreshCodeViews();
}
ut64 IaitoCore::math(const QString &expr)
{
CORE_LOCK();
@@ -1602,7 +1621,12 @@ RVA IaitoCore::getLastFunctionInstruction(RVA addr)
if (!fcn) {
return RVA_INVALID;
}
#if R2_ABIVERSION >= 168
RAnalBlock **lastBBPtr = RVecAnalBlockPtr_last(&fcn->bbs);
RAnalBlock *lastBB = lastBBPtr ? *lastBBPtr : nullptr;
#else
RAnalBlock *lastBB = (RAnalBlock *) r_list_last(fcn->bbs);
#endif
return lastBB ? lastBB->addr + r_anal_bb_offset_inst(lastBB, lastBB->ninstr - 1) : RVA_INVALID;
}
@@ -3307,6 +3331,26 @@ QList<RMutaPluginDescription> IaitoCore::getRMutaPluginDescriptions()
return ret;
}
QList<ExceptionRegion> IaitoCore::getExceptionRegions(RVA function)
{
if (exceptionJsonSupport < 0) {
// Older iw implementations print flag commands even for iwj. Probe help
// once, without calling unsupported commands or depending on a new ABI.
const QString help = cmd("i?");
const QRegularExpression jsonHelp(QStringLiteral("\\biw\\[[^\\]\\n]*j"));
exceptionJsonSupport = help.contains(jsonHelp) ? 1 : 0;
}
if (!exceptionJsonSupport) {
return {};
}
const auto regions = ExceptionRegion::parse(cmdj("iwj").array());
if (function == RVA_INVALID || regions.isEmpty()) {
return regions;
}
const auto blocks = cmdj("afbj @ " + RAddressString(function)).array();
return ExceptionRegion::forBlocks(regions, blocks);
}
QList<FunctionDescription> IaitoCore::getAllFunctions()
{
CORE_LOCK();
@@ -3328,7 +3372,11 @@ QList<FunctionDescription> IaitoCore::getAllFunctions()
function.nlocals = r_anal_var_count(core->anal, fcn, 'b', 0)
+ r_anal_var_count(core->anal, fcn, 'r', 0)
+ r_anal_var_count(core->anal, fcn, 's', 0);
#if R2_ABIVERSION >= 168
function.nbbs = RVecAnalBlockPtr_length(&fcn->bbs);
#else
function.nbbs = r_list_length(fcn->bbs);
#endif
function.calltype = fcn->callconv ? QString::fromUtf8(fcn->callconv) : QString();
function.name = fcn->name ? QString::fromUtf8(fcn->name) : QString();
function.edges = r_anal_function_count_edges(fcn, nullptr);
@@ -3442,13 +3490,22 @@ QList<SymbolDescription> IaitoCore::getAllSymbols()
if (core && core->bin && core->bin->cur && core->bin->cur->BO) {
RVecRBinSymbol *symbols = r_bin_get_symbols_vec(core->bin);
if (symbols) {
const char *language = r_config_get(core->config, "bin.lang");
const bool keepLibrary = r_config_get_b(core->config, "bin.demangle.pfxlib");
RBinSymbol *bs;
R_VEC_FOREACH(symbols, bs)
{
QString type = QString(bs->bind) + " " + QString(bs->type);
SymbolDescription symbol;
symbol.vaddr = bs->vaddr;
symbol.name = QString(r_bin_name_tostring(bs->name));
const char *rawName = r_bin_name_tostring2(bs->name, 'o');
symbol.name = QString::fromUtf8(rawName);
symbol.demangledName = QString::fromUtf8(r_bin_name_tostring2(bs->name, 'd'));
if (symbol.demangledName == symbol.name) {
char *demangled
= r_bin_demangle(core->bin->cur, language, rawName, bs->vaddr, keepLibrary);
symbol.demangledName = QString::fromUtf8(demangled);
free(demangled);
}
symbol.bind = QString(bs->bind);
symbol.type = QString(bs->type);
ret << symbol;
@@ -3658,6 +3715,121 @@ QList<FlagDescription> IaitoCore::getAllFlags(QString flagspace)
return ret;
}
QList<GlobalVariableDescription> IaitoCore::getAllGlobalVariables()
{
CORE_LOCK();
QList<GlobalVariableDescription> variables;
const QJsonArray globals = cmdj("avgj").array();
for (const auto &value : globals) {
const QJsonObject object = value.toObject();
GlobalVariableDescription variable;
variable.name = object[RJsonKey::name].toString();
const auto flag = r_flag_get(core->flags, variable.name.toUtf8().constData());
if (!flag) {
continue;
}
// Qt JSON numbers cannot preserve every address above INT64_MAX.
variable.address = flag->addr;
variable.type = object[RJsonKey::type].toString();
variable.size = object[RJsonKey::size].toVariant().toULongLong();
variables.append(variable);
}
return variables;
}
bool IaitoCore::setGlobalVariable(
RVA address,
const QString &name,
const QString &type,
const QString &previousName,
QString *errorMessage)
{
CORE_LOCK();
const auto fail = [errorMessage](const QString &message) {
if (errorMessage) {
*errorMessage = message;
}
return false;
};
const QByteArray nameBytes = name.toUtf8();
if (address == RVA_INVALID || name.contains(QChar::Null)
|| !r_name_check(nameBytes.constData())) {
return fail(
tr("Enter a valid address and name (letters, digits, underscores, dots or colons)."));
}
RFlagItem *previous = nullptr;
if (!previousName.isEmpty()) {
previous = r_flag_get(core->flags, previousName.toUtf8().constData());
if (!previous || previous->addr != address || !previous->space
|| QString::fromUtf8(previous->space->name) != QStringLiteral("globals")) {
return fail(tr("The global variable no longer exists. Refresh the list and try again."));
}
} else {
for (const auto &variable : getAllGlobalVariables()) {
if (variable.address == address) {
return fail(tr("A global variable is already registered at this address."));
}
}
}
if (const auto flag = r_flag_get(core->flags, nameBytes.constData())) {
if (flag != previous) {
return fail(tr("This name is already used by another flag or variable."));
}
}
if (type.isEmpty() || type.contains(QLatin1Char('\n')) || type.contains(QLatin1Char('\r'))
|| type.contains(QChar::Null)) {
return fail(tr("Choose a type for the global variable."));
}
const QString currentName = previous ? previousName : name;
cmdRawAt(QStringLiteral("'avg %1 %2").arg(type, currentName), address);
const auto typedFlag = r_flag_get(core->flags, currentName.toUtf8().constData());
const auto meta = typedFlag ? r_flag_get_meta(core->flags, typedFlag->id) : nullptr;
if (!typedFlag || typedFlag->addr != address || !meta || QString::fromUtf8(meta->type) != type) {
return fail(
tr("Could not apply the selected type. Check that it exists and has a valid size."));
}
if (previous && name != previousName) {
cmdRaw("fs+globals");
cmdRaw(QStringLiteral("fr %1 %2").arg(previousName, name));
cmdRaw("fs-");
}
const auto flag = r_flag_get(core->flags, nameBytes.constData());
if (!flag || flag->addr != address) {
return fail(tr("Could not update the global variable."));
}
emit flagsChanged();
emit refreshCodeViews();
return true;
}
bool IaitoCore::deleteGlobalVariable(RVA address, const QString &name)
{
CORE_LOCK();
const QByteArray nameBytes = name.toUtf8();
const auto flag = r_flag_get(core->flags, nameBytes.constData());
if (!flag || flag->addr != address || !flag->space
|| QString::fromUtf8(flag->space->name) != QStringLiteral("globals")) {
return false;
}
// avg- removes shared metadata as well as a flag, so ambiguous addresses are unsafe.
for (const auto &variable : getAllGlobalVariables()) {
if (variable.address == address && variable.name != name) {
return false;
}
}
cmdRawAt("avg-", address);
if (r_flag_get(core->flags, nameBytes.constData())) {
return false;
}
emit flagsChanged();
emit refreshCodeViews();
return true;
}
QList<SectionDescription> IaitoCore::getAllSections()
{
CORE_LOCK();
@@ -3818,6 +3990,10 @@ QList<BinClassDescription> IaitoCore::getAllClassesFromBin()
BinClassMethodDescription meth;
meth.name = methObject[RJsonKey::name].toString();
meth.mangledName = methObject[QStringLiteral("rawname")].toString();
if (meth.name.isEmpty()) {
meth.name = meth.mangledName;
}
meth.addr = methObject[RJsonKey::addr].toVariant().toULongLong();
cls.methods << meth;
@@ -4744,8 +4920,10 @@ void IaitoCore::addFlag(RVA offset, QString name, RVA size, QString color, QStri
{
CORE_LOCK();
name = sanitizeStringForCommand(name);
auto fi = r_flag_set(this->core()->flags, name.toStdString().c_str(), offset, size);
auto fi = r_flag_set(this->core()->flags, name.toStdString().c_str(), offset, 0);
if (fi) {
// r_flag_set takes a 32-bit size, while flag ranges can span 64-bit sizes.
fi->size = size;
if (!color.isEmpty()) {
r_flag_item_set_color(this->core()->flags, fi, color.toStdString().c_str());
}
@@ -4842,6 +5020,15 @@ void IaitoCore::loadPDB(const QString &file)
cmdRaw0(QStringLiteral("idp ") + sanitizeStringForCommand(file));
}
static QString memoryPinsProjectPath(const QString &root, const QString &name)
{
QString path = QDir(root).filePath(name);
if (QFileInfo(path).isFile()) {
path += QStringLiteral(".d");
}
return QDir(path).filePath(QStringLiteral("iaito-memory-pins.json"));
}
void IaitoCore::openProject(const QString &name)
{
if (!isProjectNameValid(name)) {
@@ -4849,9 +5036,61 @@ void IaitoCore::openProject(const QString &name)
return;
}
const QString pinsPath = memoryPinsProjectPath(getConfig("dir.projects"), name);
bool ok = cmdRaw0(QStringLiteral("'P ") + name); // + "@e:scr.interactive=false");
if (ok) {
notes = QString::fromUtf8(QByteArray::fromBase64(cmdRaw("Pnj").toUtf8()));
// Some r2 binary project versions omit types, type links and command metadata.
QFile file(pinsPath);
if (file.open(QIODevice::ReadOnly)) {
const QJsonObject saved = QJsonDocument::fromJson(file.readAll()).object();
if (saved.value("version").toInt() == 1) {
{
CORE_LOCK();
const QJsonObject types = saved.value("types").toObject();
for (auto it = types.begin(); it != types.end(); ++it) {
const QByteArray key = it.key().toUtf8();
if (it.value().isString()
&& !sdb_const_get(core->anal->sdb_types, key.constData(), 0)) {
sdb_set(
core->anal->sdb_types,
key.constData(),
it.value().toString().toUtf8().constData(),
0);
}
}
}
TypeLayout layout;
layout.load(cmdRaw("tk"), getConfigi("asm.bits") / 8);
const auto existing = layout.links();
const QStringList names = layout.names();
CORE_LOCK();
for (const QJsonValue &entry : saved.value("links").toArray()) {
const QJsonObject link = entry.toObject();
bool valid = false;
const RVA address = link.value("address").toString().toULongLong(&valid, 16);
const QString type = link.value("type").toString();
if (valid && names.contains(type) && !existing.contains(address)) {
r_type_set_link(core->anal->sdb_types, type.toUtf8().constData(), address);
}
}
for (const QJsonValue &entry : saved.value("commands").toArray()) {
const QJsonObject pin = entry.toObject();
bool validAddress = false;
bool validSize = false;
const RVA address
= pin.value("address").toString().toULongLong(&validAddress, 16);
const RVA size = pin.value("size").toString().toULongLong(&validSize, 16);
const QString command = pin.value("command").toString();
if (validAddress && validSize && size && size - 1 <= RVA_MAX - address
&& !command.isEmpty()
&& !r_meta_get_at(core->anal, address, R_META_TYPE_RUN, nullptr)) {
r_meta_set(
core->anal, R_META_TYPE_RUN, address, size, command.toUtf8().constData());
}
}
}
}
} else {
QMessageBox::critical(
nullptr, tr("Error"), tr("Cannot open project. See console for details"));
@@ -4870,13 +5109,58 @@ void IaitoCore::saveProject(const QString &name)
}
Core()->setConfig("scr.interactive", false);
const bool ok = cmdRaw0(QStringLiteral("'Ps ") + name.trimmed());
bool ok = cmdRaw0(QStringLiteral("'Ps ") + name.trimmed());
if (ok) {
TypeLayout layout;
const QString database = cmdRaw("tk");
layout.load(database, getConfigi("asm.bits") / 8);
QJsonArray entries;
const auto links = layout.links();
for (auto it = links.cbegin(); it != links.cend(); ++it) {
entries.append(
QJsonObject{{"address", QString::number(it.key(), 16)}, {"type", it.value()}});
}
QJsonObject types;
const QStringList names = layout.names();
for (const QString &line : database.split(QLatin1Char('\n'))) {
const int separator = line.indexOf(QLatin1Char('='));
const QString key = line.left(separator);
if (separator > 0
&& (names.contains(key) || key.startsWith("type.") || key.startsWith("struct.")
|| key.startsWith("union.") || key.startsWith("enum.")
|| key.startsWith("typedef."))) {
types.insert(key, line.mid(separator + 1));
}
}
QJsonArray commands;
static const QRegularExpression commandPin(
QStringLiteral("^Cr\\s+\\d+\\s+(0x[0-9a-fA-F]+)\\s+#"));
for (const QString &line : cmdRaw("Cr*").split(QLatin1Char('\n'))) {
const auto match = commandPin.match(line);
if (match.hasMatch()) {
const RVA address = match.captured(1).toULongLong(nullptr, 16);
RVA size = 0;
const QString command = getCommandAnnotation(address, &size);
commands.append(
QJsonObject{
{"address", QString::number(address, 16)},
{"size", QString::number(size, 16)},
{"command", command}});
}
}
QSaveFile file(memoryPinsProjectPath(getConfig("dir.projects"), name.trimmed()));
const QByteArray data
= QJsonDocument(
QJsonObject{
{"version", 1}, {"links", entries}, {"types", types}, {"commands", commands}})
.toJson();
ok = file.open(QIODevice::WriteOnly) && file.write(data) == data.size() && file.commit();
}
if (!ok) {
QMessageBox::critical(
nullptr,
tr("Error"),
tr("Cannot save project. Ensure the project name doesnt have any "
"special or uppercase character"));
tr("Cannot save the project or its memory pins. See console for details."));
}
#if 0
cmdRaw(QStringLiteral("Pnj %1").arg(QString(notes.toUtf8().toBase64())));
+13
View File
@@ -4,6 +4,7 @@
#define MONOTHREAD 1
#include "common/BasicInstructionHighlighter.h"
#include "common/ExceptionInfo.h"
#include "core/IaitoCommon.h"
#include "core/IaitoDescriptions.h"
@@ -464,6 +465,7 @@ public:
void suspendDebug();
private:
int exceptionJsonSupport = -1;
void finishStopDebug();
public:
@@ -553,6 +555,8 @@ public:
void loadPDB(const QString &file);
QByteArray ioRead(RVA addr, int len);
QString getCommandAnnotation(RVA address, RVA *size = nullptr);
void setCommandAnnotation(RVA address, RVA size, const QString &command);
QList<RVA> getSeekHistory();
@@ -580,6 +584,7 @@ public:
QList<RDebugPluginDescription> getRDebugPluginDescriptions();
QList<RMutaPluginDescription> getRMutaPluginDescriptions();
QList<FunctionDescription> getAllFunctions();
QList<ExceptionRegion> getExceptionRegions(RVA function = RVA_INVALID);
QList<ImportDescription> getAllImports();
QList<ExportDescription> getAllExports();
QList<SymbolDescription> getAllSymbols();
@@ -590,6 +595,14 @@ public:
QList<StringDescription> getAllStrings();
QList<FlagspaceDescription> getAllFlagspaces();
QList<FlagDescription> getAllFlags(QString flagspace = QString());
QList<GlobalVariableDescription> getAllGlobalVariables();
bool setGlobalVariable(
RVA address,
const QString &name,
const QString &type,
const QString &previousName = QString(),
QString *errorMessage = nullptr);
bool deleteGlobalVariable(RVA address, const QString &name);
QList<SectionDescription> getAllSections();
QList<SegmentDescription> getAllSegments();
QList<EntrypointDescription> getAllEntrypoint();
+11
View File
@@ -98,6 +98,7 @@ struct SymbolDescription
QString bind;
QString type;
QString name;
QString demangledName;
};
struct CommentDescription
@@ -137,6 +138,14 @@ struct FlagDescription
QString realname;
};
struct GlobalVariableDescription
{
RVA address = RVA_INVALID;
RVA size = 0;
QString name;
QString type;
};
struct SectionDescription
{
RVA vaddr = 0;
@@ -264,6 +273,7 @@ struct BinClassBaseClassDescription
struct BinClassMethodDescription
{
QString name;
QString mangledName;
RVA addr = RVA_INVALID;
st64 vtableOffset = -1;
};
@@ -425,6 +435,7 @@ Q_DECLARE_METATYPE(RelocDescription)
Q_DECLARE_METATYPE(StringDescription)
Q_DECLARE_METATYPE(FlagspaceDescription)
Q_DECLARE_METATYPE(FlagDescription)
Q_DECLARE_METATYPE(GlobalVariableDescription)
Q_DECLARE_METATYPE(XrefDescription)
Q_DECLARE_METATYPE(EntrypointDescription)
Q_DECLARE_METATYPE(RBinPluginDescription)
+159 -18
View File
@@ -48,6 +48,7 @@
#include "widgets/ConsoleWidget.h"
#include "widgets/CustomCommandWidget.h"
#include "widgets/Dashboard.h"
#include "widgets/DataAnalysisWidget.h"
#include "widgets/DebugActions.h"
#include "widgets/DecompilerWidget.h"
#include "widgets/DisassemblerGraphView.h"
@@ -58,9 +59,11 @@
#include "widgets/FilesystemWidget.h"
#include "widgets/FlagsWidget.h"
#include "widgets/FunctionsWidget.h"
#include "widgets/GlobalVariablesWidget.h"
#include "widgets/GraphView.h"
#include "widgets/GraphWidget.h"
#include "widgets/HeadersWidget.h"
#include "widgets/HexPatWidget.h"
#include "widgets/HexdumpWidget.h"
#include "widgets/ImportsWidget.h"
#include "widgets/InterfacesWidget.h"
@@ -1072,6 +1075,34 @@ void MainWindow::initUI()
}
});
auto *maximizePanelAction = new QAction(tr("Maximize/restore current panel"), this);
maximizePanelAction->setAutoRepeat(false);
ShortcutMgr()->bindAction("global.maximizePanel", maximizePanelAction);
ui->menuWindowLayout->insertAction(ui->actionDefault, maximizePanelAction);
connect(maximizePanelAction, &QAction::triggered, m_dockManager, [this]() {
m_dockManager->toggleCurrentPanelMaximized();
});
for (int index = 0; index < 5; ++index) {
auto *shortcut = ShortcutMgr()->registerShortcut(
QStringLiteral("global.selectPanelTab%1").arg(index + 1), this);
shortcut->setAutoRepeat(false);
connect(shortcut, &QShortcut::activated, m_dockManager, [this, index]() {
m_dockManager->selectCurrentPanelTab(index);
});
}
auto *nextFrameShortcut = ShortcutMgr()->registerShortcut("global.focusNextFrame", this);
nextFrameShortcut->setAutoRepeat(false);
connect(nextFrameShortcut, &QShortcut::activated, m_dockManager, [this]() {
m_dockManager->focusNextFrame(true);
});
auto *previousFrameShortcut = ShortcutMgr()->registerShortcut("global.focusPreviousFrame", this);
previousFrameShortcut->setAutoRepeat(false);
connect(previousFrameShortcut, &QShortcut::activated, m_dockManager, [this]() {
m_dockManager->focusNextFrame(false);
});
connect(ui->actionZoomIn, &QAction::triggered, this, &MainWindow::onZoomIn);
connect(ui->actionZoomOut, &QAction::triggered, this, &MainWindow::onZoomOut);
connect(ui->actionZoomReset, &QAction::triggered, this, &MainWindow::onZoomReset);
@@ -1151,6 +1182,14 @@ void MainWindow::initUI()
addDockWidget(Qt::DockWidgetArea::TopDockWidgetArea, r2aiDock);
m_dockManager->addPluginDock(r2aiDock);
}
{
auto *hexPatDock = new HexPatWidget(this);
addWidget(hexPatDock);
ui->menuTools->insertAction(ui->actionStart_Web_Server, hexPatDock->toggleViewAction());
addDockWidget(Qt::DockWidgetArea::TopDockWidgetArea, hexPatDock);
m_dockManager->addPluginDock(hexPatDock);
connect(ui->menuTools, &QMenu::aboutToShow, hexPatDock, &HexPatWidget::updateAvailability);
}
// Check if plugins are loaded and display tooltips accordingly
ui->menuWindows->setToolTipsVisible(true);
@@ -1208,6 +1247,7 @@ void MainWindow::initUI()
connectMenuStatusTips(ui->menuEdit);
connectMenuStatusTips(ui->menuCode);
connectMenuStatusTips(ui->menuAnalysis);
connectMenuStatusTips(ui->menuData);
connectMenuStatusTips(ui->menuTools);
connectMenuStatusTips(ui->menuPlugins);
@@ -1543,12 +1583,12 @@ void MainWindow::initDocks()
QList<IaitoDockWidget *> ioDocks
= {filesDock = new FilesWidget(this),
binariesDock = new BinariesWidget(this),
filesystemDock = new FilesystemWidget(this),
mapsDock = new MapsWidget(this)};
QList<IaitoDockWidget *> infoDocks = {
classesDock = new ClassesWidget(this),
entrypointDock = new EntrypointWidget(this),
exportsDock = new ExportsWidget(this),
flagsDock = new FlagsWidget(this),
headersDock = new HeadersWidget(this),
importsDock = new ImportsWidget(this),
interfacesDock = new InterfacesWidget(this),
@@ -1578,8 +1618,20 @@ void MainWindow::initDocks()
r2GraphDock = new R2GraphWidget(this),
callGraphDock = new CallGraphWidget(this, false),
globalCallGraphDock = new CallGraphWidget(this, true),
};
QList<IaitoDockWidget *> dataDocks = {
flagsDock = new FlagsWidget(this),
#if R2_ABIVERSION >= 154
// avgj and C type metadata require post-6.2.4 radare2.
globalVariablesDock = new GlobalVariablesWidget(this),
#endif
dataAnalysisDock = new DataAnalysisWidget(this),
zoomDock = new ZoomWidget(this),
};
dataAnalysisDock->toggleViewAction()->setText(tr("Analysis"));
connect(dataAnalysisDock, &QWidget::windowTitleChanged, this, [this]() {
dataAnalysisDock->toggleViewAction()->setText(tr("Analysis"));
});
auto makeActionList = [this](QList<IaitoDockWidget *> docks) {
QList<QAction *> result;
@@ -1599,6 +1651,7 @@ void MainWindow::initDocks()
ui->menuCode->addSeparator();
ui->menuCode->addActions(makeActionList(codeDocks));
ui->menuAnalysis->addActions(makeActionList(analysisDocks));
ui->menuData->addActions(makeActionList(dataDocks));
QList<IaitoDockWidget *> windowDocks2 = {
consoleDock,
};
@@ -1612,19 +1665,17 @@ void MainWindow::initDocks()
ui->menuView->insertSeparator(ui->menuZoom->menuAction());
ui->menuAddInfoWidgets->addActions(makeActionList(infoDocks));
ui->menuAddIoWidgets->addActions(makeActionList(ioDocks));
QAction *actionFilesystem = new QAction("Filesystem", this);
connect(actionFilesystem, &QAction::triggered, this, [this]() {
if (!filesystemDock) {
filesystemDock = new FilesystemWidget(this);
addExtraWidget(filesystemDock);
}
QAction *actionSourceFiles = new QAction(tr("Source Files"), this);
connect(actionSourceFiles, &QAction::triggered, this, [this]() {
filesystemDock->show();
filesystemDock->raise();
filesystemDock->showSourceFiles();
});
ui->menuAddIoWidgets->addAction(actionFilesystem);
ui->menuCode->addAction(actionSourceFiles);
ui->menuAddDebugWidgets->addActions(makeActionList(debugDocks));
auto uniqueDocks = mainViewDocks + windowDocks2 + infoDocks + analysisDocks;
uniqueDocks += codeDocks + ioDocks + debugDocks;
uniqueDocks += codeDocks + ioDocks + debugDocks + dataDocks;
uniqueDocks.append(overviewDock);
for (auto dock : uniqueDocks) {
if (dock) { // ignore nullptr used as separators
@@ -1678,6 +1729,7 @@ void MainWindow::applyTopLevelMenuIcons()
setAppMenuIcon(this, ui->menuAddInfoWidgets, AppMenuIcon::Info, viewColor);
setAppMenuIcon(this, ui->menuAddIoWidgets, AppMenuIcon::Storage, viewColor);
setAppMenuIcon(this, ui->menuAnalysis, AppMenuIcon::Analysis, analysisColor);
setAppMenuIcon(this, ui->menuData, AppMenuIcon::Storage, viewColor);
setAppMenuIcon(this, ui->menuAddAnother, AppMenuIcon::Extra, viewColor);
setAppMenuIcon(this, ui->menuZoom, AppMenuIcon::ZoomIn, viewColor);
setAppMenuIcon(this, ui->actionZoomIn, AppMenuIcon::ZoomIn, viewColor);
@@ -2433,6 +2485,10 @@ void MainWindow::restoreDocks()
tabifyDockWidget(functionsDock, symbolsDock);
tabifyDockWidget(functionsDock, importsDock);
tabifyDockWidget(functionsDock, exportsDock);
if (globalVariablesDock) {
tabifyDockWidget(functionsDock, globalVariablesDock);
globalVariablesDock->hide();
}
// main area
tabifyDockWidget(dashboardDock, entrypointDock);
@@ -2460,6 +2516,8 @@ void MainWindow::restoreDocks()
filesDock->hide();
binariesDock->hide();
tabifyDockWidget(dashboardDock, zoomDock);
tabifyDockWidget(dashboardDock, dataAnalysisDock);
dataAnalysisDock->hide();
for (const auto &it : m_dockManager->docks()) {
// Check whether or not current widgets is graph, hexdump or disasm
if (isExtraMemoryWidget(it)) {
@@ -2538,7 +2596,8 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type)
for (auto &dock : m_dockManager->docks()) {
if (auto memoryWidget = qobject_cast<MemoryDockWidget *>(dock)) {
if (memoryWidget->getType() == type && memoryWidget->getSeekable()->isSynchronized()) {
memoryWidget->tryRaiseMemoryWidget();
// Explicit view requests should also open an empty graph.
memoryWidget->raiseMemoryWidget();
return;
}
}
@@ -2547,6 +2606,11 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type)
memoryDockWidget->raiseMemoryWidget();
}
void MainWindow::showDataAnalysis(RVA start, RVA end)
{
dataAnalysisDock->showRange(start, end);
}
void MainWindow::gotoOffset(const QString &offset)
{
ut64 address;
@@ -2590,6 +2654,16 @@ QMenu *MainWindow::createShowInMenu(QWidget *parent, RVA address, AddressTypeHin
}
QAction *action = new QAction(memoryWidget->windowTitle(), menu);
connect(action, &QAction::triggered, this, [memoryWidget, address]() {
if (auto *analysis = qobject_cast<DataAnalysisWidget *>(memoryWidget)) {
if (Core()->hasAddressRangeSelection()
&& Core()->getAddressRangeSelectionStart() <= address
&& address <= Core()->getAddressRangeSelectionEnd()) {
analysis->showRange(
Core()->getAddressRangeSelectionStart(),
Core()->getAddressRangeSelectionEnd());
return;
}
}
memoryWidget->getSeekable()->seek(address);
memoryWidget->raiseMemoryWidget();
});
@@ -2717,12 +2791,37 @@ void MainWindow::initBackForwardMenu()
void MainWindow::updateHistoryMenu(QMenu *menu, bool redo)
{
QPointer<IaitoSeekable> seekable = navigationSeekable();
if (seekable && !seekable->isSynchronized()) {
menu->clear();
int steps = 0;
for (RVA address : seekable->localHistory(redo)) {
const QString label = steps == 0
? tr("Current position (%1)").arg(RAddressString(address))
: RAddressString(address);
QAction *action = menu->addAction(label);
action->setEnabled(steps != 0);
connect(action, &QAction::triggered, menu, [seekable, steps, redo]() {
if (!seekable) {
return;
}
for (int i = 0; i < steps; ++i) {
if (redo) {
seekable->seekNext();
} else {
seekable->seekPrev();
}
}
});
++steps;
}
return;
}
// Not too long so that whole screen doesn't get covered,
// not too short so that reasonable length c++ names can be seen most of the
// time
const int MAX_NAME_LENGTH = 64;
auto hist = Core()->cmdj("sj");
bool history = true;
QList<QAction *> actions;
for (auto item : Core()->cmdj("sj").array()) {
@@ -2752,6 +2851,7 @@ void MainWindow::updateHistoryMenu(QMenu *menu, bool redo)
QFont font;
font.setBold(true);
action->setFont(font);
action->setEnabled(false);
}
}
}
@@ -2760,7 +2860,7 @@ void MainWindow::updateHistoryMenu(QMenu *menu, bool redo)
}
menu->clear();
menu->addActions(actions);
int steps = 1;
int steps = 0;
for (QAction *item : menu->actions()) {
if (redo) {
connect(item, &QAction::triggered, item, [steps]() {
@@ -3111,7 +3211,7 @@ IaitoLayout MainWindow::getViewLayout()
{
IaitoLayout layout;
layout.geometry = saveGeometry();
layout.state = saveState();
layout.state = m_dockManager->saveState();
for (auto dock : m_dockManager->docks()) {
QVariantMap properties;
@@ -3134,6 +3234,7 @@ IaitoLayout MainWindow::getViewLayout(const QString &name)
void MainWindow::setViewLayout(const IaitoLayout &layout)
{
m_dockManager->restoreMaximizedPanel();
bool isDefault = layout.state.isEmpty() || layout.geometry.isEmpty();
bool isDebug = Core()->currentlyDebugging;
bool wasAnimated = isAnimated();
@@ -3201,6 +3302,10 @@ void MainWindow::setViewLayout(const IaitoLayout &layout)
// other
}
for (auto dock : newDocks) {
// Data analysis scans are opt-in, including when upgrading a saved layout.
if (dock == dataAnalysisDock) {
continue;
}
dockOnMainArea(dock);
// Show any new docks by default.
// Showing new builtin docks helps discovering features added in
@@ -3505,12 +3610,31 @@ void MainWindow::on_actionQuit_triggered()
void MainWindow::on_actionBackward_triggered()
{
core->seekPrev();
if (auto *seekable = navigationSeekable()) {
seekable->seekPrev();
} else {
core->seekPrev();
}
}
void MainWindow::on_actionForward_triggered()
{
core->seekNext();
if (auto *seekable = navigationSeekable()) {
seekable->seekNext();
} else {
core->seekNext();
}
}
IaitoSeekable *MainWindow::navigationSeekable(QWidget *origin) const
{
for (QWidget *widget = origin ? origin : QApplication::focusWidget(); widget;
widget = widget->parentWidget()) {
if (auto *dock = qobject_cast<AddressableDockWidget *>(widget)) {
return dock->getSeekable();
}
}
return navigationDock && navigationDock->isVisible() ? navigationDock->getSeekable() : nullptr;
}
void MainWindow::on_actionDisasAdd_comment_triggered()
@@ -4109,10 +4233,10 @@ void MainWindow::mousePressEvent(QMouseEvent *event)
{
switch (event->button()) {
case Qt::BackButton:
core->seekPrev();
on_actionBackward_triggered();
break;
case Qt::ForwardButton:
core->seekNext();
on_actionForward_triggered();
break;
default:
break;
@@ -4142,6 +4266,15 @@ static bool isTextInputFocused()
bool MainWindow::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::FocusIn) {
for (QWidget *widget = qobject_cast<QWidget *>(watched); widget;
widget = widget->parentWidget()) {
if (auto *dock = qobject_cast<AddressableDockWidget *>(widget)) {
navigationDock = dock;
break;
}
}
}
if (m_dockManager && m_dockManager->handleEvent(watched, event)) {
return true;
}
@@ -4162,7 +4295,15 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event)
if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::ForwardButton || mouseEvent->button() == Qt::BackButton) {
mousePressEvent(mouseEvent);
if (auto *seekable = navigationSeekable(qobject_cast<QWidget *>(watched))) {
if (mouseEvent->button() == Qt::BackButton) {
seekable->seekPrev();
} else {
seekable->seekNext();
}
} else {
mousePressEvent(mouseEvent);
}
return true;
}
}
+7
View File
@@ -35,6 +35,7 @@ class RelocsWidget;
class CommentsWidget;
class StringsWidget;
class FlagsWidget;
class GlobalVariablesWidget;
class Dashboard;
class QLineEdit;
class QRubberBand;
@@ -66,6 +67,7 @@ class FilesWidget;
class FilesystemWidget;
class BinariesWidget;
class ZoomWidget;
class DataAnalysisWidget;
class XrefsWidget;
class RefsWidget;
class CodeInfoWidget;
@@ -151,6 +153,7 @@ public:
QString getUniqueObjectName(const QString &widgetType) const;
void showMemoryWidget();
void showMemoryWidget(MemoryWidgetType type);
void showDataAnalysis(RVA start, RVA end);
void gotoOffset(const QString &offset);
enum class AddressTypeHint { Function, Data, Unknown };
QMenu *createShowInMenu(
@@ -340,6 +343,7 @@ private:
CommentsWidget *commentsDock = nullptr;
StringsWidget *stringsDock = nullptr;
FlagsWidget *flagsDock = nullptr;
GlobalVariablesWidget *globalVariablesDock = nullptr;
Dashboard *dashboardDock = nullptr;
SdbWidget *sdbDock = nullptr;
SectionsWidget *sectionsDock = nullptr;
@@ -366,6 +370,7 @@ private:
CallGraphWidget *callGraphDock = nullptr;
CallGraphWidget *globalCallGraphDock = nullptr;
ZoomWidget *zoomDock = nullptr;
DataAnalysisWidget *dataAnalysisDock = nullptr;
XrefsWidget *xrefsDock = nullptr;
RefsWidget *refsDock = nullptr;
CodeInfoWidget *codeInfoDock = nullptr;
@@ -415,6 +420,7 @@ private:
* @param redo set to false for undo history, true for redo.
*/
void updateHistoryMenu(QMenu *menu, bool redo = false);
IaitoSeekable *navigationSeekable(QWidget *origin = nullptr) const;
void updateSaveProjectAction();
void updateWriteUndoRedoActions();
void rebuildRecentScriptsMenu();
@@ -445,6 +451,7 @@ private:
MemoryDockWidget *lastSyncMemoryWidget = nullptr;
MemoryDockWidget *lastMemoryWidget = nullptr;
QPointer<AddressableDockWidget> navigationDock;
int functionDockWidthToRestore = 0;
// True when the main window UI has been fully initialized and it's safe to
+6
View File
@@ -130,9 +130,15 @@
<string>Analysis</string>
</property>
</widget>
<widget class="QMenu" name="menuData">
<property name="title">
<string>Data</string>
</property>
</widget>
<addaction name="actionHighlight"/>
<addaction name="separator"/>
<addaction name="menuAnalysis"/>
<addaction name="menuData"/>
<addaction name="menuAddInfoWidgets"/>
<addaction name="menuAddIoWidgets"/>
<addaction name="separator"/>
+44 -39
View File
@@ -3,13 +3,12 @@
#include <QAction>
#include <QColor>
#include <QColorDialog>
#include <QEvent>
#include <QMenu>
#include "core/Iaito.h"
#include <QAbstractButton>
#include <QIntValidator>
#include <QPushButton>
#include <QRegularExpressionValidator>
// Forwarding constructor: default flag size when only offset & parent provided
FlagDialog::FlagDialog(RVA offset, QWidget *parent)
@@ -38,15 +37,14 @@ FlagDialog::FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent)
connect(ui->colorEdit, &QLineEdit::textChanged, this, [this](const QString &color) {
ui->colorPreview->setStyleSheet(QStringLiteral("background-color: %1;").arg(color));
});
RFlagItem *flag = r_flag_get_i(Core()->core()->flags, offset);
RFlagItem *flag = r_flag_get_in(Core()->core()->flags, offset);
if (flag) {
flagName = QString(flag->name);
flagOffset = flag->addr;
}
auto size_validator = new QIntValidator(ui->sizeEdit);
size_validator->setBottom(1);
ui->sizeEdit->setValidator(size_validator);
ui->sizeEdit->setValidator(new QRegularExpressionValidator(
QRegularExpression(QStringLiteral("[0-9]{1,20}")), ui->sizeEdit));
// Pre-fill size field: use defaultSize for new flags, override with existing for edits
ui->sizeEdit->setText(QString::number(defaultSize));
if (flag) {
@@ -61,15 +59,31 @@ FlagDialog::FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent)
}
}
}
// Enable color picker on color field
ui->colorEdit->installEventFilter(this);
connect(ui->colorButton, &QPushButton::clicked, this, [this]() {
QColor color
= QColorDialog::getColor(QColor(ui->colorEdit->text()), this, tr("Select flag color"));
if (color.isValid()) {
ui->colorEdit->setText(color.name());
}
});
if (flag) {
setWindowTitle(tr("Edit Flag"));
ui->nameEdit->setText(flag->name);
ui->labelAction->setText(tr("Edit flag at %1").arg(RAddressString(offset)));
} else {
ui->labelAction->setText(tr("Add flag at %1").arg(RAddressString(offset)));
}
auto updateAcceptButton = [this]() {
bool validSize = false;
const ut64 size = ui->sizeEdit->text().toULongLong(&validSize);
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(!ui->nameEdit->text().trimmed().isEmpty() && validSize && size > 0);
};
connect(ui->nameEdit, &QLineEdit::textChanged, this, updateAcceptButton);
connect(ui->sizeEdit, &QLineEdit::textChanged, this, updateAcceptButton);
updateAcceptButton();
// Connect slots for OK/Cancel
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &FlagDialog::buttonBoxAccepted);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &FlagDialog::buttonBoxRejected);
@@ -88,34 +102,27 @@ FlagDialog::~FlagDialog() {}
void FlagDialog::buttonBoxAccepted()
{
RVA size = ui->sizeEdit->text().toULongLong();
QString name = ui->nameEdit->text();
bool validSize = false;
RVA size = ui->sizeEdit->text().toULongLong(&validSize);
QString name = ui->nameEdit->text().trimmed();
if (name.isEmpty() || !validSize || size == 0) {
return;
}
QString color = ui->colorEdit->text();
QString comment = ui->commentEdit->text();
if (flagOffset != RVA_INVALID) {
// Existing flag: remove old flag by name
Core()->delFlag(flagName);
// If a name is provided, recreate/update the flag with new attributes
if (!name.isEmpty()) {
Core()->addFlag(offset, name, size, color, comment);
}
} else {
// New flag: only add if name is provided
if (!name.isEmpty()) {
Core()->addFlag(offset, name, size, color, comment);
}
}
close();
setResult(QDialog::Accepted);
Core()->addFlag(offset, name, size, color, comment);
accept();
}
void FlagDialog::buttonBoxRejected()
{
close();
this->setResult(QDialog::Rejected);
reject();
}
std::pair<QAction *, QAction *> FlagDialog::addFlagMenuActions(
std::tuple<QAction *, QAction *, QAction *> FlagDialog::addFlagMenuActions(
QMenu *menu,
QWidget *dialogParent,
RVA addr,
@@ -145,19 +152,17 @@ std::pair<QAction *, QAction *> FlagDialog::addFlagMenuActions(
}
});
return {addAction, editAction};
}
bool FlagDialog::eventFilter(QObject *watched, QEvent *event)
{
// Open color picker when clicking on color field
if (watched == ui->colorEdit && event->type() == QEvent::MouseButtonPress) {
QColor initial = QColor(ui->colorEdit->text());
QColor c = QColorDialog::getColor(initial, this, tr("Select flag color"));
if (c.isValid()) {
ui->colorEdit->setText(c.name());
QAction *deleteAction = menu->addAction(tr("Delete flag"));
deleteAction->setEnabled(existing != nullptr);
QObject::connect(deleteAction, &QAction::triggered, dialogParent, [addr, onChanged]() {
RFlagItem *flag = r_flag_get_in(Core()->core()->flags, addr);
if (flag) {
Core()->delFlag(QString::fromUtf8(flag->name));
if (onChanged) {
onChanged();
}
}
return true;
}
return QDialog::eventFilter(watched, event);
});
return {addAction, editAction, deleteAction};
}
+5 -12
View File
@@ -4,7 +4,7 @@
#include "core/IaitoCommon.h"
#include <functional>
#include <memory>
#include <utility>
#include <tuple>
#include <QDialog>
class QAction;
@@ -19,18 +19,14 @@ class FlagDialog : public QDialog
Q_OBJECT
public:
// offset: address of flag; defaultSize: initial size to pre-fill (number of bytes)
// Constructor: offset is address; defaultSize optional size for new flags
// defaultSize pre-fills the byte count for new flags.
explicit FlagDialog(RVA offset, QWidget *parent = nullptr);
explicit FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent = nullptr);
~FlagDialog();
// Append an "Add flag..." / "Edit flag..." action pair to menu, wired to open
// FlagDialog at addr. Whichever action is inapplicable (add when a flag already
// exists, edit when none does) is created disabled. onChanged is invoked when
// the dialog is accepted (typically a refresh callback). Returns the two
// actions so callers can attach icons, etc.
static std::pair<QAction *, QAction *> addFlagMenuActions(
// Append add, edit and delete actions at addr, disabling inapplicable actions.
// onChanged is called after accepting the dialog or deleting the flag.
static std::tuple<QAction *, QAction *, QAction *> addFlagMenuActions(
QMenu *menu,
QWidget *dialogParent,
RVA addr,
@@ -48,9 +44,6 @@ private:
RVA offset;
QString flagName;
ut64 flagOffset;
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
};
#endif // FLAGDIALOG_H
+7
View File
@@ -107,6 +107,13 @@
<item>
<widget class="QLineEdit" name="colorEdit"/>
</item>
<item>
<widget class="QPushButton" name="colorButton">
<property name="text">
<string>Choose color...</string>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="colorPreview">
<property name="minimumSize">
+45 -29
View File
@@ -48,15 +48,19 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
: QDialog(parent)
, m_process(new QProcess(this))
{
setWindowTitle(tr("Package Manager"));
setWindowTitle(tr("Radare2 Package Manager (r2pm)"));
resize(900, 600);
auto *layout = new QVBoxLayout(this);
auto *filterLayout = new QHBoxLayout();
m_filterLineEdit = new QLineEdit(this);
m_filterLineEdit->setPlaceholderText(tr("Filter packages..."));
m_filterLineEdit->setMinimumHeight(32);
m_binaryPackagesCheckBox = new QCheckBox(tr("Binary packages"), this);
m_binaryPackagesCheckBox->setToolTip(
tr("Install ready-to-use packages, including scripts, without building from source."));
m_showAllPlatformsCheckBox = new QCheckBox(tr("Show all platforms"), this);
filterLayout->addWidget(m_filterLineEdit);
filterLayout->addWidget(m_binaryPackagesCheckBox);
filterLayout->addWidget(m_showAllPlatformsCheckBox);
layout->addLayout(filterLayout);
@@ -77,11 +81,15 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
auto *buttonLayout = new QHBoxLayout();
m_refreshButton = new QPushButton(tr("Refresh"), this);
m_installButton = new QPushButton(tr("Install"), this);
m_uninstallButton = new QPushButton(tr("Uninstall"), this);
m_installButton = new QPushButton(tr("Install"), this);
for (auto *button : {m_refreshButton, m_uninstallButton, m_installButton}) {
button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
}
buttonLayout->addWidget(m_refreshButton);
buttonLayout->addWidget(m_installButton);
buttonLayout->addStretch();
buttonLayout->addWidget(m_uninstallButton);
buttonLayout->addWidget(m_installButton);
layout->addLayout(buttonLayout);
m_logTextEdit = new QTextEdit(this);
@@ -90,7 +98,12 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
layout->addWidget(m_logTextEdit);
connect(m_filterLineEdit, &QLineEdit::textChanged, this, &PackageManagerDialog::filterPackages);
connect(m_showAllPlatformsCheckBox, &QCheckBox::toggled, this, [this]() { refreshPackages(); });
connect(m_binaryPackagesCheckBox, &QCheckBox::toggled, this, [this]() {
filterPackages(m_filterLineEdit->text());
});
connect(m_showAllPlatformsCheckBox, &QCheckBox::toggled, this, [this]() {
filterPackages(m_filterLineEdit->text());
});
connect(m_refreshButton, &QPushButton::clicked, this, &PackageManagerDialog::refreshPackages);
connect(m_installButton, &QPushButton::clicked, this, &PackageManagerDialog::installPackage);
connect(m_uninstallButton, &QPushButton::clicked, this, &PackageManagerDialog::uninstallPackage);
@@ -128,21 +141,18 @@ void PackageManagerDialog::refreshPackages()
// Then list packages
QProcess listProc;
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
if (m_showAllPlatformsCheckBox->isChecked()) {
environment.insert("R2PM_PLATFORM", "any");
} else {
environment.remove("R2PM_PLATFORM");
}
environment.remove("R2PM_PLATFORM");
listProc.setProcessEnvironment(environment);
listProc.start("r2pm", QStringList() << "-sj");
listProc.start("r2pm", QStringList() << "-Asj");
if (!listProc.waitForFinished(30000)) {
QMessageBox::warning(this, tr("Error"), processError(listProc, tr("Failed to run r2pm -sj.")));
QMessageBox::warning(
this, tr("Error"), processError(listProc, tr("Failed to run r2pm -Asj.")));
return;
}
QByteArray out = listProc.readAllStandardOutput();
QJsonDocument doc = QJsonDocument::fromJson(out);
if (!doc.isArray()) {
QMessageBox::warning(this, tr("Error"), tr("Invalid JSON output from r2pm -sj"));
QMessageBox::warning(this, tr("Error"), tr("Invalid JSON output from r2pm -Asj"));
return;
}
QJsonArray array = doc.array();
@@ -158,19 +168,11 @@ void PackageManagerDialog::refreshPackages()
if (desc == "") {
desc = obj["desc"].toString();
}
QStringList platformTags;
QStringList platformNames;
QStringList platforms;
for (const QJsonValue &platformValue : obj["platforms"].toArray()) {
const QString platform = platformValue.toString();
if (platform == "windows") {
platformTags << "w";
platformNames << tr("Windows");
} else if (platform == "unix") {
platformTags << "u";
platformNames << tr("Unix");
} else if (platform == "qjs") {
platformTags << "j";
platformNames << tr("QuickJS");
if (!platform.isEmpty()) {
platforms << platform;
}
}
int row = m_tableWidget->rowCount();
@@ -180,11 +182,13 @@ void PackageManagerDialog::refreshPackages()
itemInstalled->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
m_tableWidget->setItem(row, 0, itemInstalled);
QTableWidgetItem *itemName = new QTableWidgetItem(name);
itemName->setData(Qt::UserRole, obj["binary"].toBool());
itemName->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
m_tableWidget->setItem(row, 1, itemName);
QTableWidgetItem *itemPlatforms = new QTableWidgetItem(platformTags.join(' '));
itemPlatforms->setToolTip(platformNames.join(", "));
itemPlatforms->setTextAlignment(Qt::AlignCenter);
QTableWidgetItem *itemPlatforms = new QTableWidgetItem(platforms.join(' '));
itemPlatforms->setData(Qt::UserRole, obj["supported"].toBool(true));
itemPlatforms->setToolTip(platforms.join(", "));
itemPlatforms->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
itemPlatforms->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
m_tableWidget->setItem(row, 2, itemPlatforms);
QTableWidgetItem *itemDesc = new QTableWidgetItem(desc);
@@ -216,29 +220,41 @@ void PackageManagerDialog::filterPackages(const QString &text)
|| m_tableWidget->item(row, 1)->text().contains(text, Qt::CaseInsensitive)
|| m_tableWidget->item(row, 2)->text().contains(text, Qt::CaseInsensitive)
|| m_tableWidget->item(row, 3)->text().contains(text, Qt::CaseInsensitive);
if (m_binaryPackagesCheckBox->isChecked()) {
visible = visible && m_tableWidget->item(row, 1)->data(Qt::UserRole).toBool();
}
if (!m_showAllPlatformsCheckBox->isChecked()) {
visible = visible && m_tableWidget->item(row, 2)->data(Qt::UserRole).toBool();
}
m_tableWidget->setRowHidden(row, !visible);
}
if (m_tableWidget->currentRow() >= 0
&& m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
m_tableWidget->setCurrentItem(nullptr);
m_tableWidget->clearSelection();
}
}
void PackageManagerDialog::installPackage()
{
if (m_process->state() != QProcess::NotRunning)
return;
if (m_tableWidget->currentRow() < 0) {
if (m_tableWidget->currentRow() < 0 || m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
QMessageBox::information(this, tr("Install"), tr("Please select a package to install."));
return;
}
QString pkg = m_tableWidget->item(m_tableWidget->currentRow(), 1)->text();
m_logTextEdit->clear();
m_logTextEdit->setVisible(true);
m_process->start("r2pm", QStringList() << "-ci" << pkg);
const QString flags = m_binaryPackagesCheckBox->isChecked() ? "-bi" : "-ci";
m_process->start("r2pm", QStringList() << flags << pkg);
}
void PackageManagerDialog::uninstallPackage()
{
if (m_process->state() != QProcess::NotRunning)
return;
if (m_tableWidget->currentRow() < 0) {
if (m_tableWidget->currentRow() < 0 || m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
QMessageBox::information(this, tr("Uninstall"), tr("Please select a package to uninstall."));
return;
}
+1
View File
@@ -43,6 +43,7 @@ private slots:
private:
QLineEdit *m_filterLineEdit;
QCheckBox *m_binaryPackagesCheckBox;
QCheckBox *m_showAllPlatformsCheckBox;
QTableWidget *m_tableWidget;
QPushButton *m_refreshButton;
+3 -3
View File
@@ -50,7 +50,7 @@
</widget>
</item>
<item>
<widget class="AddressableItemList&lt;&gt;" name="toTreeWidget">
<widget class="AddressableItemView" name="toTreeWidget">
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
@@ -73,7 +73,7 @@
</widget>
</item>
<item>
<widget class="AddressableItemList&lt;&gt;" name="fromTreeWidget">
<widget class="AddressableItemView" name="fromTreeWidget">
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
@@ -142,7 +142,7 @@
</widget>
<customwidgets>
<customwidget>
<class>AddressableItemList&lt;&gt;</class>
<class>AddressableItemView</class>
<extends>QTreeView</extends>
<header>widgets/AddressableItemList.h</header>
</customwidget>
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" stroke="#888" stroke-width="1.8" stroke-linecap="round">
<path d="M9 5h11M9 12h11M9 19h11"/>
<path d="M4 5h.5M4 12h.5M4 19h.5" stroke-width="3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 263 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" stroke="#888" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 5v14m0-7h5m-5 7h5M10 5h9m-5 7h5m-5 7h5"/>
<rect x="3" y="3" width="4" height="4" fill="#888" stroke="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 319 B

+7 -1
View File
@@ -210,6 +210,9 @@ void AddressableItemContextMenu::onActionAddComment()
void AddressableItemContextMenu::aboutToShowSlot()
{
if (!hasTarget) {
return;
}
if (actionShowInMenu->menu()) {
actionShowInMenu->menu()->deleteLater();
}
@@ -225,6 +228,9 @@ void AddressableItemContextMenu::setHasTarget(bool hasTarget)
{
this->hasTarget = hasTarget;
for (const auto &action : this->actions()) {
action->setEnabled(hasTarget);
// View options also apply to group rows and empty lists.
if (!action->property("addressIndependent").toBool()) {
action->setEnabled(hasTarget);
}
}
}
+131
View File
@@ -0,0 +1,131 @@
#ifndef CONTEXTMENUICONS_H
#define CONTEXTMENUICONS_H
#include <QAction>
#include <QColor>
#include <QIcon>
#include <QPainter>
#include <QPixmap>
#include <QPolygonF>
#include <QVector>
#include <QWidget>
namespace ContextMenuIcons {
enum class Icon {
Copy,
Navigation,
XRefs,
Comment,
Rename,
Delete,
Edit,
View,
Breakpoint,
Debug,
Fold,
Collapse,
Expand,
Sync
};
inline void setIcon(const QWidget *widget, QAction *action, Icon icon, const QColor &color)
{
const qreal ratio = widget->devicePixelRatioF();
const int size = 16;
QPixmap pixmap(size * ratio, size * ratio);
pixmap.setDevicePixelRatio(ratio);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
QColor accent = color;
accent.setAlpha(205);
painter.setPen(QPen(accent, 1.8, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
painter.setBrush(Qt::NoBrush);
switch (icon) {
case Icon::Copy:
painter.drawRect(QRectF(5, 3.5, 7, 9));
painter.drawRect(QRectF(3, 5.5, 7, 7));
break;
case Icon::Navigation:
painter.drawLine(QPointF(4, 8), QPointF(11, 8));
painter.drawPolyline(
QPolygonF(QVector<QPointF>{QPointF(8, 5), QPointF(11, 8), QPointF(8, 11)}));
break;
case Icon::XRefs:
painter.drawEllipse(QPointF(5, 8), 2.0, 2.0);
painter.drawEllipse(QPointF(11, 5), 2.0, 2.0);
painter.drawEllipse(QPointF(11, 11), 2.0, 2.0);
painter.drawLine(QPointF(6.7, 7), QPointF(9.3, 5.8));
painter.drawLine(QPointF(6.7, 9), QPointF(9.3, 10.2));
break;
case Icon::Comment:
painter.drawRoundedRect(QRectF(3.5, 4, 9, 7), 1.5, 1.5);
painter.drawLine(QPointF(6, 11), QPointF(4.5, 13));
break;
case Icon::Rename:
painter.drawLine(QPointF(4, 12), QPointF(12, 4));
painter.drawLine(QPointF(10, 4), QPointF(12, 6));
painter.drawLine(QPointF(4, 12), QPointF(7, 11));
break;
case Icon::Delete:
painter.drawLine(QPointF(4, 5), QPointF(12, 5));
painter.drawLine(QPointF(6, 3.5), QPointF(10, 3.5));
painter.drawRoundedRect(QRectF(5, 6, 6, 7), 1.0, 1.0);
painter.drawLine(QPointF(7, 7.5), QPointF(7, 11.5));
painter.drawLine(QPointF(9, 7.5), QPointF(9, 11.5));
break;
case Icon::Edit:
painter.drawRect(QRectF(4, 4, 8, 8));
painter.drawLine(QPointF(6, 7), QPointF(10, 7));
painter.drawLine(QPointF(6, 9), QPointF(9, 9));
break;
case Icon::View:
painter.drawEllipse(QRectF(3, 5, 10, 6));
painter.drawEllipse(QPointF(8, 8), 1.8, 1.8);
break;
case Icon::Breakpoint:
painter.setBrush(accent);
painter.drawEllipse(QPointF(8, 8), 4.0, 4.0);
break;
case Icon::Debug:
painter.drawLine(QPointF(8, 3), QPointF(8, 13));
painter.drawLine(QPointF(4, 5), QPointF(12, 5));
painter.drawLine(QPointF(4, 11), QPointF(12, 11));
painter.drawRoundedRect(QRectF(5, 4, 6, 8), 1.5, 1.5);
break;
case Icon::Fold:
painter.drawPolyline(
QPolygonF(QVector<QPointF>{QPointF(4, 3), QPointF(8, 7), QPointF(12, 3)}));
painter.drawPolyline(
QPolygonF(QVector<QPointF>{QPointF(4, 13), QPointF(8, 9), QPointF(12, 13)}));
break;
case Icon::Collapse:
case Icon::Expand:
painter.drawRoundedRect(QRectF(3, 3, 10, 10), 1.5, 1.5);
painter.drawLine(QPointF(5.5, 8), QPointF(10.5, 8));
if (icon == Icon::Expand) {
painter.drawLine(QPointF(8, 5.5), QPointF(8, 10.5));
}
break;
case Icon::Sync:
painter.drawLine(QPointF(3, 5), QPointF(12, 5));
painter.drawPolyline(
QPolygonF(QVector<QPointF>{QPointF(10, 3), QPointF(12, 5), QPointF(10, 7)}));
painter.drawLine(QPointF(13, 11), QPointF(4, 11));
painter.drawPolyline(
QPolygonF(QVector<QPointF>{QPointF(6, 9), QPointF(4, 11), QPointF(6, 13)}));
break;
}
painter.end();
action->setIcon(QIcon(pixmap));
action->setIconVisibleInMenu(true);
}
} // namespace ContextMenuIcons
#endif // CONTEXTMENUICONS_H
+93 -14
View File
@@ -1,4 +1,5 @@
#include "DecompilerContextMenu.h"
#include "ContextMenuIcons.h"
#include "MainWindow.h"
#include "common/DeepLink.h"
#include "common/ShortcutManager.h"
@@ -19,6 +20,7 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
, curHighlightedWord(QString())
, offset(0)
, decompiledFunctionAddress(RVA_INVALID)
, firstOffsetInLine(RVA_INVALID)
, isTogglingBreakpoints(false)
, annotationHere(nullptr)
, actionCopy(tr("Copy"), this)
@@ -33,7 +35,8 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
, actionDeleteName(tr("Delete <name>"), this)
, actionEditFunctionVariables(tr("Edit variable <name of variable>"), this)
, actionXRefs(tr("Show X-Refs"), this)
, actionShowAddress(tr("Show address"), this)
, actionShowAddress(tr("Show addresses"), this)
, actionShowDisassembly(tr("Show disassembly"), this)
, actionToggleBreakpoint(tr("Add/remove breakpoint"), this)
, actionAdvancedBreakpoint(tr("Advanced breakpoint"), this)
, breakpointsInLineMenu(new QMenu(this))
@@ -54,6 +57,7 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
setActionXRefs();
setActionShowAddress();
setActionShowDisassembly();
setActionEditFunctionVariables();
@@ -64,6 +68,36 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
// actionEditAnnotation.setVisible(true);
// actionEditAnnotation.setEnabled(true);
using ContextMenuIcons::Icon;
using ContextMenuIcons::setIcon;
const QColor copyColor(56, 142, 60);
const QColor navigationColor(103, 80, 164);
const QColor annotationColor(67, 160, 71);
const QColor analysisColor(0, 137, 190);
const QColor viewColor(191, 128, 32);
const QColor debugColor(216, 88, 64);
setIcon(this, &actionCopy, Icon::Copy, copyColor);
setIcon(this, &actionCopyInstructionAddress, Icon::Copy, copyColor);
setIcon(this, &actionCopyReferenceAddress, Icon::Copy, copyColor);
setIcon(this, &actionCopyDeepLink, Icon::Copy, copyColor);
setIcon(this, &actionShowInSubmenu, Icon::Navigation, navigationColor);
setIcon(this, &actionAddComment, Icon::Comment, annotationColor);
setIcon(this, &actionDeleteComment, Icon::Delete, debugColor);
setIcon(this, &actionRenameThingHere, Icon::Rename, annotationColor);
setIcon(this, &actionDeleteName, Icon::Delete, debugColor);
setIcon(this, &actionEditFunctionVariables, Icon::Edit, analysisColor);
setIcon(this, &actionXRefs, Icon::XRefs, analysisColor);
setIcon(this, &actionShowAddress, Icon::View, viewColor);
setIcon(this, &actionShowDisassembly, Icon::View, viewColor);
setIcon(this, breakpointMenu->menuAction(), Icon::Breakpoint, debugColor);
setIcon(this, &actionToggleBreakpoint, Icon::Breakpoint, debugColor);
setIcon(this, &actionAdvancedBreakpoint, Icon::Edit, debugColor);
setIcon(this, debugMenu->menuAction(), Icon::Debug, debugColor);
setIcon(this, &actionContinueUntil, Icon::Navigation, debugColor);
setIcon(this, &actionSetPC, Icon::Debug, debugColor);
setIcon(this, &actionEditAnnotation, Icon::Edit, annotationColor);
setShortcutContextInActions(this);
connect(
@@ -100,6 +134,8 @@ void DecompilerContextMenu::setDecompiledFunctionAddress(RVA functionAddr)
void DecompilerContextMenu::setFirstOffsetInLine(RVA firstOffset)
{
firstOffsetInLine = firstOffset;
actionAddComment.setEnabled(firstOffset != RVA_INVALID);
actionDeleteComment.setEnabled(firstOffset != RVA_INVALID);
}
RVA DecompilerContextMenu::getFirstOffsetInLine()
@@ -117,6 +153,8 @@ void DecompilerContextMenu::setupBreakpointsInLineMenu()
breakpointsInLineMenu->clear();
for (auto curOffset : this->availableBreakpoints) {
QAction *action = breakpointsInLineMenu->addAction(RAddressString(curOffset));
action->setIcon(actionToggleBreakpoint.icon());
action->setIconVisibleInMenu(true);
connect(action, &QAction::triggered, this, [this, curOffset] {
BreakpointsDialog::editBreakpoint(Core()->getBreakpointAt(curOffset), this);
});
@@ -163,14 +201,15 @@ void DecompilerContextMenu::aboutToHideSlot()
void DecompilerContextMenu::aboutToShowSlot()
{
DecompilerWidget *decompilerWidget = qobject_cast<DecompilerWidget *>(parentWidget());
actionShowAddress.setText(
decompilerWidget && decompilerWidget->isAddressColumnVisible() ? tr("Hide address")
: tr("Show address"));
actionShowAddress.setChecked(decompilerWidget && decompilerWidget->isAddressColumnVisible());
actionShowDisassembly.setChecked(
decompilerWidget && decompilerWidget->isDisassemblyColumnVisible());
actionAddComment.setVisible(true);
actionAddComment.setEnabled(firstOffsetInLine != RVA_INVALID);
if (this->firstOffsetInLine != RVA_MAX) {
actionShowInSubmenu.setVisible(true);
QString comment = Core()->cmdRawAt("CC.", this->firstOffsetInLine);
actionAddComment.setVisible(true);
if (comment.isEmpty()) {
actionDeleteComment.setVisible(false);
actionAddComment.setText(tr("Add Comment"));
@@ -180,7 +219,7 @@ void DecompilerContextMenu::aboutToShowSlot()
}
} else {
actionShowInSubmenu.setVisible(false);
actionAddComment.setVisible(false);
actionAddComment.setText(tr("Add Comment"));
actionDeleteComment.setVisible(false);
}
@@ -310,6 +349,8 @@ void DecompilerContextMenu::setActionShowInSubmenu()
void DecompilerContextMenu::setActionAddComment()
{
actionAddComment.setObjectName(QStringLiteral("actionAddComment"));
actionAddComment.setEnabled(firstOffsetInLine != RVA_INVALID);
connect(
&actionAddComment,
&QAction::triggered,
@@ -321,6 +362,7 @@ void DecompilerContextMenu::setActionAddComment()
void DecompilerContextMenu::setActionDeleteComment()
{
actionDeleteComment.setObjectName(QStringLiteral("actionDeleteComment"));
connect(
&actionDeleteComment,
&QAction::triggered,
@@ -338,15 +380,32 @@ void DecompilerContextMenu::setActionXRefs()
void DecompilerContextMenu::setActionShowAddress()
{
connect(&actionShowAddress, &QAction::triggered, this, [this]() {
actionShowAddress.setObjectName(QStringLiteral("actionShowAddresses"));
actionShowAddress.setCheckable(true);
actionShowAddress.setToolTip(tr("Show the address associated with each decompiled line."));
connect(&actionShowAddress, &QAction::triggered, this, [this](bool checked) {
DecompilerWidget *decompilerWidget = qobject_cast<DecompilerWidget *>(parentWidget());
if (decompilerWidget) {
decompilerWidget->setAddressColumnVisible(!decompilerWidget->isAddressColumnVisible());
decompilerWidget->setAddressColumnVisible(checked);
}
});
addAction(&actionShowAddress);
}
void DecompilerContextMenu::setActionShowDisassembly()
{
actionShowDisassembly.setObjectName(QStringLiteral("actionShowDisassembly"));
actionShowDisassembly.setCheckable(true);
actionShowDisassembly.setToolTip(tr("Show disassembly beside the decompiled code, like pdca."));
connect(&actionShowDisassembly, &QAction::triggered, this, [this](bool checked) {
DecompilerWidget *decompilerWidget = qobject_cast<DecompilerWidget *>(parentWidget());
if (decompilerWidget) {
decompilerWidget->setDisassemblyColumnVisible(checked);
}
});
addAction(&actionShowDisassembly);
}
void DecompilerContextMenu::setActionRenameThingHere()
{
ShortcutMgr()->bindAction("decompiler.rename", &actionRenameThingHere);
@@ -435,12 +494,21 @@ void DecompilerContextMenu::actionCopyInstructionAddressTriggered()
void DecompilerContextMenu::actionEditAnnotationTriggered()
{
QString os = Core()->cmdRaw("anos");
QString *s = openTextEditDialog(os, this);
if (s != nullptr) {
Core()->cmdRaw(QStringLiteral("ano=base64:%1").arg(QString(s->toLocal8Bit().toBase64())));
this->mainWindow->refreshAll();
const RVA functionAddress = decompiledFunctionAddress;
if (functionAddress == RVA_INVALID) {
return;
}
TextEditDialog dialog(Core()->cmdRawAt("anos", functionAddress), this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
const QString text = dialog.getEditedText();
const QString command
= text.isEmpty()
? QStringLiteral("ano-")
: QStringLiteral("ano=base64:%1").arg(QString::fromLatin1(text.toUtf8().toBase64()));
Core()->cmdRawAt(command, functionAddress);
this->mainWindow->refreshAll();
}
void DecompilerContextMenu::actionCopyReferenceAddressTriggered()
@@ -456,11 +524,20 @@ void DecompilerContextMenu::actionCopyDeepLinkTriggered()
void DecompilerContextMenu::actionAddCommentTriggered()
{
CommentsDialog::addOrEditComment(this->firstOffsetInLine, this);
if (firstOffsetInLine == RVA_INVALID) {
return;
}
CommentsDialog::addOrEditComment(this->firstOffsetInLine, parentWidget());
if (auto *widget = qobject_cast<DecompilerWidget *>(parentWidget())) {
widget->setFocus(Qt::OtherFocusReason);
}
}
void DecompilerContextMenu::actionDeleteCommentTriggered()
{
if (firstOffsetInLine == RVA_INVALID) {
return;
}
Core()->delComment(this->firstOffsetInLine);
}
@@ -676,6 +753,8 @@ void DecompilerContextMenu::updateTargetMenuActions()
RAddressString(annotationHere->reference.offset));
}
auto action = new QAction(name, this);
action->setIcon(actionShowInSubmenu.icon());
action->setIconVisibleInMenu(true);
showTargetMenuActions.append(action);
action->setMenu(menu);
insertActions(copySeparator, showTargetMenuActions);
+2
View File
@@ -105,6 +105,7 @@ private:
QAction actionXRefs;
QAction actionShowAddress;
QAction actionShowDisassembly;
QMenu *breakpointMenu;
QAction actionToggleBreakpoint;
@@ -137,6 +138,7 @@ private:
void setActionXRefs();
void setActionShowAddress();
void setActionShowDisassembly();
void setActionRenameThingHere();
void setActionDeleteName();
+50 -32
View File
@@ -52,8 +52,7 @@ DisassemblyContextMenu::DisassemblyContextMenu(QWidget *parent, MainWindow *main
, actionAnalyzeFunction(this)
, actionEditFunction(this)
, actionRename(this)
, actionAddFlag(this)
, actionEditFlag(this)
, actionFlag(this)
, actionAddFlagAtRef(this)
, actionEditFlagAtRef(this)
, actionSetFunctionVarTypes(this)
@@ -170,6 +169,15 @@ void DisassemblyContextMenu::registerActionShortcuts()
ShortcutMgr()->bindAction("disasm.undefineFunction", &actionDeleteFunction);
}
void DisassemblyContextMenu::registerFlagShortcut()
{
ShortcutMgr()->bindAction("disasm.flag", &actionFlag);
actionFlag.setShortcutContext(Qt::WidgetWithChildrenShortcut);
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
actionFlag.setShortcutVisibleInContextMenu(true);
#endif
}
DisassemblyContextMenu::~DisassemblyContextMenu() {}
QIcon DisassemblyContextMenu::makeMenuIcon(MenuIcon icon, const QColor &color) const
@@ -644,25 +652,25 @@ void DisassemblyContextMenu::addFlagActions()
flagMenu = addMenu(tr("Flag"));
setMenuIcon(flagMenu, MenuIcon::Tag, flagColor);
initAction(&actionAddFlag, tr("Add flag here"), nullptr);
setActionIcon(&actionAddFlag, MenuIcon::Tag, flagColor);
connect(&actionAddFlag, &QAction::triggered, this, [this]() {
FlagDialog dlg(offset, 1, this->mainWindow);
initAction(&actionFlag, tr("Add / edit flag here"));
setActionIcon(&actionFlag, MenuIcon::Tag, flagColor);
connect(&actionFlag, &QAction::triggered, this, [this]() {
emit flagAboutToChange();
const bool hasSelection = selectionStart != RVA_INVALID && selectionByteCount > 0;
FlagDialog
dlg(hasSelection ? selectionStart : offset,
hasSelection ? static_cast<ut64>(selectionByteCount) : 1,
mainWindow);
if (dlg.exec() == QDialog::Accepted && this->mainWindow) {
this->mainWindow->refreshAll();
}
});
flagMenu->addAction(&actionAddFlag);
flagMenu->addAction(&actionFlag);
initAction(&actionEditFlag, tr("Edit flag here"), nullptr);
setActionIcon(&actionEditFlag, MenuIcon::Tag, flagColor);
connect(&actionEditFlag, &QAction::triggered, this, [this]() {
FlagDialog dlg(offset, 1, this->mainWindow);
if (dlg.exec() == QDialog::Accepted && this->mainWindow) {
this->mainWindow->refreshAll();
}
});
flagMenu->addAction(&actionEditFlag);
initAction(&actionDeleteFlag, tr("Delete flag"), SLOT(on_actionDeleteFlag_triggered()));
setActionIcon(&actionDeleteFlag, MenuIcon::Tag, QColor(117, 117, 117));
flagMenu->addAction(&actionDeleteFlag);
flagMenu->addSeparator();
initAction(&actionAddFlagAtRef, tr("Add flag at reference"), nullptr);
setActionIcon(&actionAddFlagAtRef, MenuIcon::Tag, flagColor);
@@ -694,11 +702,12 @@ void DisassemblyContextMenu::addFlagActions()
void DisassemblyContextMenu::refreshFlagActions()
{
RFlag *rf = Core()->core()->flags;
RFlagItem *here = rf ? r_flag_get_in(rf, offset) : nullptr;
actionAddFlag.setText(tr("Add flag at %1").arg(RAddressString(offset)));
actionAddFlag.setEnabled(here == nullptr);
actionEditFlag.setText(tr("Edit flag at %1").arg(RAddressString(offset)));
actionEditFlag.setEnabled(here != nullptr);
const RVA address = selectionStart != RVA_INVALID ? selectionStart : offset;
RFlagItem *here = rf ? r_flag_get_in(rf, address) : nullptr;
actionFlag.setText(
(here ? tr("Edit flag at %1") : tr("Add flag at %1")).arg(RAddressString(address)));
actionDeleteFlag.setText(tr("Delete flag at %1").arg(RAddressString(address)));
actionDeleteFlag.setEnabled(here != nullptr);
ut64 ref = curHighlightedWord.isEmpty() ? 0 : Core()->num(curHighlightedWord);
hasFlagRefAddr = (ref != 0 && ref != offset);
@@ -733,9 +742,6 @@ void DisassemblyContextMenu::addEditMenu()
setActionIcon(&actionDeleteComment, MenuIcon::Comment, QColor(67, 160, 71));
editMenu->addAction(&actionDeleteComment);
initAction(&actionDeleteFlag, tr("Delete flag"), SLOT(on_actionDeleteFlag_triggered()));
setActionIcon(&actionDeleteFlag, MenuIcon::Tag, QColor(117, 117, 117));
editMenu->addAction(&actionDeleteFlag);
editMenu->addSeparator();
initAction(&actionEditInstruction, tr("Instruction"), SLOT(on_actionEditInstruction_triggered()));
@@ -878,7 +884,6 @@ void DisassemblyContextMenu::buildRenameMenu(ThingUsedHere *tuh)
return;
}
actionDeleteFlag.setVisible(false);
// TODO: use switch
if (tuh->type == ThingUsedHere::Type::Address) {
// Adding a flag at a referenced address is handled by the dedicated
@@ -902,7 +907,6 @@ void DisassemblyContextMenu::buildRenameMenu(ThingUsedHere *tuh)
doRenameInfo.name = tuh->name;
doRenameInfo.addr = tuh->offset;
actionRename.setText(tr("Rename flag \"%1\" (used here)").arg(doRenameInfo.name));
actionDeleteFlag.setVisible(true);
} else {
qWarning() << "Unexpected renaming type";
doRenameAction = RENAME_DO_NOTHING;
@@ -1403,12 +1407,21 @@ void DisassemblyContextMenu::on_actionAnalyzeFunction_triggered()
void DisassemblyContextMenu::on_actionEditAnnotation_triggered()
{
QString os = Core()->cmdRaw("anos");
QString *s = openTextEditDialog(os, this);
if (s != nullptr) {
Core()->cmdRaw(QStringLiteral("ano=base64:%1").arg(QString(s->toLocal8Bit().toBase64())));
this->mainWindow->refreshAll();
const RVA functionAddress = Core()->getFunctionStart(offset);
if (functionAddress == RVA_INVALID) {
return;
}
TextEditDialog dialog(Core()->cmdRawAt("anos", functionAddress), this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
const QString text = dialog.getEditedText();
const QString command
= text.isEmpty()
? QStringLiteral("ano-")
: QStringLiteral("ano=base64:%1").arg(QString::fromLatin1(text.toUtf8().toBase64()));
Core()->cmdRawAt(command, functionAddress);
this->mainWindow->refreshAll();
}
void DisassemblyContextMenu::on_actionRename_triggered()
@@ -1625,7 +1638,12 @@ void DisassemblyContextMenu::on_actionDeleteComment_triggered()
void DisassemblyContextMenu::on_actionDeleteFlag_triggered()
{
Core()->delFlag(offset);
emit flagAboutToChange();
const RVA address = selectionStart != RVA_INVALID ? selectionStart : offset;
RFlagItem *flag = r_flag_get_in(Core()->core()->flags, address);
if (flag) {
Core()->delFlag(QString::fromUtf8(flag->name));
}
}
void DisassemblyContextMenu::on_actionDeleteFunction_triggered()
+5 -2
View File
@@ -19,7 +19,11 @@ public:
DisassemblyContextMenu(QWidget *parent, MainWindow *mainWindow);
~DisassemblyContextMenu();
// The graph view shares this menu and uses F for branch navigation.
void registerFlagShortcut();
signals:
void flagAboutToChange();
void copy();
/**
* @brief Signal emitted to copy raw bytes of selected instructions
@@ -177,8 +181,7 @@ private:
QAction actionAnalyzeFunction;
QAction actionEditFunction;
QAction actionRename;
QAction actionAddFlag;
QAction actionEditFlag;
QAction actionFlag;
QAction actionAddFlagAtRef;
QAction actionEditFlagAtRef;
RVA flagRefAddr = 0;
+29
View File
@@ -42,6 +42,35 @@
<name>radare</name>
</developer>
<releases>
<release version="6.2.6" date="2026-11-06">
<description>
<p>Fix disasm lines glitch</p>
<p>Colorize basic blocks in minigraph</p>
<p>Toggle decompiler with tab and graph with space</p>
<p>Decai decompiler options</p>
<p>Global variable edition and navigation</p>
<p>Filesystem browser</p>
<p>Several bug fixes and optimizations</p>
<p>Zoom panels with meta+enter</p>
<p>Switch tabs with cmd+number</p>
<p>Cycle dock frames with cmd+JK</p>
<p>Hover indicators in disasm and decompile</p>
<p>Per-view seek history fixes</p>
<p>Folding braced blocks in decompiler</p>
</description>
</release>
<release version="6.2.4" date="2026-10-05">
<description>
<p>Collapse sparse gaps in the hexdump</p>
<p>TryCatch analysis via disasm and graph</p>
<p>View symbols, class, imports as tree</p>
<p>Compact large symbol names grouped by prefix</p>
<p>Optimize listings filtering</p>
<p>New advanced data analysis widget</p>
<p>Hand cursor over clickable items in disasm</p>
<p>Enable the bounded scrollbar by default</p>
</description>
</release>
<release version="6.2.2" date="2026-09-06">
<description>
<p>Scrollbars in disasm and hexdump</p>
+2
View File
@@ -100,6 +100,8 @@
<file>img/icons/delete.svg</file>
<file>img/icons/previous.svg</file>
<file>img/icons/list.svg</file>
<file>img/icons/name-tree.svg</file>
<file>img/icons/name-list.svg</file>
<file>img/icons/fork.svg</file>
<file>img/icons/plugins.svg</file>
<file>img/icons/light/plugins.svg</file>
+2 -2
View File
@@ -25,8 +25,8 @@ spacing=6
sliderGroove=4
[palette]
accent=#039be5
accentText=#ffffff
accent=#c6dff2
accentText=#00162e
background=#fafcfe
border=#bdbdbd
mutedText=#7d7d7d
+2 -1
View File
@@ -161,7 +161,8 @@ void AddressScrollBar::applyMode()
void AddressScrollBar::applyRange()
{
const RVA span = rangeEnd - rangeStart;
unit = span > MaxSliderSpan ? (span + MaxSliderSpan - 1) / MaxSliderSpan : 1;
// Round up without overflowing when the range spans the full address space.
unit = span > MaxSliderSpan ? (span - 1) / MaxSliderSpan + 1 : 1;
syncing = true;
setRange(0, int(span / unit));
setPageStep(qMax<int>(1, int(pageBytes / unit)));
+94
View File
@@ -4,7 +4,11 @@
#include <QAction>
#include <QContextMenuEvent>
#include <QEvent>
#include <QLabel>
#include <QMenu>
#include <QStyle>
#include <QToolBar>
#include <QVBoxLayout>
AddressableDockWidget::AddressableDockWidget(MainWindow *parent)
: IaitoDockWidget(parent)
@@ -19,9 +23,99 @@ AddressableDockWidget::AddressableDockWidget(MainWindow *parent)
dockMenu = new QMenu(this);
dockMenu->addAction(&syncAction);
connect(seekable, &IaitoSeekable::syncChanged, this, [this]() {
if (!seekable->isSynchronized()) {
setupSeekHistoryBar();
}
updateSeekHistoryBar();
});
connect(
seekable,
&IaitoSeekable::historyChanged,
this,
&AddressableDockWidget::updateSeekHistoryBar);
connect(
seekable,
&IaitoSeekable::seekableSeekChanged,
this,
&AddressableDockWidget::updateSeekHistoryBar);
connect(this, &QDockWidget::visibilityChanged, this, [this](bool visible) {
if (visible) {
setupSeekHistoryBar();
}
});
setContextMenuPolicy(Qt::ContextMenuPolicy::DefaultContextMenu);
}
void AddressableDockWidget::setupSeekHistoryBar(bool alwaysVisible)
{
historyBarAlwaysVisible |= alwaysVisible;
if (seekHistoryBar || !widget()) {
updateSeekHistoryBar();
return;
}
QWidget *contents = widget();
seekHistoryBar = new QToolBar(contents);
seekHistoryBar->setObjectName(QStringLiteral("seekHistoryBar"));
seekHistoryBar->setIconSize(QSize(16, 16));
seekHistoryBar->setMovable(false);
seekHistoryBar->setFloatable(false);
seekHistoryBar->setFocusPolicy(Qt::NoFocus);
seekBackAction
= seekHistoryBar->addAction(style()->standardIcon(QStyle::SP_ArrowBack), tr("Back"));
seekBackAction->setObjectName(QStringLiteral("seekBackAction"));
seekForwardAction
= seekHistoryBar->addAction(style()->standardIcon(QStyle::SP_ArrowForward), tr("Forward"));
seekForwardAction->setObjectName(QStringLiteral("seekForwardAction"));
connect(seekBackAction, &QAction::triggered, seekable, &IaitoSeekable::seekPrev);
connect(seekForwardAction, &QAction::triggered, seekable, &IaitoSeekable::seekNext);
for (QAction *action : {seekBackAction, seekForwardAction}) {
seekHistoryBar->widgetForAction(action)->setFocusPolicy(Qt::NoFocus);
}
seekAddressLabel = new QLabel(seekHistoryBar);
seekAddressLabel->setMargin(3);
seekHistoryBar->addWidget(seekAddressLabel);
if (auto *layout = qobject_cast<QVBoxLayout *>(contents->layout())) {
layout->insertWidget(0, seekHistoryBar);
} else {
// Graph-only docks have no content layout in which to insert the bar.
auto *container = new QWidget(this);
auto *containerLayout = new QVBoxLayout(container);
containerLayout->setContentsMargins(0, 0, 0, 0);
containerLayout->setSpacing(0);
containerLayout->addWidget(seekHistoryBar);
containerLayout->addWidget(contents);
setWidget(container);
contents->show();
}
updateSeekHistoryBar();
}
void AddressableDockWidget::setSeekHistoryBarEnabled(bool enabled)
{
historyBarEnabled = enabled;
updateSeekHistoryBar();
}
void AddressableDockWidget::updateSeekHistoryBar()
{
if (!seekHistoryBar) {
return;
}
const bool synced = seekable->isSynchronized();
seekHistoryBar->setVisible(historyBarEnabled && (historyBarAlwaysVisible || !synced));
seekBackAction->setEnabled(seekable->canSeekPrev());
seekForwardAction->setEnabled(seekable->canSeekNext());
seekBackAction->setToolTip(synced ? tr("Back in shared seek history") : tr("Back in this view"));
seekForwardAction->setToolTip(
synced ? tr("Forward in shared seek history") : tr("Forward in this view"));
seekAddressLabel->setText(RAddressString(seekable->getOffset()));
seekAddressLabel->setToolTip(
synced ? tr("Shared offset") : tr("Independent offset for this view"));
}
QVariantMap AddressableDockWidget::serializeViewProprties()
{
auto result = IaitoDockWidget::serializeViewProprties();
+15
View File
@@ -7,6 +7,8 @@
#include <QAction>
class IaitoSeekable;
class QLabel;
class QToolBar;
class AddressableDockWidget : public IaitoDockWidget
{
@@ -28,8 +30,21 @@ protected:
QAction syncAction;
QMenu *dockMenu = nullptr;
void setupSeekHistoryBar(bool alwaysVisible = false);
void setSeekHistoryBarEnabled(bool enabled);
virtual QString getWindowTitle() const = 0;
void contextMenuEvent(QContextMenuEvent *event) override;
private:
QToolBar *seekHistoryBar = nullptr;
QLabel *seekAddressLabel = nullptr;
QAction *seekBackAction = nullptr;
QAction *seekForwardAction = nullptr;
bool historyBarAlwaysVisible = false;
bool historyBarEnabled = true;
void updateSeekHistoryBar();
};
#endif // ADDRESSABLE_DOCK_WIDGET_H
+64 -57
View File
@@ -5,6 +5,7 @@
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QMenu>
#include <QSet>
#include <QSortFilterProxyModel>
#include "IaitoDockWidget.h"
@@ -45,16 +46,42 @@ public:
void setAddressModel(AddressableItemModelI *model)
{
this->addressableModel = model;
setModel(model ? model->asItemModel() : nullptr);
}
BaseListWidget::setModel(this->addressableModel->asItemModel());
void setModel(QAbstractItemModel *model) override
{
BaseListWidget::setModel(model);
if (this->selectionModel()) {
this->connect(
this->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
&AddressableItemList<BaseListWidget>::onSelectedItemChanged);
}
}
this->connect(
this->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
&AddressableItemList<BaseListWidget>::onSelectedItemChanged);
auto cast_model = dynamic_cast<QAbstractItemModel *>(model);
IaitoTreeView::setModel(cast_model);
RVA itemAddress(const QModelIndex &index) const
{
auto provider = addressProvider();
auto source = mapToProviderIndex(provider, index);
return source.isValid() ? provider->address(source) : RVA_INVALID;
}
QList<RVA> selectedAddresses() const
{
QList<RVA> result;
QSet<RVA> seen;
if (auto selection = this->selectionModel()) {
for (const auto &index : selection->selectedIndexes()) {
const RVA address = itemAddress(index);
if (address != RVA_INVALID && !seen.contains(address)) {
seen.insert(address);
result.append(address);
}
}
}
return result;
}
void setMainWindow(MainWindow *mainWindow)
@@ -76,21 +103,17 @@ public:
protected:
virtual void showItemContextMenu(const QPoint &pt)
{
auto provider = addressProvider();
if (!provider) {
if (!itemContextMenu) {
return;
}
auto index = this->currentIndex();
if (index.isValid() && itemContextMenu) {
QModelIndex pIndex = mapToProviderIndex(provider, index);
if (!pIndex.isValid()) {
return;
}
auto offset = provider->address(pIndex);
auto name = provider->name(pIndex);
itemContextMenu->setTarget(offset, name);
itemContextMenu->exec(this->mapToGlobal(pt));
const auto index = this->indexAt(pt);
if (this->selectionModel()->isSelected(index)) {
this->selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
} else {
this->setCurrentIndex(index);
}
updateMenuFromItem(index);
itemContextMenu->exec(this->viewport()->mapToGlobal(pt));
}
virtual void onItemActivated(const QModelIndex &index)
@@ -107,18 +130,21 @@ protected:
return;
}
auto offset = provider->address(pIndex);
Core()->seekAndShow(offset);
if (offset != RVA_INVALID) {
Core()->seekAndShow(offset);
}
}
virtual void onSelectedItemChanged(const QModelIndex &index) { updateMenuFromItem(index); }
void updateMenuFromItem(const QModelIndex &index)
{
auto provider = addressProvider();
if (!provider) {
if (!provider || !itemContextMenu) {
return;
}
if (index.isValid()) {
QModelIndex pIndex = mapToProviderIndex(provider, index);
if (!pIndex.isValid()) {
if (!pIndex.isValid() || provider->address(pIndex) == RVA_INVALID) {
itemContextMenu->clearTarget();
return;
}
auto offset = provider->address(pIndex);
@@ -132,42 +158,21 @@ protected:
// Handle 'j' and 'k' keys to navigate and seek without pressing Enter
void keyPressEvent(QKeyEvent *event) override
{
if (event->modifiers() == Qt::NoModifier) {
if (event->modifiers() == Qt::NoModifier
&& (event->key() == Qt::Key_J || event->key() == Qt::Key_K)) {
auto selModel = this->selectionModel();
const QModelIndex curr = selModel->currentIndex();
if (curr.isValid()) {
if (event->key() == Qt::Key_J) {
// Move down
const QModelIndex parent = curr.parent();
const int total = this->model()->rowCount(parent);
const int newRow = qMin(curr.row() + 1, total - 1);
if (newRow != curr.row()) {
const QModelIndex newIndex
= this->model()->index(newRow, curr.column(), parent);
selModel->setCurrentIndex(
newIndex,
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
this->scrollTo(newIndex);
onItemActivated(newIndex);
this->setFocus();
}
return;
} else if (event->key() == Qt::Key_K) {
// Move up
if (curr.row() > 0) {
const QModelIndex parent = curr.parent();
const int newRow = curr.row() - 1;
const QModelIndex newIndex
= this->model()->index(newRow, curr.column(), parent);
selModel->setCurrentIndex(
newIndex,
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
this->scrollTo(newIndex);
onItemActivated(newIndex);
this->setFocus();
}
return;
auto tree = qobject_cast<QTreeView *>(this);
if (selModel && tree && this->currentIndex().isValid()) {
const auto next = event->key() == Qt::Key_J
? tree->indexBelow(this->currentIndex())
: tree->indexAbove(this->currentIndex());
if (next.isValid()) {
selModel->setCurrentIndex(
next, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
this->scrollTo(next);
onItemActivated(next);
}
return;
}
}
BaseListWidget::keyPressEvent(event);
@@ -228,4 +233,6 @@ private:
}
};
using AddressableItemView = AddressableItemList<>;
#endif // ADDRESSABLE_ITEM_LIST_H
+2 -2
View File
@@ -28,7 +28,7 @@
<number>0</number>
</property>
<item>
<widget class="AddressableItemList&lt;&gt;" name="breakpointTreeView">
<widget class="AddressableItemView" name="breakpointTreeView">
<property name="styleSheet">
<string notr="true">IaitoTreeView::item
{
@@ -80,7 +80,7 @@
</widget>
<customwidgets>
<customwidget>
<class>AddressableItemList&lt;&gt;</class>
<class>AddressableItemView</class>
<extends>QTreeView</extends>
<header>widgets/AddressableItemList.h</header>
<container>1</container>
+145 -52
View File
@@ -1,5 +1,8 @@
#include "ClassesWidget.h"
#include "NameListView.h"
#include "QuickFilterView.h"
#include "common/Helpers.h"
#include "common/NameListModel.h"
#include "common/SvgIconEngine.h"
#include "core/MainWindow.h"
#include "dialogs/EditMethodDialog.h"
@@ -9,6 +12,29 @@
#include <QList>
#include <QMenu>
#include <QMouseEvent>
#include <QShortcut>
RVA ClassesModel::address(const QModelIndex &index) const
{
const auto offset = index.data(OffsetRole);
return offset.isValid() ? offset.value<RVA>() : RVA_INVALID;
}
QString ClassesModel::name(const QModelIndex &index) const
{
const QString member = index.sibling(index.row(), NAME).data().toString();
if (!index.parent().isValid() || index.data(TypeRole).value<RowType>() == RowType::Base
|| index.data(TypeRole).value<RowType>() == RowType::VTable) {
return member;
}
const QString owner = index.parent().data(NameRole).toString();
const QString separator = owner.endsWith(';') ? QStringLiteral("->")
: owner.contains("::") ? QStringLiteral("::")
: owner.contains('/') ? QStringLiteral("/")
: owner.contains('.') ? QStringLiteral(".")
: QStringLiteral("::");
return member.startsWith(owner + separator) ? member : owner + separator + member;
}
QVariant ClassesModel::headerData(int section, Qt::Orientation, int role) const
{
@@ -44,6 +70,9 @@ void BinClassesModel::setClasses(const QList<BinClassDescription> &classes)
QModelIndex BinClassesModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent)) {
return {};
}
if (!parent.isValid()) {
return createIndex(row, column,
(quintptr) 0); // root function nodes have id = 0
@@ -70,6 +99,9 @@ QModelIndex BinClassesModel::parent(const QModelIndex &index) const
int BinClassesModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid() && parent.column() != 0) {
return 0;
}
if (!parent.isValid()) { // root
return classes.count();
}
@@ -89,6 +121,9 @@ int BinClassesModel::columnCount(const QModelIndex &) const
QVariant BinClassesModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
const BinClassDescription *cls;
const BinClassMethodDescription *meth = nullptr;
const BinClassFieldDescription *field = nullptr;
@@ -136,6 +171,15 @@ QVariant BinClassesModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue(meth->addr);
case NameRole:
return meth->name;
case MangledNameRole:
return meth->mangledName;
case NameListModel::NameToolTipRole:
case Qt::ToolTipRole:
if (!meth->mangledName.isEmpty() && meth->mangledName != meth->name) {
return tr("<qt><b>Name:</b> %1<br><b>Mangled:</b> %2</qt>")
.arg(name(index).toHtmlEscaped(), meth->mangledName.toHtmlEscaped());
}
return QStringLiteral("<qt>%1</qt>").arg(name(index).toHtmlEscaped());
case TypeRole:
return QVariant::fromValue(RowType::Method);
default:
@@ -354,6 +398,9 @@ const QVector<AnalClassesModel::Attribute> &AnalClassesModel::getAttrs(const QSt
QModelIndex AnalClassesModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent)) {
return {};
}
if (!parent.isValid()) {
return createIndex(row, column,
(quintptr) 0); // root function nodes have id = 0
@@ -380,6 +427,9 @@ QModelIndex AnalClassesModel::parent(const QModelIndex &index) const
int AnalClassesModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid() && parent.column() != 0) {
return 0;
}
if (!parent.isValid()) { // root
return classes.count();
}
@@ -393,7 +443,8 @@ int AnalClassesModel::rowCount(const QModelIndex &parent) const
bool AnalClassesModel::hasChildren(const QModelIndex &parent) const
{
return !parent.isValid() || !parent.parent().isValid();
return !parent.isValid() ? !classes.isEmpty()
: parent.column() == 0 && parent.internalId() == 0;
}
int AnalClassesModel::columnCount(const QModelIndex &) const
@@ -403,6 +454,9 @@ int AnalClassesModel::columnCount(const QModelIndex &) const
QVariant AnalClassesModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.internalId() == 0) { // class row
if (index.row() >= classes.count()) {
return QVariant();
@@ -533,13 +587,29 @@ QVariant AnalClassesModel::data(const QModelIndex &index, int role) const
}
ClassesSortFilterProxyModel::ClassesSortFilterProxyModel(QObject *parent)
: QSortFilterProxyModel(parent)
{}
: AddressableFilterProxyModel(nullptr, parent)
{
setFilterCaseSensitivity(Qt::CaseInsensitive);
}
bool ClassesSortFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
QModelIndex index = sourceModel()->index(row, 0, parent);
return index.data(ClassesModel::NameRole).toString().contains(FILTER_REGEX);
const auto matches = [this](const QModelIndex &item) {
return item.data(ClassesModel::NameRole).toString().contains(FILTER_REGEX)
|| item.data(ClassesModel::MangledNameRole).toString().contains(FILTER_REGEX);
};
if (filterAcceptsType(row, parent)
&& (matches(index) || (parent.isValid() && matches(parent)))) {
return true;
}
// Keep the owning class when a member matches, but hide unrelated members.
for (int child = 0; child < sourceModel()->rowCount(index); ++child) {
if (filterAcceptsRow(child, index)) {
return true;
}
}
return false;
}
bool ClassesSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
@@ -577,11 +647,6 @@ bool ClassesSortFilterProxyModel::lessThan(const QModelIndex &left, const QModel
}
}
bool ClassesSortFilterProxyModel::hasChildren(const QModelIndex &parent) const
{
return !parent.isValid() || !parent.parent().isValid();
}
ClassesWidget::ClassesWidget(MainWindow *main)
: IaitoDockWidget(main)
, ui(new Ui::ClassesWidget)
@@ -592,9 +657,32 @@ ClassesWidget::ClassesWidget(MainWindow *main)
proxy_model = new ClassesSortFilterProxyModel(this);
ui->classesTreeView->setModel(proxy_model);
nameMenu = new QMenu(this);
names = new NameListView(
ui->classesTreeView, nameMenu, proxy_model, ClassesModel::NAME, objectName());
auto filter = new QuickFilterView(this);
filter->setTypeFilter(proxy_model, ClassesModel::TYPE);
ui->verticalLayout->insertWidget(1, filter);
filter->addActionButton(names->toggleAction());
connect(
filter,
&QuickFilterView::filterTextChanged,
proxy_model,
&AddressableFilterProxyModel::setFilterWildcard);
connect(
filter,
&QuickFilterView::filterClosed,
ui->classesTreeView,
static_cast<void (QWidget::*)()>(&QWidget::setFocus));
auto find = new QShortcut(QKeySequence::Find, this);
find->setContext(Qt::WidgetWithChildrenShortcut);
connect(find, &QShortcut::activated, filter, &QuickFilterView::showFilter);
auto clear = new QShortcut(QKeySequence(Qt::Key_Escape), this);
clear->setContext(Qt::WidgetWithChildrenShortcut);
connect(clear, &QShortcut::activated, filter, &QuickFilterView::clearFilter);
ui->classesTreeView->sortByColumn(ClassesModel::TYPE, Qt::AscendingOrder);
ui->classSourceCombo->setCurrentIndex(1);
ui->classSourceCombo->setCurrentIndex(0);
connect<void (QComboBox::*)(int)>(
ui->classSourceCombo, &QComboBox::currentIndexChanged, this, &ClassesWidget::refreshClasses);
@@ -603,11 +691,21 @@ ClassesWidget::ClassesWidget(MainWindow *main)
&QTreeView::customContextMenuRequested,
this,
&ClassesWidget::showContextMenu);
connect(Core(), &IaitoCore::refreshAll, this, &ClassesWidget::refreshClasses);
connect(Core(), &IaitoCore::codeRebased, this, &ClassesWidget::refreshClasses);
refreshClasses();
}
ClassesWidget::~ClassesWidget() {}
ClassesWidget::~ClassesWidget()
{
ui->classesTreeView->setModel(nullptr);
}
QModelIndex ClassesWidget::currentSourceIndex() const
{
return names->presentationModel()->mapToSource(ui->classesTreeView->currentIndex());
}
ClassesWidget::Source ClassesWidget::getSource()
{
@@ -625,7 +723,7 @@ void ClassesWidget::refreshClasses()
case Source::BIN:
if (!bin_model) {
bin_model = new BinClassesModel(this);
proxy_model->setSourceModel(bin_model);
proxy_model->setClassesModel(bin_model);
delete anal_model;
anal_model = nullptr;
}
@@ -634,7 +732,7 @@ void ClassesWidget::refreshClasses()
case Source::ANAL:
if (!anal_model) {
anal_model = new AnalClassesModel(this);
proxy_model->setSourceModel(anal_model);
proxy_model->setClassesModel(anal_model);
delete bin_model;
bin_model = nullptr;
}
@@ -651,59 +749,54 @@ void ClassesWidget::on_classesTreeView_doubleClicked(const QModelIndex &index)
if (!index.isValid())
return;
const auto type = index.data(ClassesModel::TypeRole);
if (!type.isValid() || type.value<ClassesModel::RowType>() == ClassesModel::RowType::Class) {
return;
}
QVariant offsetData = index.data(ClassesModel::OffsetRole);
if (!offsetData.isValid()) {
return;
}
RVA offset = offsetData.value<RVA>();
Core()->seekAndShow(offset);
if (offset != RVA_INVALID) {
Core()->seekAndShow(offset);
}
}
void ClassesWidget::showContextMenu(const QPoint &pt)
{
if (!anal_model) {
// no context menu for bin classes
return;
}
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
if (!index.isValid()) {
return;
}
auto type = static_cast<ClassesModel::RowType>(index.data(ClassesModel::TypeRole).toInt());
ui->classesTreeView->setCurrentIndex(ui->classesTreeView->indexAt(pt));
const QModelIndex index = currentSourceIndex();
const auto type = index.data(ClassesModel::TypeRole).value<ClassesModel::RowType>();
QMenu menu(ui->classesTreeView);
menu.addAction(ui->newClassAction);
if (type == ClassesModel::RowType::Class) {
menu.addAction(ui->renameClassAction);
menu.addAction(ui->deleteClassAction);
}
menu.addSeparator();
menu.addAction(ui->addMethodAction);
if (type == ClassesModel::RowType::Method) {
menu.addAction(ui->editMethodAction);
QString className = index.parent().data(ClassesModel::NameRole).toString();
QString methodName = index.data(ClassesModel::NameRole).toString();
AnalMethodDescription desc;
if (Core()->getAnalMethod(className, methodName, &desc)) {
if (desc.vtableOffset >= 0) {
menu.addAction(ui->seekToVTableAction);
if (anal_model) {
menu.addAction(ui->newClassAction);
if (index.isValid()) {
if (type == ClassesModel::RowType::Class) {
menu.addAction(ui->renameClassAction);
menu.addAction(ui->deleteClassAction);
}
menu.addAction(ui->addMethodAction);
if (type == ClassesModel::RowType::Method) {
menu.addAction(ui->editMethodAction);
const QString className = index.parent().data(ClassesModel::NameRole).toString();
const QString methodName = index.data(ClassesModel::NameRole).toString();
AnalMethodDescription desc;
if (Core()->getAnalMethod(className, methodName, &desc) && desc.vtableOffset >= 0) {
menu.addAction(ui->seekToVTableAction);
}
}
}
}
menu.exec(ui->classesTreeView->mapToGlobal(pt));
menu.addActions(nameMenu->actions());
menu.exec(ui->classesTreeView->viewport()->mapToGlobal(pt));
}
void ClassesWidget::on_seekToVTableAction_triggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = currentSourceIndex();
QString className = index.parent().data(ClassesModel::NameRole).toString();
QList<AnalVTableDescription> vtables = Core()->getAnalClassVTables(className);
@@ -726,7 +819,7 @@ void ClassesWidget::on_seekToVTableAction_triggered()
void ClassesWidget::on_addMethodAction_triggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = currentSourceIndex();
if (!index.isValid()) {
return;
}
@@ -744,7 +837,7 @@ void ClassesWidget::on_addMethodAction_triggered()
void ClassesWidget::on_editMethodAction_triggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = currentSourceIndex();
if (!index.isValid()
|| index.data(ClassesModel::TypeRole).toInt()
!= static_cast<int>(ClassesModel::RowType::Method)) {
@@ -767,7 +860,7 @@ void ClassesWidget::on_newClassAction_triggered()
void ClassesWidget::on_deleteClassAction_triggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = currentSourceIndex();
if (!index.isValid()
|| index.data(ClassesModel::TypeRole).toInt()
!= static_cast<int>(ClassesModel::RowType::Class)) {
@@ -786,7 +879,7 @@ void ClassesWidget::on_deleteClassAction_triggered()
void ClassesWidget::on_renameClassAction_triggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = currentSourceIndex();
if (!index.isValid()
|| index.data(ClassesModel::TypeRole).toInt()
!= static_cast<int>(ClassesModel::RowType::Class)) {
+16 -4
View File
@@ -4,6 +4,7 @@
#include <memory>
#include "IaitoDockWidget.h"
#include "common/AddressableItemModel.h"
#include "core/Iaito.h"
#include <QAbstractListModel>
@@ -13,6 +14,8 @@ namespace Ui {
class ClassesWidget;
}
class NameListView;
class QMenu;
class QTreeWidget;
class QTreeWidgetItem;
class MainWindow;
@@ -21,7 +24,7 @@ class ClassesWidget;
/**
* @brief Common abstract base class for Bin and Anal classes models
*/
class ClassesModel : public QAbstractItemModel
class ClassesModel : public AddressableItemModel<>
{
public:
enum Columns { NAME = 0, TYPE, OFFSET, VTABLE, COUNT };
@@ -60,13 +63,16 @@ public:
* by vtable offset
*/
static const int VTableRole = Qt::UserRole + 3;
static const int MangledNameRole = Qt::UserRole + 4;
explicit ClassesModel(QObject *parent = nullptr)
: QAbstractItemModel(parent)
: AddressableItemModel<>(parent)
{}
QVariant headerData(
int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
RVA address(const QModelIndex &index) const override;
QString name(const QModelIndex &index) const override;
};
Q_DECLARE_METATYPE(ClassesModel::RowType)
@@ -158,17 +164,20 @@ public slots:
void classAttrsChanged(const QString &cls);
};
class ClassesSortFilterProxyModel : public QSortFilterProxyModel
class ClassesSortFilterProxyModel : public AddressableFilterProxyModel
{
Q_OBJECT
public:
explicit ClassesSortFilterProxyModel(QObject *parent = nullptr);
void setClassesModel(ClassesModel *model)
{
AddressableFilterProxyModel::setSourceModel(static_cast<AddressableItemModelI *>(model));
}
protected:
bool filterAcceptsRow(int row, const QModelIndex &parent) const override;
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
bool hasChildren(const QModelIndex &parent = QModelIndex()) const override;
};
class ClassesWidget : public IaitoDockWidget
@@ -203,6 +212,9 @@ private:
BinClassesModel *bin_model = nullptr;
AnalClassesModel *anal_model = nullptr;
ClassesSortFilterProxyModel *proxy_model;
NameListView *names;
QMenu *nameMenu;
QModelIndex currentSourceIndex() const;
};
#endif // CLASSESWIDGET_H
+75
View File
@@ -244,6 +244,24 @@ void CodeInfoWidget::buildUi()
functionTree->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
functionTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
layout->addWidget(functionTree);
exceptionStatus = new QLabel(functionSection);
exceptionStatus->setWordWrap(true);
layout->addWidget(exceptionStatus);
exceptionTree = new QTreeWidget(functionSection);
exceptionTree->setHeaderLabels({tr("Exception"), tr("Address / value")});
exceptionTree->setRootIsDecorated(true);
exceptionTree->setAlternatingRowColors(true);
exceptionTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
exceptionTree->setToolTip(tr("Activate an address to navigate. Range ends are exclusive."));
exceptionTree->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
connect(exceptionTree, &QTreeWidget::itemActivated, this, [](QTreeWidgetItem *item, int) {
const auto address = item->data(0, Qt::UserRole);
if (address.isValid()) {
Core()->seek(address.toULongLong());
}
});
layout->addWidget(exceptionTree);
}
rootLayout->addWidget(functionSection);
@@ -400,6 +418,7 @@ void CodeInfoWidget::refresh()
const RVA off = Core()->getOffset();
const QString addrStr = RAddressString(off);
refreshExceptions(off);
// Opcode (aoj)
QJsonDocument opDoc = Core()->cmdj("aoj");
@@ -492,6 +511,62 @@ void CodeInfoWidget::refresh()
}
}
void CodeInfoWidget::refreshExceptions(RVA address)
{
exceptionTree->clear();
const RVA function = Core()->getFunctionStart(address);
auto regions = Core()->getExceptionRegions(function);
if (function == RVA_INVALID) {
for (auto it = regions.begin(); it != regions.end();) {
if (!it->touches(address, 1)) {
it = regions.erase(it);
} else {
++it;
}
}
}
exceptionStatus->setText(
regions.isEmpty()
? tr("No exception metadata reported for this function.")
: tr("Exception handlers (%1) · activate an address to navigate").arg(regions.size()));
exceptionTree->setVisible(!regions.isEmpty());
exceptionStatus->setVisible(!regions.isEmpty());
for (const auto &region : regions) {
auto *entry = new QTreeWidgetItem(
exceptionTree,
{QStringLiteral("#%1 · %2").arg(region.index).arg(region.label()),
region.contains(address) ? tr("Protects current instruction") : QString()});
entry->setToolTip(
0,
region.description().toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br>")));
QPixmap swatch(12, 12);
swatch.fill(region.color());
entry->setIcon(0, QIcon(swatch));
const auto addAddress = [entry](const QString &name, quint64 value) {
auto *item = new QTreeWidgetItem(entry, {name, RAddressString(value)});
item->setData(0, Qt::UserRole, QVariant::fromValue(value));
};
addAddress(tr("Try start"), region.from);
addAddress(tr("Try end (exclusive)"), region.to);
addAddress(tr("Handler"), region.handler);
if (region.filter && region.filter != ExceptionRegion::Invalid) {
if (region.filter > 1) {
addAddress(tr("Filter"), region.filter);
} else {
new QTreeWidgetItem(entry, {tr("Filter constant"), QString::number(region.filter)});
}
}
if (region.typeFilter) {
new QTreeWidgetItem(entry, {tr("Type filter"), QString::number(region.typeFilter)});
}
if (region.source != ExceptionRegion::Invalid) {
addAddress(tr("Metadata source"), region.source);
}
entry->setExpanded(true);
}
fitTreeToContent(exceptionTree);
}
void CodeInfoWidget::onPatchInstruction()
{
if (mainWindow) {
+3
View File
@@ -57,6 +57,7 @@ private:
void buildUi();
void populateTree(QTreeWidget *tree, const QJsonValue &value);
void addJsonNode(QTreeWidgetItem *parent, const QString &key, const QJsonValue &value);
void refreshExceptions(RVA address);
CodeInfoSection *opcodeSection;
CodeInfoSection *blockSection;
@@ -73,6 +74,8 @@ private:
QTreeWidget *opcodeTree;
QTreeWidget *blockTree;
QTreeWidget *functionTree;
QTreeWidget *exceptionTree;
QLabel *exceptionStatus;
QPushButton *btnPatchInstr;
QPushButton *btnPatchBytes;
+2 -2
View File
@@ -399,7 +399,7 @@ void ColorShowWidget::setColor(const QColor &c)
void ColorShowWidget::paintEvent(QPaintEvent *event)
{
QPainter p(this);
const int miniRectWidth = rect().width() / 2;
const int miniRectWidth = qMax(2, rect().width() / 2);
for (int y = rect().topLeft().ry(); y < rect().bottomRight().ry(); y++) {
for (int x = rect().topLeft().rx(); x < rect().bottomRight().rx(); x++) {
p.setPen(
@@ -438,7 +438,7 @@ void AlphaChannelBar::paintEvent(QPaintEvent *event)
barRect.setWidth(barRect.width() - triangleSize);
const int miniRectWidth = barRect.width() / 2;
const int miniRectWidth = qMax(2, barRect.width() / 2);
for (int y = barRect.topLeft().ry(); y < barRect.bottomRight().ry(); y++) {
for (int x = barRect.topLeft().rx(); x < barRect.bottomRight().rx(); x++) {
p.setPen(
+17 -25
View File
@@ -1,46 +1,38 @@
#include "ComboQuickFilterView.h"
#include "ui_ComboQuickFilterView.h"
#include <QHBoxLayout>
#include <QLabel>
ComboQuickFilterView::ComboQuickFilterView(QWidget *parent)
: QWidget(parent)
, ui(new Ui::ComboQuickFilterView)
: QuickFilterView(parent)
, label(new QLabel(this))
, combo(new QComboBox(this))
{
ui->setupUi(this);
connect(ui->lineEdit, &QLineEdit::textChanged, this, [this](const QString &text) {
emit filterTextChanged(text);
});
}
ComboQuickFilterView::~ComboQuickFilterView()
{
delete ui;
label->setObjectName(QStringLiteral("label"));
combo->setObjectName(QStringLiteral("comboBox"));
auto row = qobject_cast<QHBoxLayout *>(layout());
row->addWidget(label);
row->addWidget(combo);
}
void ComboQuickFilterView::setLabelText(const QString &text)
{
ui->label->setText(text);
label->setText(text);
}
QComboBox *ComboQuickFilterView::comboBox()
{
return ui->comboBox;
}
void ComboQuickFilterView::showFilter()
{
show();
ui->lineEdit->setFocus();
return combo;
}
void ComboQuickFilterView::clearFilter()
{
ui->lineEdit->setText("");
QuickFilterView::clearFilter();
combo->setCurrentIndex(0);
}
void ComboQuickFilterView::closeFilter()
{
ui->lineEdit->setText("");
hide();
emit filterClosed();
combo->setCurrentIndex(0);
QuickFilterView::closeFilter();
}
+6 -15
View File
@@ -1,37 +1,28 @@
#ifndef COMBOQUICKFILTERVIEW_H
#define COMBOQUICKFILTERVIEW_H
#include "core/IaitoCommon.h"
#include "QuickFilterView.h"
#include <QComboBox>
#include <QWidget>
class QLabel;
namespace Ui {
class ComboQuickFilterView;
}
class IAITO_EXPORT ComboQuickFilterView : public QWidget
class IAITO_EXPORT ComboQuickFilterView : public QuickFilterView
{
Q_OBJECT
public:
explicit ComboQuickFilterView(QWidget *parent = nullptr);
~ComboQuickFilterView();
void setLabelText(const QString &text);
QComboBox *comboBox();
public slots:
void showFilter();
void closeFilter();
void clearFilter();
signals:
void filterTextChanged(const QString &text);
void filterClosed();
void closeFilter();
private:
Ui::ComboQuickFilterView *ui;
QLabel *label;
QComboBox *combo;
};
#endif // COMBOQUICKFILTERVIEW_H
-50
View File
@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ComboQuickFilterView</class>
<widget class="QWidget" name="ComboQuickFilterView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>378</width>
<height>20</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="lineEdit">
<property name="placeholderText">
<string>Quick Filter</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+3 -1
View File
@@ -44,6 +44,7 @@ static const char *refreshAllCommandPrefixes[] = {
"C",
"aa",
"af",
"avg",
"w",
};
@@ -150,7 +151,8 @@ ConsoleWidget::ConsoleWidget(MainWindow *main)
historyDownShortcut->setContext(Qt::WidgetShortcut);
QShortcut *completionShortcut
= ShortcutMgr()->registerShortcut("console.complete", ui->r2InputLineEdit);
= ShortcutMgr()
->registerShortcut("console.complete", ui->r2InputLineEdit, Qt::WidgetShortcut);
connect(completionShortcut, &QShortcut::activated, this, &ConsoleWidget::triggerCompletion);
connect(ui->r2InputLineEdit, &QLineEdit::editingFinished, this, &ConsoleWidget::disableCompletion);
+679
View File
@@ -0,0 +1,679 @@
#include "DataAnalysisView.h"
#include <QApplication>
#include <QContextMenuEvent>
#include <QGestureEvent>
#include <QKeyEvent>
#include <QLinearGradient>
#include <QMouseEvent>
#include <QNativeGestureEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPinchGesture>
#include <QToolTip>
#include <QWheelEvent>
#include <algorithm>
#include <cmath>
using namespace DataAnalysis;
DataAnalysisView::DataAnalysisView(bool overview, QWidget *parent)
: QWidget(parent)
, overview(overview)
{
setMouseTracking(true);
setFocusPolicy(Qt::StrongFocus);
#ifndef Q_OS_MACOS
// On macOS, handle native trackpad events directly to retain their position.
grabGesture(Qt::PinchGesture);
#endif
setMinimumSize(220, overview ? 70 : 180);
setAccessibleName(overview ? tr("Data overview") : tr("Data analysis graph"));
setAccessibleDescription(
overview
? tr("Drag the window or use the wheel to pan; drag its edges to resize; pinch to zoom."
" Double-click to fit.")
: tr("Click to seek; drag to select bytes; wheel to pan; pinch to zoom."
" Right-click for comments, flags and range actions."));
tooltipTimer.setSingleShot(true);
tooltipTimer.setInterval(650);
connect(&tooltipTimer, &QTimer::timeout, this, &DataAnalysisView::showTooltip);
}
QString DataAnalysisView::metricName(int metric)
{
static const char *names[]
= {QT_TR_NOOP("Entropy"),
QT_TR_NOOP("Printable ASCII"),
QT_TR_NOOP("0x00 bytes"),
QT_TR_NOOP("0xFF bytes"),
QT_TR_NOOP("Unique bytes"),
QT_TR_NOOP("Mean byte"),
QT_TR_NOOP("ASCII letters"),
QT_TR_NOOP("ASCII digits"),
QT_TR_NOOP("ASCII whitespace"),
QT_TR_NOOP("ASCII control"),
QT_TR_NOOP("High bytes")};
return metric >= 0 && metric < Count ? tr(names[metric]) : QString();
}
QString DataAnalysisView::metricValue(int metric, double value)
{
if (metric == Entropy) {
return tr("%1 bits/byte").arg(value * 8, 0, 'f', 3);
}
if (metric == Unique) {
return tr("%1 / 256").arg(qRound(value * 256));
}
if (metric == Mean) {
return QString::number(value * 255, 'f', 2);
}
return tr("%1%").arg(value * 100, 0, 'f', 2);
}
QString DataAnalysisView::metricDescription(int metric)
{
static const char *descriptions[]
= {QT_TR_NOOP(
"Shannon entropy: 0–8 bits per byte. Higher values indicate a more varied byte "
"distribution. Overlaid values are divided by 8."),
QT_TR_NOOP("Percentage of printable ASCII bytes (0x20–0x7e)."),
QT_TR_NOOP("Percentage of zero bytes (0x00), often padding or uninitialized data."),
QT_TR_NOOP("Percentage of 0xff bytes, often padding or erased storage."),
QT_TR_NOOP("Number of distinct byte values observed, out of 256 possible values."),
QT_TR_NOOP("Average byte value (0–255). Overlaid values are divided by 255."),
QT_TR_NOOP("Percentage of ASCII letters (A–Z and a–z)."),
QT_TR_NOOP("Percentage of ASCII decimal digits (0–9)."),
QT_TR_NOOP(
"Percentage of ASCII whitespace (space, tab, newline, vertical tab, form feed and "
"carriage return)."),
QT_TR_NOOP("Percentage of ASCII control bytes (0x00–0x1f and 0x7f)."),
QT_TR_NOOP("Percentage of bytes with the high bit set (0x80–0xff).")};
return metric >= 0 && metric < Count ? tr(descriptions[metric]) : QString();
}
QString DataAnalysisView::markerName(MarkerType type)
{
static const char *names[]
= {QT_TR_NOOP("Section"),
QT_TR_NOOP("Segment"),
QT_TR_NOOP("Map"),
QT_TR_NOOP("Flag"),
QT_TR_NOOP("Comment")};
return tr(names[type]);
}
Range DataAnalysisView::axisRange() const
{
return overview ? domain : visibleRange;
}
QRectF DataAnalysisView::plotRect() const
{
return QRectF(
46, overview ? 5 : 30, qMax(1, width() - 60), qMax(1, height() - (overview ? 26 : 59)));
}
qreal DataAnalysisView::xForAddress(RVA address) const
{
const Range range = axisRange();
if (!range.valid()) {
return plotRect().left();
}
const long double distance = address >= range.start ? (long double) (address - range.start)
: -(long double) (range.start - address);
return plotRect().left() + distance / range.size() * plotRect().width();
}
RVA DataAnalysisView::addressAt(qreal x) const
{
const Range range = axisRange();
if (!range.valid()) {
return RVA_INVALID;
}
const long double fraction = (x - plotRect().left()) / plotRect().width();
if (fraction <= 0) {
return range.start;
}
if (fraction >= 1) {
return range.end - 1;
}
return range.start + qMin<RVA>(RVA(fraction * range.size()), range.size() - 1);
}
void DataAnalysisView::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.fillRect(rect(), palette().color(QPalette::Base));
const QRectF plot = plotRect();
const Range range = axisRange();
const QColor text = palette().color(QPalette::Text);
QColor grid = text;
grid.setAlpha(35);
p.setPen(grid);
p.drawRect(plot);
if (!range.valid()) {
p.setPen(text);
p.drawText(plot, Qt::AlignCenter, tr("Open a file to analyze its data"));
return;
}
const bool split = !overview && separateTracks;
const int tracks = split ? qMax(1, int(metrics.size())) : 1;
for (int track = 0; track < tracks; ++track) {
const qreal top = plot.top() + track * plot.height() / tracks;
const qreal bottom = plot.top() + (track + 1) * plot.height() / tracks;
const qreal graphTop = top + (split ? 19 : 0);
const qreal graphBottom = bottom - (split ? 5 : 0);
const qreal graphHeight = qMax(1.0, graphBottom - graphTop);
if (!overview) {
for (int tick = 0; tick <= 4; ++tick) {
const qreal y = graphBottom - graphHeight * tick / 4;
p.setPen(grid);
p.drawLine(QPointF(plot.left(), y), QPointF(plot.right(), y));
if (tracks == 1 || tick == 0 || tick == 4) {
int maximum = 100;
if ((separateTracks || metrics.size() == 1) && !metrics.isEmpty()) {
const int metric = metrics[track];
maximum = metric == Entropy ? 8
: metric == Mean ? 255
: metric == Unique ? 256
: 100;
}
p.setPen(text);
QString label = QString::number(maximum * tick / 4.0, 'g', 3);
if (maximum == 100 && tick == 4) {
label += '%';
}
p.drawText(
QRectF(0, y - 8, plot.left() - 7, 16),
Qt::AlignRight | Qt::AlignVCenter,
label);
}
}
if (split && !metrics.isEmpty()) {
p.setPen(colors[metrics[track]]);
p.drawText(
QRectF(plot.left() + 5, top, plot.width() - 10, 18),
Qt::AlignLeft | Qt::AlignVCenter,
metricName(metrics[track]));
}
}
p.save();
p.setClipRect(QRectF(plot.left(), graphTop, plot.width(), graphHeight));
const QVector<int> drawn = overview ? QVector<int>{Entropy}
: split && !metrics.isEmpty() ? QVector<int>{metrics[track]}
: metrics;
for (int series = 0; series < drawn.size(); ++series) {
const int metric = drawn[series];
const QColor color = colors[metric];
QPainterPath path;
qreal firstX = 0, lastX = 0;
auto flush = [&]() {
if (path.isEmpty()) {
return;
}
if (overview || style == Areas) {
QPainterPath area = path;
area.lineTo(lastX, graphBottom);
area.lineTo(firstX, graphBottom);
area.closeSubpath();
QLinearGradient gradient(0, graphTop, 0, graphBottom);
QColor fill = color;
fill.setAlpha(overview ? 110 : 85);
gradient.setColorAt(0, fill);
fill.setAlpha(12);
gradient.setColorAt(1, fill);
p.fillPath(area, gradient);
}
p.setPen(QPen(color, overview ? 1 : 1.5));
p.drawPath(path);
path = QPainterPath();
};
for (const auto &sample : samples) {
if (!sample.bytes) {
flush();
continue;
}
const qreal left = xForAddress(sample.range.start);
const qreal right = xForAddress(sample.range.end);
const qreal y = graphBottom - qBound(0.0, sample.values[metric], 1.0) * graphHeight;
if (!overview && style == Bars) {
const qreal barWidth = (right - left) / qMax(1, int(drawn.size()));
QColor fill = color;
fill.setAlpha(190);
p.fillRect(
QRectF(left + series * barWidth, y, qMax(0.6, barWidth), graphBottom - y),
fill);
} else {
if (path.isEmpty()) {
firstX = left;
path.moveTo(left, y);
}
path.lineTo((left + right) / 2, y);
lastX = right;
// Retain single-bucket signals and reach both ends of the range.
path.lineTo(right, y);
}
}
flush();
}
p.restore();
}
// Markers are coalesced by pixel and category to keep dense flag sets cheap to paint.
std::array<int, MarkerCount> lastPixel;
lastPixel.fill(-1000);
qreal labelEnd = plot.left();
for (const auto &marker : markers) {
if (!markerEnabled[marker.type]) {
continue;
}
const QColor color = colors[(marker.type + 4) % Count];
for (RVA address : {marker.range.start, marker.range.end}) {
if (address == marker.range.end && marker.type >= Flag) {
continue;
}
if (address < range.start || address > range.end) {
continue;
}
const qreal x = xForAddress(address);
const int pixel = qRound(x);
if (std::abs(pixel - lastPixel[marker.type]) < 2) {
continue;
}
lastPixel[marker.type] = pixel;
QColor line = color;
line.setAlpha(95);
p.setPen(QPen(line, 1, marker.type >= Flag ? Qt::DotLine : Qt::DashLine));
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
p.setPen(color);
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.top() + 5));
}
if (!overview && marker.type <= Map && intersect(marker.range, range).valid()) {
const qreal left = qMax(plot.left(), xForAddress(marker.range.start));
const qreal right = qMin(plot.right(), xForAddress(marker.range.end));
if (left >= labelEnd && right - left > 38) {
p.setPen(color);
const QString label
= fontMetrics().elidedText(marker.name, Qt::ElideRight, int(right - left - 6));
p.drawText(QRectF(left + 3, 4, right - left - 6, 20), Qt::AlignLeft, label);
labelEnd = left + fontMetrics().horizontalAdvance(label) + 10;
}
}
}
auto highlight = [&](Range selected, bool window) {
selected = intersect(selected, range);
if (!selected.valid()) {
return;
}
const qreal left = xForAddress(selected.start), right = xForAddress(selected.end);
QColor fill = palette().color(QPalette::Highlight);
fill.setAlpha(window ? 35 : 55);
p.fillRect(QRectF(left, plot.top(), qMax(1.0, right - left), plot.height()), fill);
p.setPen(QPen(palette().color(QPalette::Highlight), window ? 2 : 1));
p.drawRect(QRectF(left, plot.top(), qMax(1.0, right - left), plot.height()));
if (window) {
p.fillRect(QRectF(left - 2, plot.center().y() - 7, 4, 14), text);
p.fillRect(QRectF(right - 2, plot.center().y() - 7, 4, 14), text);
}
};
if (overview) {
const QColor shade(0, 0, 0, 65);
const qreal left = xForAddress(visibleRange.start), right = xForAddress(visibleRange.end);
p.fillRect(
QRectF(plot.left(), plot.top(), qMax(0.0, left - plot.left()), plot.height()), shade);
p.fillRect(QRectF(right, plot.top(), qMax(0.0, plot.right() - right), plot.height()), shade);
highlight(visibleRange, true);
}
highlight(drag == Select && dragSelection.valid() ? dragSelection : selection, false);
if (range.contains(seekAddress)) {
const qreal x = xForAddress(seekAddress);
p.setPen(QPen(palette().color(QPalette::Highlight), 2));
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
p.setBrush(palette().color(QPalette::Highlight));
p.drawPolygon(QPolygonF(
QVector<QPointF>{
QPointF(x - 4, plot.top()), QPointF(x + 4, plot.top()), QPointF(x, plot.top() + 6)}));
}
if (!overview && hoverX >= plot.left() && hoverX <= plot.right()) {
p.setPen(QPen(text, 1, Qt::DotLine));
p.drawLine(QPointF(hoverX, plot.top()), QPointF(hoverX, plot.bottom()));
}
p.setPen(text);
const int addressWidth = fontMetrics().horizontalAdvance(RAddressString(range.end - 1));
const int labels = qBound(1, int(plot.width() / (addressWidth + 24)), 4);
for (int i = 0; i <= labels; ++i) {
const qreal x = plot.left() + plot.width() * i / labels;
const QString label = fontMetrics().elidedText(
RAddressString(addressAt(x)), Qt::ElideMiddle, int(plot.width() / (labels + 1)));
const qreal width = fontMetrics().horizontalAdvance(label) + 2;
const qreal left
= qBound(plot.left(), x - width / 2, qMax(plot.left(), plot.right() - width));
p.drawText(QRectF(left, plot.bottom() + 4, width, 20), Qt::AlignLeft, label);
}
const bool hasData = std::any_of(samples.cbegin(), samples.cend(), [](const Sample &sample) {
return sample.bytes > 0;
});
if (!overview && !hasData) {
p.drawText(plot, Qt::AlignCenter, loading ? tr("Analyzing…") : tr("No readable data"));
}
}
bool DataAnalysisView::event(QEvent *event)
{
if (event->type() == QEvent::Gesture) {
auto *gestures = static_cast<QGestureEvent *>(event);
if (auto *pinch = static_cast<QPinchGesture *>(gestures->gesture(Qt::PinchGesture))) {
if (pinch->state() != Qt::GestureFinished && pinch->state() != Qt::GestureCanceled
&& (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged)) {
const QPoint position = mapFromGlobal(pinch->centerPoint().toPoint());
pinchZoom(pinch->scaleFactor(), position.x());
}
gestures->accept(pinch);
return true;
}
} else if (event->type() == QEvent::NativeGesture) {
auto *gesture = static_cast<QNativeGestureEvent *>(event);
if (gesture->gestureType() == Qt::ZoomNativeGesture) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const qreal x = gesture->position().x();
#else
const qreal x = gesture->localPos().x();
#endif
pinchZoom(1.0 + gesture->value(), x);
event->accept();
return true;
}
}
return QWidget::event(event);
}
void DataAnalysisView::pinchZoom(qreal scaleFactor, qreal x)
{
cancelTooltip();
drag = None;
dragSelection = {};
if (std::isfinite(scaleFactor) && scaleFactor > 0 && scaleFactor != 1) {
// Magnifying the graph narrows the address range around the gesture.
zoom(1.0 / scaleFactor, addressAt(x));
}
update();
}
void DataAnalysisView::zoom(double factor, RVA anchor)
{
if (!domain.valid() || !visibleRange.valid() || !std::isfinite(factor) || factor <= 0) {
return;
}
long double scaled = (long double) visibleRange.size() * factor;
if (factor > 1) {
// Small pinch steps must still let the user zoom out from a one-byte range.
scaled = std::ceil(scaled);
}
const RVA size = scaled >= domain.size() ? domain.size() : qMax<RVA>(1, RVA(scaled));
anchor = qBound(visibleRange.start, anchor, visibleRange.end - 1);
const long double ratio = (long double) (anchor - visibleRange.start) / visibleRange.size();
const RVA before = qMin<RVA>(size - 1, RVA(ratio * size));
const RVA start = anchor - qMin(anchor - domain.start, before);
const RVA clamped = qMin(start, domain.end - size);
emit rangeRequested(clamped, clamped + size);
}
void DataAnalysisView::pan(int direction)
{
if (!visibleRange.valid() || !domain.valid()) {
return;
}
const RVA step = qMax<RVA>(1, visibleRange.size() / 4);
const RVA start = direction < 0
? visibleRange.start - qMin(step, visibleRange.start - domain.start)
: visibleRange.start + qMin(step, domain.end - visibleRange.end);
emit rangeRequested(start, start + visibleRange.size());
}
void DataAnalysisView::mousePressEvent(QMouseEvent *event)
{
cancelTooltip();
if (event->button() != Qt::LeftButton || !axisRange().valid()
|| !plotRect().contains(event->pos())) {
return;
}
setFocus();
pressPosition = event->pos();
pressAddress = addressAt(event->pos().x());
pressRange = visibleRange;
dragSelection = {};
if (overview) {
if (std::abs(event->pos().x() - xForAddress(visibleRange.start)) < 7) {
drag = LeftEdge;
} else if (std::abs(event->pos().x() - xForAddress(visibleRange.end)) < 7) {
drag = RightEdge;
} else {
drag = Move;
if (!visibleRange.contains(pressAddress)) {
pressRange = centered(domain, pressAddress, visibleRange.size());
emit rangeRequested(pressRange.start, pressRange.end);
}
}
} else {
drag = Select;
}
event->accept();
}
void DataAnalysisView::updateDrag(QPoint position)
{
const RVA address = addressAt(position.x());
if (drag == Select) {
dragSelection = {qMin(address, pressAddress), qMax(address, pressAddress) + 1};
update();
} else if (drag == LeftEdge) {
emit rangeRequested(qMin(address, pressRange.end - 1), pressRange.end);
} else if (drag == RightEdge) {
emit rangeRequested(pressRange.start, qMax(address + 1, pressRange.start + 1));
} else if (drag == Move) {
const RVA start = address >= pressAddress
? pressRange.start
+ qMin(address - pressAddress, domain.end - pressRange.end)
: pressRange.start
- qMin(pressAddress - address, pressRange.start - domain.start);
emit rangeRequested(start, start + pressRange.size());
}
}
void DataAnalysisView::mouseMoveEvent(QMouseEvent *event)
{
const bool moved = hoverPosition != event->pos();
hoverPosition = event->pos();
hoverX = event->pos().x();
if (drag != None) {
updateDrag(event->pos());
} else if (plotRect().contains(event->pos())) {
if (moved) {
cancelTooltip();
tooltipTimer.start();
}
const bool edge = overview
&& (std::abs(hoverX - xForAddress(visibleRange.start)) < 7
|| std::abs(hoverX - xForAddress(visibleRange.end)) < 7);
setCursor(edge ? Qt::SizeHorCursor : overview ? Qt::OpenHandCursor : Qt::CrossCursor);
} else {
cancelTooltip();
}
update();
}
void DataAnalysisView::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton || drag == None) {
return;
}
updateDrag(event->pos());
if (drag == Select) {
if ((event->pos() - pressPosition).manhattanLength() < QApplication::startDragDistance()) {
emit selectionRequested(0, 0);
emit seekRequested(nearestMarker(event->pos().x()));
} else {
emit selectionRequested(dragSelection.start, dragSelection.end);
}
}
drag = None;
dragSelection = {};
update();
}
void DataAnalysisView::mouseDoubleClickEvent(QMouseEvent *event)
{
cancelTooltip();
if (event->button() == Qt::LeftButton && axisRange().valid()) {
drag = None;
if (overview) {
emit rangeRequested(domain.start, domain.end);
} else {
zoom(0.5, addressAt(event->pos().x()));
}
}
}
void DataAnalysisView::wheelEvent(QWheelEvent *event)
{
cancelTooltip();
const QPoint delta = event->angleDelta();
const int movement = delta.y() ? delta.y() : delta.x();
if (!movement) {
event->ignore();
return;
}
pan(movement > 0 ? -1 : 1);
event->accept();
}
void DataAnalysisView::keyPressEvent(QKeyEvent *event)
{
cancelTooltip();
const RVA center = visibleRange.start + visibleRange.size() / 2;
switch (event->key()) {
case Qt::Key_Plus:
case Qt::Key_Equal:
zoom(0.5, center);
break;
case Qt::Key_Minus:
zoom(2, center);
break;
case Qt::Key_Left:
pan(-1);
break;
case Qt::Key_Right:
pan(1);
break;
case Qt::Key_Home:
emit rangeRequested(domain.start, domain.end);
break;
case Qt::Key_Escape:
drag = None;
emit selectionRequested(0, 0);
update();
break;
default:
QWidget::keyPressEvent(event);
return;
}
event->accept();
}
void DataAnalysisView::leaveEvent(QEvent *)
{
cancelTooltip();
hoverX = -1;
unsetCursor();
QToolTip::hideText();
update();
}
void DataAnalysisView::cancelTooltip()
{
tooltipTimer.stop();
QToolTip::hideText();
}
void DataAnalysisView::hideEvent(QHideEvent *event)
{
cancelTooltip();
QWidget::hideEvent(event);
}
RVA DataAnalysisView::nearestMarker(qreal x) const
{
qreal distance = 5;
RVA result = addressAt(x);
for (const auto &marker : markers) {
if (markerEnabled[marker.type] && axisRange().contains(marker.range.start)) {
const qreal candidate = std::abs(x - xForAddress(marker.range.start));
if (candidate < distance) {
distance = candidate;
result = marker.range.start;
}
}
}
return result;
}
void DataAnalysisView::showTooltip()
{
if (!isVisible() || drag != None || !plotRect().contains(hoverPosition)) {
return;
}
const RVA address = addressAt(hoverPosition.x());
if (address == RVA_INVALID) {
return;
}
QString tip = RAddressString(address);
for (const auto &sample : samples) {
if (!sample.range.contains(address)) {
continue;
}
tip += tr("\nBucket: %1 – %2 (%3 bytes)\nRead %4 of %5 mapped bytes")
.arg(RAddressString(sample.range.start), RAddressString(sample.range.end - 1))
.arg(sample.range.size())
.arg(sample.bytes)
.arg(sample.covered);
if (sample.bytes) {
for (int metric : metrics) {
tip += QString("\n%1: %2")
.arg(metricName(metric), metricValue(metric, sample.values[metric]));
}
} else {
tip += tr("\nNo readable data");
}
break;
}
int found = 0;
for (const auto &marker : markers) {
if (markerEnabled[marker.type]
&& (marker.range.contains(address)
|| std::abs(xForAddress(marker.range.start) - hoverPosition.x()) < 5)) {
tip += QString("\n%1: %2 @ %3")
.arg(
markerName(marker.type),
marker.name.left(160),
RAddressString(marker.range.start));
if (++found == 12) {
break;
}
}
}
QToolTip::showText(mapToGlobal(hoverPosition), tip.toHtmlEscaped().replace('\n', "<br>"), this);
}
void DataAnalysisView::contextMenuEvent(QContextMenuEvent *event)
{
cancelTooltip();
if (axisRange().valid()) {
emit contextRequested(nearestMarker(event->pos().x()), event->globalPos());
event->accept();
}
}
+88
View File
@@ -0,0 +1,88 @@
#ifndef DATAANALYSISVIEW_H
#define DATAANALYSISVIEW_H
#include "common/DataAnalysis.h"
#include <QColor>
#include <QTimer>
#include <QWidget>
class DataAnalysisView : public QWidget
{
Q_OBJECT
public:
enum MarkerType { Section, Segment, Map, Flag, Comment, MarkerCount };
enum Style { Lines, Areas, Bars };
struct Marker
{
DataAnalysis::Range range;
QString name;
MarkerType type;
};
explicit DataAnalysisView(bool overview, QWidget *parent = nullptr);
static QString metricName(int metric);
static QString metricDescription(int metric);
static QString metricValue(int metric, double value);
static QString markerName(MarkerType type);
DataAnalysis::Range domain;
DataAnalysis::Range visibleRange;
DataAnalysis::Range selection;
QVector<DataAnalysis::Sample> samples;
QVector<Marker> markers;
QVector<int> metrics = {DataAnalysis::Entropy, DataAnalysis::Printable, DataAnalysis::Zero};
std::array<QColor, DataAnalysis::Count> colors;
std::array<bool, MarkerCount> markerEnabled = {true, false, false, true, true};
RVA seekAddress = RVA_INVALID;
Style style = Areas;
bool separateTracks = false;
bool loading = false;
QRectF plotRect() const;
RVA addressAt(qreal x) const;
qreal xForAddress(RVA address) const;
void zoom(double factor, RVA anchor);
void pan(int direction);
signals:
void seekRequested(RVA address);
void selectionRequested(RVA start, RVA end); // Half-open.
void rangeRequested(RVA start, RVA end); // Half-open.
void contextRequested(RVA address, QPoint globalPosition);
protected:
bool event(QEvent *) override;
void paintEvent(QPaintEvent *) override;
void mousePressEvent(QMouseEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
void mouseReleaseEvent(QMouseEvent *) override;
void mouseDoubleClickEvent(QMouseEvent *) override;
void wheelEvent(QWheelEvent *) override;
void keyPressEvent(QKeyEvent *) override;
void leaveEvent(QEvent *) override;
void hideEvent(QHideEvent *) override;
void contextMenuEvent(QContextMenuEvent *) override;
private:
bool overview;
enum Drag { None, Select, Move, LeftEdge, RightEdge };
Drag drag = None;
QPoint pressPosition;
RVA pressAddress = 0;
DataAnalysis::Range pressRange;
DataAnalysis::Range dragSelection;
int hoverX = -1;
QTimer tooltipTimer;
QPoint hoverPosition;
DataAnalysis::Range axisRange() const;
RVA nearestMarker(qreal x) const;
void pinchZoom(qreal scaleFactor, qreal x);
void updateDrag(QPoint position);
void showTooltip();
void cancelTooltip();
};
#endif
+966
View File
@@ -0,0 +1,966 @@
#include "DataAnalysisWidget.h"
#include "common/IaitoSeekable.h"
#include "core/MainWindow.h"
#include "dialogs/CommentsDialog.h"
#include "dialogs/FlagDialog.h"
#include "ui_DataAnalysisWidget.h"
#include <QActionGroup>
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QColorDialog>
#include <QDialogButtonBox>
#include <QElapsedTimer>
#include <QFileDialog>
#include <QFormLayout>
#include <QJsonArray>
#include <QJsonObject>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QSaveFile>
#include <QScrollArea>
#include <QSet>
#include <QTextStream>
#include <QToolButton>
#include <algorithm>
using namespace DataAnalysis;
namespace {
RVA number(const QJsonObject &object, const char *key)
{
return object[QLatin1String(key)].toVariant().toULongLong();
}
QByteArray readData(RVA address, int length)
{
auto core = Core()->core();
QByteArray bytes(length, '\0');
int count = 0;
while (count < length) {
// nread distinguishes short/failed reads from actual 0xff bytes. A read
// can end at a submap boundary even when the following map is readable.
const int n = r_io_nread_at(
core->io,
address + count,
reinterpret_cast<ut8 *>(bytes.data()) + count,
length - count);
if (n <= 0) {
break;
}
count += n;
}
bytes.resize(count);
return bytes;
}
} // namespace
DataAnalysisWidget::DataAnalysisWidget(MainWindow *main)
: AddressableDockWidget(main)
, ui(new Ui::DataAnalysisWidget)
{
setObjectName("DataAnalysisWidget");
updateWindowTitle();
auto *contents = new QWidget(this);
ui->setupUi(contents);
ui->legend->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
setWidget(contents);
plot = new DataAnalysisView(false, contents);
plot->setObjectName("analysisPlot");
overview = new DataAnalysisView(true, contents);
overview->setObjectName("analysisOverview");
overview->setFixedHeight(80);
auto *scroll = new QScrollArea(contents);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setWidget(plot);
ui->plotLayout->addWidget(scroll, 1);
ui->plotLayout->addWidget(overview);
ui->verticalLayout->setStretch(3, 1);
setupActions();
updateColors();
refreshDeferrer = createRefreshDeferrer([this]() { reloadTimer.start(); });
reloadTimer.setSingleShot(true);
reloadTimer.setInterval(100);
rangeTimer.setSingleShot(true);
rangeTimer.setInterval(80);
sampleTimer.setInterval(0);
connect(&reloadTimer, &QTimer::timeout, this, &DataAnalysisWidget::reload);
connect(&rangeTimer, &QTimer::timeout, this, [this]() {
if (!samplingOverview) {
beginSampling(false);
}
});
connect(&sampleTimer, &QTimer::timeout, this, &DataAnalysisWidget::sampleBatch);
connect(plot, &DataAnalysisView::seekRequested, seekable, &IaitoSeekable::seek);
connect(plot, &DataAnalysisView::selectionRequested, this, [this](RVA start, RVA end) {
selectRange({start, end}, true);
});
for (auto *view : {plot, overview}) {
connect(view, &DataAnalysisView::rangeRequested, this, [this](RVA start, RVA end) {
setRange({start, end});
});
connect(view, &DataAnalysisView::contextRequested, this, &DataAnalysisWidget::showContextMenu);
}
connect(seekable, &IaitoSeekable::seekableSeekChanged, this, &DataAnalysisWidget::onSeekChanged);
connect(Core(), &IaitoCore::addressRangeSelectionChanged, this, [this](RVA start, RVA end) {
if (!publishingSelection && seekable->isSynchronized()
&& Config()->getAddressRangeSelectionSyncEnabled()) {
selectRange(
start != RVA_INVALID && end != RVA_INVALID && end >= start ? Range{start, end + 1}
: Range{},
false);
}
});
connect(Core(), &IaitoCore::refreshAll, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::codeRebased, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::ioModeChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::ioCacheChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::flagsChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::commentsChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::refreshCodeViews, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::instructionChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::debugTaskStateChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Config(), &Configuration::colorsUpdated, this, &DataAnalysisWidget::updateColors);
connect(this, &QDockWidget::visibilityChanged, this, [this](bool visible) {
if (!visible) {
metadataDirty |= sampleTimer.isActive() || rangeTimer.isActive();
sampleTimer.stop();
rangeTimer.stop();
reloadTimer.stop();
} else if (metadataDirty) {
scheduleReload();
}
});
scheduleReload();
}
DataAnalysisWidget::~DataAnalysisWidget() = default;
QWidget *DataAnalysisWidget::widgetToFocusOnRaise()
{
return plot;
}
void DataAnalysisWidget::setupActions()
{
auto menuButton = [this](const QString &label, QMenu *menu) {
auto *button = new QToolButton(ui->toolbar);
button->setText(label);
button->setPopupMode(QToolButton::InstantPopup);
button->setMenu(menu);
ui->toolbar->addWidget(button);
};
auto *ranges = new QMenu(this);
ranges->addAction(tr("All data"), this, [this]() { setRange(plot->domain); });
ranges->addAction(tr("Current section"), this, [this]() {
fitMarker(DataAnalysisView::Section);
});
ranges->addAction(tr("Current segment"), this, [this]() {
fitMarker(DataAnalysisView::Segment);
});
ranges->addAction(tr("Current map"), this, [this]() { fitMarker(DataAnalysisView::Map); });
ranges->addAction(tr("Current block"), this, [this]() {
const int size = Core()->core()->blocksize;
setRange(sizedRange(seekable->getOffset(), qMax(1, size)));
});
selectionAction
= ranges->addAction(tr("Fit selection"), this, &DataAnalysisWidget::fitSelection);
selectionAction->setEnabled(false);
ranges->addSeparator();
ranges->addAction(tr("Edit range…"), this, &DataAnalysisWidget::editRange);
menuButton(tr("Range"), ranges);
auto *algorithms = new QMenu(this);
for (int i = 0; i < Count; ++i) {
auto *action = algorithms->addAction(DataAnalysisView::metricName(i));
action->setCheckable(true);
action->setChecked(plot->metrics.contains(i));
metricActions[i] = action;
action->setToolTip(DataAnalysisView::metricDescription(i));
auto *entry = ui->legend->addAction(DataAnalysisView::metricName(i));
entry->setToolTip(
DataAnalysisView::metricDescription(i) + tr("\nClick to configure this algorithm."));
legendActions[i] = entry;
connect(entry, &QAction::triggered, this, [this, i]() { configureMetric(i); });
connect(action, &QAction::toggled, this, &DataAnalysisWidget::updateMetrics);
}
menuButton(tr("Algorithms"), algorithms);
ui->toolbar
->addAction(
tr("+"),
this,
[this]() { plot->zoom(0.5, plot->visibleRange.start + plot->visibleRange.size() / 2); })
->setToolTip(tr("Zoom in (+)"));
ui->toolbar
->addAction(
tr("−"),
this,
[this]() { plot->zoom(2, plot->visibleRange.start + plot->visibleRange.size() / 2); })
->setToolTip(tr("Zoom out (-)"));
ui->toolbar->addAction(tr("Fit"), this, [this]() { setRange(plot->domain); })
->setToolTip(tr("Fit all data (Home)"));
followAction = ui->toolbar->addAction(tr("Follow"));
followAction->setCheckable(true);
followAction->setChecked(true);
followAction->setToolTip(tr("Pan to keep the current disassembly or hexdump address visible"));
connect(followAction, &QAction::toggled, this, [this](bool checked) {
if (checked) {
onSeekChanged(seekable->getOffset());
}
});
optionsMenu = new QMenu(this);
tracksAction = optionsMenu->addAction(tr("Separate algorithm tracks"));
tracksAction->setCheckable(true);
connect(tracksAction, &QAction::toggled, this, [this](bool checked) {
plot->separateTracks = checked;
plot->setMinimumHeight(checked ? qMax(180, int(plot->metrics.size()) * 90) : 180);
plot->update();
});
auto *styles = optionsMenu->addMenu(tr("Drawing style"));
auto *styleGroup = new QActionGroup(styles);
const QStringList styleNames = {tr("Lines"), tr("Filled lines"), tr("Vertical bars")};
for (int i = 0; i < styleNames.size(); ++i) {
auto *action = styles->addAction(styleNames[i]);
styleGroup->addAction(action);
action->setCheckable(true);
action->setData(i);
action->setChecked(i == plot->style);
connect(action, &QAction::triggered, this, [this, i]() {
plot->style = static_cast<DataAnalysisView::Style>(i);
plot->update();
});
}
styles->setObjectName("analysisStyles");
auto *colors = optionsMenu->addMenu(tr("Colors"));
auto *colorGroup = new QActionGroup(colors);
const QStringList modes
= {tr("Greyscale"), tr("Rainbow"), tr("Theme single color"), tr("Theme palette")};
for (int i = 0; i < modes.size(); ++i) {
auto *action = colors->addAction(modes[i]);
colorGroup->addAction(action);
action->setCheckable(true);
action->setData(i);
action->setChecked(i == colorMode);
connect(action, &QAction::triggered, this, [this, i]() {
colorMode = i;
updateColors();
});
}
colors->setObjectName("analysisColors");
colors->addSeparator();
auto *custom = colors->addMenu(tr("Customize algorithm colors"));
for (int i = 0; i < Count; ++i) {
custom->addAction(DataAnalysisView::metricName(i), this, [this, i]() {
const QColor color
= QColorDialog::getColor(plot->colors[i], this, DataAnalysisView::metricName(i));
if (color.isValid()) {
customColors[i] = color;
updateColors();
}
});
}
custom->addSeparator();
custom->addAction(tr("Reset custom colors"), this, [this]() {
customColors.fill(QColor());
updateColors();
});
auto *markers = optionsMenu->addMenu(tr("Markers"));
markers->setObjectName("analysisMarkers");
for (int i = 0; i < DataAnalysisView::MarkerCount; ++i) {
auto *action = markers->addAction(
DataAnalysisView::markerName(static_cast<DataAnalysisView::MarkerType>(i)));
action->setCheckable(true);
action->setData(i);
action->setChecked(plot->markerEnabled[i]);
connect(action, &QAction::toggled, this, [this, i](bool checked) {
plot->markerEnabled[i] = overview->markerEnabled[i] = checked;
plot->update();
overview->update();
});
}
auto *samples = optionsMenu->addMenu(tr("Resolution (buckets)"));
samples->setObjectName("analysisResolution");
auto *sampleGroup = new QActionGroup(samples);
for (int n : {32, 64, 128, 256, 512, 1024, 2048, 4096}) {
auto *action = samples->addAction(QString::number(n));
sampleGroup->addAction(action);
action->setCheckable(true);
action->setData(n);
action->setChecked(n == resolution);
connect(action, &QAction::triggered, this, [this, n]() {
resolution = n;
beginSampling(samplingOverview);
});
}
optionsMenu->addSeparator();
optionsMenu->addAction(&syncAction);
optionsMenu->addAction(tr("Find marker…"), this, &DataAnalysisWidget::findMarker);
optionsMenu->addAction(tr("Refresh"), this, &DataAnalysisWidget::scheduleReload);
exportAction = optionsMenu->addAction(
tr("Export visible samples as CSV…"), this, &DataAnalysisWidget::exportCsv);
exportAction->setEnabled(false);
menuButton(tr("Options"), optionsMenu);
ui->legend->setToolTip(
tr("Click a legend entry to configure its color and visibility."
" Overlaid values use a 0–100% scale; entropy is divided by 8."
" Separate tracks use native units."));
updateMetrics();
}
void DataAnalysisWidget::updateMetrics()
{
plot->metrics.clear();
for (int i = 0; i < Count; ++i) {
legendActions[i]->setVisible(metricActions[i]->isChecked());
if (metricActions[i]->isChecked()) {
plot->metrics.append(i);
}
}
overview->metrics = plot->metrics;
plot->setMinimumHeight(plot->separateTracks ? qMax(180, int(plot->metrics.size()) * 90) : 180);
plot->update();
}
void DataAnalysisWidget::updateColors()
{
const char *keys[]
= {"gui.navbar.code",
"gui.navbar.str",
"gui.navbar.data",
"num",
"flag",
"comment",
"flow",
"func_var",
"reg",
"invalid",
"other"};
QColor base = Config()->getColor("gui.navbar.code");
if (!base.isValid()) {
base = QColor(120, 150, 230);
}
QSet<QRgb> used;
for (int i = 0; i < Count; ++i) {
QColor color;
switch (colorMode) {
case 0:
color = QColor::fromHsv(0, 0, 85 + i * 16);
break;
case 1:
color = QColor::fromHsv((270 + i * 137) % 360, 165, 220);
break;
case 2:
color = QColor::fromHsv(qMax(0, base.hue()), 70 + i * 16, 235 - i * 9);
break;
default:
color = Config()->getColor(keys[i]);
if (!color.isValid() || used.contains(color.rgb())
|| color == palette().color(QPalette::Base)) {
color = QColor::fromHsv((270 + i * 137) % 360, 155, 210);
}
break;
}
if (customColors[i].isValid()) {
color = customColors[i];
}
used.insert(color.rgb());
plot->colors[i] = overview->colors[i] = color;
QPixmap swatch(12, 12);
swatch.fill(color);
metricActions[i]->setIcon(QIcon(swatch));
legendActions[i]->setIcon(QIcon(swatch));
}
plot->update();
overview->update();
}
void DataAnalysisWidget::configureMetric(int metric)
{
QDialog dialog(this);
dialog.setObjectName("analysisMetricSettings");
dialog.setWindowTitle(tr("%1 settings").arg(DataAnalysisView::metricName(metric)));
QFormLayout layout(&dialog);
QLabel description(DataAnalysisView::metricDescription(metric));
description.setWordWrap(true);
description.setMaximumWidth(420);
QCheckBox visible(tr("Show this algorithm"));
visible.setObjectName("metricVisible");
visible.setChecked(metricActions[metric]->isChecked());
QColor chosen = customColors[metric];
QPushButton colorButton(tr("Change color…"));
colorButton.setIcon(legendActions[metric]->icon());
QPushButton resetButton(tr("Use palette color"));
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout.addRow(&description);
layout.addRow(tr("Color:"), &colorButton);
layout.addRow(&resetButton);
layout.addRow(&visible);
layout.addRow(&buttons);
connect(&colorButton, &QPushButton::clicked, &dialog, [&]() {
const QColor color = QColorDialog::getColor(
chosen.isValid() ? chosen : plot->colors[metric],
&dialog,
DataAnalysisView::metricName(metric));
if (color.isValid()) {
chosen = color;
QPixmap swatch(12, 12);
swatch.fill(color);
colorButton.setIcon(QIcon(swatch));
colorButton.setText(color.name());
}
});
connect(&resetButton, &QPushButton::clicked, &dialog, [&]() {
chosen = QColor();
colorButton.setIcon(QIcon());
colorButton.setText(tr("Palette default"));
});
connect(&buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (dialog.exec() == QDialog::Accepted) {
customColors[metric] = chosen;
metricActions[metric]->setChecked(visible.isChecked());
updateColors();
}
}
void DataAnalysisWidget::scheduleReload()
{
metadataDirty = true;
if (refreshDeferrer->attemptRefresh(nullptr)) {
sampleTimer.stop();
reloadTimer.start();
}
}
void DataAnalysisWidget::loadMetadata()
{
using View = DataAnalysisView;
virtualAddresses = Core()->getConfigb("io.va");
readable.clear();
plot->markers.clear();
auto addMarker = [this](Range range, const QString &name, View::MarkerType type) {
if (range.valid()) {
plot->markers.append({range, name, type});
}
};
for (const auto &value : Core()->cmdj("omj").array()) {
const QJsonObject map = value.toObject();
const RVA start = number(map, "from"), last = number(map, "to");
const Range range = last >= start ? Range{start, last == RVA_MAX ? RVA_MAX : last + 1}
: Range{};
if (virtualAddresses) {
addMarker(range, map["name"].toString(), View::Map);
if (range.valid() && map["perm"].toString().contains('r')) {
readable.append(range);
}
}
}
if (!virtualAddresses) {
auto core = Core()->core();
const RVA size = r_io_size(core->io);
if (size != RVA_MAX && size) {
readable.append({0, size});
}
}
readable = mergeRanges(readable);
const Range domain = readable.isEmpty() ? Range{}
: Range{readable.first().start, readable.last().end};
const bool changed = domain != plot->domain;
plot->domain = overview->domain = domain;
if (changed || !plot->visibleRange.valid()) {
plot->visibleRange = overview->visibleRange = domain;
}
for (const auto &entry :
{qMakePair(QString("iSj"), View::Section), qMakePair(QString("iSSj"), View::Segment)}) {
const QJsonDocument document = Core()->cmdj(entry.first);
const QJsonArray objects
= document.isArray()
? document.array()
: document.object()[entry.second == View::Section ? "sections" : "segments"]
.toArray();
for (const auto &value : objects) {
const QJsonObject object = value.toObject();
const RVA start = number(object, virtualAddresses ? "vaddr" : "paddr");
const RVA size = number(object, virtualAddresses ? "vsize" : "size");
addMarker(sizedRange(start, size), object["name"].toString(), entry.second);
}
}
// @F temporarily selects all flag spaces without changing the user's space.
for (const auto &value : Core()->cmdj("fj @F:*").array()) {
const QJsonObject flag = value.toObject();
const RVA address = number(flag, flag.contains("offset") ? "offset" : "addr");
addMarker(
sizedRange(address, qMax<RVA>(1, number(flag, "size"))),
flag["name"].toString(),
View::Flag);
}
for (const auto &comment : Core()->getAllComments("CCu")) {
addMarker(sizedRange(comment.offset, 1), comment.name, View::Comment);
}
std::stable_sort(
plot->markers.begin(),
plot->markers.end(),
[](const View::Marker &a, const View::Marker &b) { return a.range.start < b.range.start; });
overview->markers = plot->markers;
metadataDirty = false;
}
void DataAnalysisWidget::reload()
{
if (!refreshDeferrer->attemptRefresh(nullptr) || Core()->isDebugTaskInProgress()) {
return;
}
sampleTimer.stop();
rangeTimer.stop();
loadMetadata();
plot->seekAddress = overview->seekAddress = seekable->getOffset();
if (requestedRange.valid()) {
const Range requested = intersect(requestedRange, plot->domain);
if (requested.valid()) {
plot->visibleRange = overview->visibleRange = requested;
}
requestedRange = {};
}
if (seekable->isSynchronized() && Config()->getAddressRangeSelectionSyncEnabled()
&& Core()->hasAddressRangeSelection()) {
selectRange(
{Core()->getAddressRangeSelectionStart(), Core()->getAddressRangeSelectionEnd() + 1},
false);
}
plot->samples.clear();
overview->samples.clear();
beginSampling(true);
}
void DataAnalysisWidget::beginSampling(bool mini)
{
rangeTimer.stop();
sampleTimer.stop();
if (!isVisibleToUser()) {
metadataDirty = true;
refreshDeferrer->attemptRefresh(nullptr);
return;
}
samplingOverview = mini;
pendingRange = mini ? plot->domain : plot->visibleRange;
pending.clear();
nextBucket = 0;
plot->loading = pendingRange.valid();
exportAction->setEnabled(false);
if (pendingRange.valid()) {
const int count = int(qMin<RVA>(mini ? 256 : resolution, pendingRange.size()));
pending.resize(count);
sampleTimer.start();
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::sampleBatch()
{
if (!isVisibleToUser() || Core()->isDebugTaskInProgress()) {
sampleTimer.stop();
metadataDirty = true;
refreshDeferrer->attemptRefresh(nullptr);
return;
}
QElapsedTimer timer;
timer.start();
// Yield to navigation and painting every few milliseconds. A range change
// cancels the old job; no stale worker result can overwrite the new range.
do {
pending[nextBucket]
= sample(bucket(pendingRange, nextBucket, pending.size()), readable, readData);
++nextBucket;
} while (nextBucket < pending.size() && timer.elapsed() < 6);
if (nextBucket < pending.size()) {
ui->statusLabel->setText(tr("Analyzing %1… %2%")
.arg(samplingOverview ? tr("overview") : tr("visible range"))
.arg(nextBucket * 100 / pending.size()));
return;
}
sampleTimer.stop();
if (samplingOverview) {
overview->samples = pending;
overview->update();
beginSampling(false);
} else {
plot->samples = pending;
plot->loading = false;
exportAction->setEnabled(!plot->samples.isEmpty());
updateLabels();
plot->update();
}
}
void DataAnalysisWidget::setRange(Range range)
{
range = intersect(range, plot->domain);
if (!range.valid() || range == plot->visibleRange) {
return;
}
plot->visibleRange = overview->visibleRange = range;
plot->samples.clear();
plot->loading = true;
exportAction->setEnabled(false);
if (!samplingOverview) {
sampleTimer.stop();
}
rangeTimer.start();
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::showRange(RVA start, RVA end)
{
if (start == RVA_INVALID || end == RVA_INVALID || end < start) {
return;
}
requestedRange = {start, end + 1};
raiseMemoryWidget();
seekable->seek(start);
if (metadataDirty) {
scheduleReload();
} else {
setRange(requestedRange);
requestedRange = {};
}
}
void DataAnalysisWidget::onSeekChanged(RVA address)
{
plot->seekAddress = overview->seekAddress = address;
if (followAction->isChecked() && plot->domain.contains(address)
&& !plot->visibleRange.contains(address)) {
setRange(centered(plot->domain, address, plot->visibleRange.size()));
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::selectRange(Range range, bool publish)
{
plot->selection = overview->selection = range;
selectionAction->setEnabled(range.valid());
if (publish && seekable->isSynchronized() && Config()->getAddressRangeSelectionSyncEnabled()) {
publishingSelection = true;
if (range.valid()) {
Core()->setAddressRangeSelection(range.start, range.end - 1);
} else {
Core()->clearAddressRangeSelection();
}
publishingSelection = false;
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::updateLabels()
{
const Range range = plot->visibleRange;
if (!range.valid()) {
ui->rangeLabel->setText(tr("No readable data in the current address space"));
ui->statusLabel->setText(tr("Open a file or refresh after mapping data."));
return;
}
QString text = tr("%1: %2 – %3 · %4 bytes · Cursor %5")
.arg(
virtualAddresses ? tr("Addresses") : tr("File offsets"),
RAddressString(range.start),
RAddressString(range.end - 1))
.arg(range.size())
.arg(RAddressString(seekable->getOffset()));
if (plot->selection.valid()) {
text += tr("\nSelection: %1 – %2 · %3 bytes")
.arg(
RAddressString(plot->selection.start),
RAddressString(plot->selection.end - 1))
.arg(plot->selection.size());
}
ui->rangeLabel->setText(text);
if (plot->loading) {
ui->statusLabel->setText(tr("Analyzing…"));
} else {
RVA bytes = 0, covered = 0;
for (const auto &sample : plot->samples) {
bytes += sample.bytes;
covered += sample.covered;
}
if (!bytes) {
ui->statusLabel->setText(tr("No readable bytes in the visible range."));
return;
}
ui->statusLabel->setText(
tr("%1 buckets · %2 / %3 mapped bytes read (%4) · Overview: entropy")
.arg(plot->samples.size())
.arg(bytes)
.arg(covered)
.arg(bytes == covered ? tr("complete") : tr("sampled or unreadable")));
ui->statusLabel->setToolTip(
tr("Each bucket reads up to 4096 bytes from centered windows in readable spans."
" Zoom in for complete coverage. Unmapped bytes are excluded."
" Shannon entropy: 0–8 bits/byte. Overlays use normalized percentages."));
}
}
void DataAnalysisWidget::fitSelection()
{
if (plot->selection.valid()) {
setRange(plot->selection);
}
}
void DataAnalysisWidget::fitMarker(DataAnalysisView::MarkerType type)
{
Range best;
for (const auto &marker : plot->markers) {
if (marker.type == type && marker.range.contains(seekable->getOffset())
&& (!best.valid() || marker.range.size() < best.size())) {
best = marker.range;
}
}
if (best.valid()) {
setRange(best);
} else {
ui->statusLabel->setText(
tr("No %1 contains the current address.").arg(DataAnalysisView::markerName(type)));
}
}
void DataAnalysisWidget::editRange()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Data analysis range"));
QFormLayout layout(&dialog);
QLineEdit start(RAddressString(plot->visibleRange.start));
QLineEdit end(RAddressString(plot->visibleRange.valid() ? plot->visibleRange.end - 1 : 0));
QLabel hint(tr("Enter addresses or radare2 expressions. Both endpoints are inclusive."));
hint.setWordWrap(true);
QLabel error;
error.setWordWrap(true);
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout.addRow(&hint);
layout.addRow(tr("Start:"), &start);
layout.addRow(tr("End (inclusive):"), &end);
layout.addRow(&error);
layout.addRow(&buttons);
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
connect(&buttons, &QDialogButtonBox::accepted, &dialog, [&]() {
RVA first, last;
if (!Core()->tryMath(start.text(), first) || !Core()->tryMath(end.text(), last)
|| first == RVA_INVALID || last == RVA_INVALID || first > last) {
error.setText(tr("Enter a valid start and an end greater than or equal to it."));
return;
}
const Range requested{first, last + 1};
if (intersect(requested, plot->domain) != requested) {
error.setText(
tr("The range must be within %1 – %2.")
.arg(RAddressString(plot->domain.start), RAddressString(plot->domain.end - 1)));
return;
}
setRange(requested);
dialog.accept();
});
dialog.exec();
}
void DataAnalysisWidget::findMarker()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Find section, map, flag or comment"));
dialog.resize(620, 400);
QVBoxLayout layout(&dialog);
QLineEdit filter;
filter.setPlaceholderText(tr("Filter by name, comment, type or hexadecimal address…"));
QListWidget list;
QLabel hint(
tr("Activate a result to seek. Section, segment and map results also fit the range."
" Showing up to 500 matches."));
hint.setWordWrap(true);
QDialogButtonBox buttons(QDialogButtonBox::Close);
layout.addWidget(&filter);
layout.addWidget(&list);
layout.addWidget(&hint);
layout.addWidget(&buttons);
// Keep result indices stable while the modal loop receives metadata updates.
const auto markers = plot->markers;
auto fill = [&]() {
list.clear();
for (int i = 0; i < markers.size(); ++i) {
const auto &marker = markers[i];
const QString label = QString("%1 · %2 · %3")
.arg(
RAddressString(marker.range.start),
DataAnalysisView::markerName(marker.type),
marker.name);
if (label.contains(filter.text(), Qt::CaseInsensitive)) {
auto *item = new QListWidgetItem(label, &list);
item->setData(Qt::UserRole, i);
if (list.count() >= 500) {
break;
}
}
}
};
connect(&filter, &QLineEdit::textChanged, &dialog, fill);
connect(&list, &QListWidget::itemActivated, &dialog, [&](QListWidgetItem *item) {
const int index = item->data(Qt::UserRole).toInt();
if (index < 0 || index >= markers.size()) {
return;
}
const auto marker = markers[index];
seekable->seek(marker.range.start);
if (marker.type <= DataAnalysisView::Map) {
setRange(marker.range);
} else if (!plot->visibleRange.contains(marker.range.start)) {
setRange(centered(plot->domain, marker.range.start, plot->visibleRange.size()));
}
dialog.accept();
});
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
fill();
dialog.exec();
}
void DataAnalysisWidget::showContextMenu(RVA address, QPoint position)
{
QMenu menu(this);
menu.addAction(RAddressString(address))->setEnabled(false);
menu.addAction(tr("Seek here"), this, [this, address]() { seekable->seek(address); });
if (mainWindow) {
auto *showIn = mainWindow->createShowInMenu(&menu, address);
showIn->setTitle(tr("Show in"));
menu.addMenu(showIn);
}
menu.addAction(tr("Copy address"), this, [address]() {
QApplication::clipboard()->setText(RAddressString(address));
});
menu.addAction(tr("Add or edit comment…"), this, [this, address]() {
CommentsDialog::addOrEditComment(address, this);
});
const Range selection = plot->selection;
const RVA flagSize = selection.contains(address) ? selection.end - address : 1;
menu.addAction(tr("Add or edit flag…"), this, [this, address, flagSize]() {
FlagDialog dialog(address, flagSize, this);
dialog.exec();
});
menu.addSeparator();
menu.addAction(selectionAction);
menu.addAction(tr("Clear selection"), this, [this]() { selectRange({}, true); });
menu.addAction(tr("Edit range…"), this, &DataAnalysisWidget::editRange);
menu.addAction(tr("Find marker…"), this, &DataAnalysisWidget::findMarker);
menu.addAction(tr("Fit all data"), this, [this]() { setRange(plot->domain); });
menu.addSeparator();
menu.addAction(&syncAction);
menu.exec(position);
}
void DataAnalysisWidget::exportCsv()
{
const auto samples = plot->samples;
const auto metrics = plot->metrics;
const QString path = QFileDialog::getSaveFileName(
this, tr("Export data analysis"), QString(), tr("CSV files (*.csv)"));
if (path.isEmpty()) {
return;
}
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, tr("Export failed"), file.errorString());
return;
}
QTextStream out(&file);
out << "start,end_exclusive,bytes_read,mapped_bytes";
for (int metric : metrics) {
QString name = DataAnalysisView::metricName(metric);
name.replace('"', "\"\"");
out << ",\"" << name << " (normalized 0-1)\"";
}
out << '\n';
for (const auto &sample : samples) {
out << RAddressString(sample.range.start) << ',' << RAddressString(sample.range.end) << ','
<< sample.bytes << ',' << sample.covered;
for (int metric : metrics) {
out << ',';
if (sample.bytes) {
out << QString::number(sample.values[metric], 'g', 12);
}
}
out << '\n';
}
out.flush();
if (!file.commit()) {
QMessageBox::warning(this, tr("Export failed"), file.errorString());
}
}
QVariantMap DataAnalysisWidget::serializeViewProprties()
{
auto properties = AddressableDockWidget::serializeViewProprties();
QVariantList metrics, colors, markers;
for (int i = 0; i < Count; ++i) {
if (metricActions[i]->isChecked()) {
metrics.append(i);
}
colors.append(customColors[i].isValid() ? customColors[i].name() : QString());
}
for (bool enabled : plot->markerEnabled) {
markers.append(enabled);
}
properties["metrics"] = metrics;
properties["colors"] = colors;
properties["markers"] = markers;
properties["style"] = int(plot->style);
properties["colorMode"] = colorMode;
properties["resolution"] = resolution;
properties["tracks"] = tracksAction->isChecked();
properties["follow"] = followAction->isChecked();
return properties;
}
void DataAnalysisWidget::deserializeViewProperties(const QVariantMap &properties)
{
AddressableDockWidget::deserializeViewProperties(properties);
const QVariantList metrics
= properties.value("metrics", QVariantList{Entropy, Printable, Zero}).toList();
const QVariantList colors = properties.value("colors").toList();
for (int i = 0; i < Count; ++i) {
metricActions[i]->setChecked(metrics.contains(i));
customColors[i] = i < colors.size() ? QColor(colors[i].toString()) : QColor();
}
auto restoreMenu = [this](const char *name, int selected) {
for (auto *action : optionsMenu->findChild<QMenu *>(name)->actions()) {
if (action->isCheckable() && action->data().toInt() == selected) {
action->trigger();
}
}
};
restoreMenu("analysisStyles", qBound(0, properties.value("style", 1).toInt(), 2));
restoreMenu("analysisColors", qBound(0, properties.value("colorMode", 3).toInt(), 3));
int count = properties.value("resolution", 1024).toInt();
if (!QList<int>{32, 64, 128, 256, 512, 1024, 2048, 4096}.contains(count)) {
count = 1024;
}
restoreMenu("analysisResolution", count);
const QVariantList markers
= properties.value("markers", QVariantList{true, false, false, true, true}).toList();
for (auto *action : optionsMenu->findChild<QMenu *>("analysisMarkers")->actions()) {
const int index = action->data().toInt();
action->setChecked(index < markers.size() && markers[index].toBool());
}
tracksAction->setChecked(properties.value("tracks", false).toBool());
followAction->setChecked(properties.value("follow", true).toBool());
updateColors();
}
+81
View File
@@ -0,0 +1,81 @@
#ifndef DATAANALYSISWIDGET_H
#define DATAANALYSISWIDGET_H
#include "AddressableDockWidget.h"
#include "DataAnalysisView.h"
#include <memory>
#include <QTimer>
namespace Ui {
class DataAnalysisWidget;
}
class QMenu;
class DataAnalysisWidget : public AddressableDockWidget
{
Q_OBJECT
public:
explicit DataAnalysisWidget(MainWindow *main);
~DataAnalysisWidget() override;
QVariantMap serializeViewProprties() override;
void deserializeViewProperties(const QVariantMap &properties) override;
// Reusable entry point for other widgets. Both endpoints are inclusive.
void showRange(RVA start, RVA end);
protected:
QString getWindowTitle() const override { return tr("Data Analysis"); }
QWidget *widgetToFocusOnRaise() override;
private:
std::unique_ptr<Ui::DataAnalysisWidget> ui;
DataAnalysisView *plot;
DataAnalysisView *overview;
RefreshDeferrer *refreshDeferrer;
QTimer reloadTimer;
QTimer sampleTimer;
QTimer rangeTimer;
QVector<DataAnalysis::Range> readable;
QVector<DataAnalysis::Sample> pending;
DataAnalysis::Range pendingRange;
DataAnalysis::Range requestedRange;
int nextBucket = 0;
int resolution = 1024;
int colorMode = 3;
bool samplingOverview = false;
bool virtualAddresses = true;
bool metadataDirty = true;
bool publishingSelection = false;
QAction *followAction;
QAction *tracksAction;
QAction *selectionAction;
QAction *exportAction;
QMenu *optionsMenu;
std::array<QAction *, DataAnalysis::Count> metricActions;
std::array<QAction *, DataAnalysis::Count> legendActions;
std::array<QColor, DataAnalysis::Count> customColors;
void setupActions();
void scheduleReload();
void reload();
void loadMetadata();
void beginSampling(bool mini);
void sampleBatch();
void setRange(DataAnalysis::Range range);
void updateLabels();
void updateColors();
void updateMetrics();
void configureMetric(int metric);
void onSeekChanged(RVA address);
void selectRange(DataAnalysis::Range range, bool publish);
void fitSelection();
void editRange();
void fitMarker(DataAnalysisView::MarkerType type);
void findMarker();
void showContextMenu(RVA address, QPoint position);
void exportCsv();
};
#endif
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DataAnalysisWidget</class>
<widget class="QWidget" name="DataAnalysisWidget">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing"><number>4</number></property>
<property name="leftMargin"><number>4</number></property>
<property name="topMargin"><number>4</number></property>
<property name="rightMargin"><number>4</number></property>
<property name="bottomMargin"><number>4</number></property>
<item><widget class="QToolBar" name="toolbar"><property name="movable"><bool>false</bool></property></widget></item>
<item><widget class="QLabel" name="rangeLabel">
<property name="text"><string>Open a file to analyze its data</string></property>
<property name="wordWrap"><bool>true</bool></property>
<property name="textInteractionFlags"><set>Qt::TextSelectableByMouse</set></property>
</widget></item>
<item><widget class="QToolBar" name="legend"><property name="movable"><bool>false</bool></property></widget></item>
<item><layout class="QVBoxLayout" name="plotLayout"/></item>
<item><widget class="QLabel" name="statusLabel">
<property name="wordWrap"><bool>true</bool></property>
<property name="text"><string>Click to seek · Drag to select · Wheel or pinch to zoom · Shift+wheel to pan</string></property>
<property name="textInteractionFlags"><set>Qt::TextSelectableByMouse</set></property>
</widget></item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+408
View File
@@ -0,0 +1,408 @@
#include "DecompilerTextEdit.h"
#include <algorithm>
#include <QAbstractTextDocumentLayout>
#include <QMouseEvent>
#include <QPainter>
#include <QScopedValueRollback>
#include <QSet>
#include <QSignalBlocker>
#include <QStyle>
#include <QStyleOption>
#include <QTextBlock>
DecompilerTextEdit::DecompilerTextEdit(QWidget *parent)
: QPlainTextEdit(parent)
, foldArea(new QWidget(this))
{
setLineWrapMode(QPlainTextEdit::NoWrap);
foldArea->setObjectName(QStringLiteral("decompilerFoldArea"));
foldArea->setAccessibleName(tr("Code folding"));
foldArea->setToolTip(tr("Click an arrow to collapse or expand a { ... } block"));
foldArea->setMouseTracking(true);
foldArea->installEventFilter(this);
connect(this, &QPlainTextEdit::updateRequest, this, [this](const QRect &rect, int dy) {
if (foldArea->height() != viewport()->height()) {
updateFoldAreaGeometry();
}
if (dy) {
foldArea->scroll(0, dy);
} else {
foldArea->update(0, rect.y(), foldArea->width(), rect.height());
}
});
connect(this, &QPlainTextEdit::cursorPositionChanged, this, &DecompilerTextEdit::revealCursor);
updateFoldAreaGeometry();
}
void DecompilerTextEdit::setDecompiledCode(
const QString &text, const QString &source, bool preserveFolds)
{
QScopedValueRollback<bool> guard(updatingCode, true);
QString normalizedSource = source;
normalizedSource.replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
normalizedSource.replace(QLatin1Char('\r'), QLatin1Char('\n'));
preserveFolds = preserveFolds && normalizedSource == foldingSource;
const QTextCursor previous = textCursor();
const int previousLine = previous.blockNumber();
const QString sourceLine = foldingSource.section(QLatin1Char('\n'), previousLine, previousLine);
const int sourceColumn
= qMax(0, previous.positionInBlock() - (previous.block().text().size() - sourceLine.size()));
QSet<QPair<int, int>> collapsed;
if (preserveFolds) {
for (const FoldRegion &region : regions) {
if (region.collapsed) {
collapsed.insert({region.first, region.last});
}
}
}
regions.clear();
setPlainText(text);
foldingSource = normalizedSource;
findFoldRegions(normalizedSource);
for (FoldRegion &region : regions) {
region.collapsed = collapsed.contains({region.first, region.last});
}
if (preserveFolds) {
const QTextBlock block = document()->findBlockByNumber(previousLine);
if (block.isValid()) {
const int prefix = block.text().size() - sourceLine.size();
QTextCursor cursor(block);
cursor.setPosition(
block.position() + qBound(0, prefix + sourceColumn, block.length() - 1));
setTextCursor(cursor);
}
}
applyFolding();
}
void DecompilerTextEdit::findFoldRegions(const QString &source)
{
enum State { Code, LineComment, BlockComment, Quoted, RawString } state = Code;
QVector<QPair<int, int>> openings;
matchingBraces.clear();
// Translate source positions past optional address and disassembly columns.
QVector<int> lineOffsets;
QTextBlock block = document()->begin();
int sourcePosition = 0;
for (const QString &text : source.split(QLatin1Char('\n'))) {
lineOffsets.append(block.position() + block.text().size() - text.size() - sourcePosition);
sourcePosition += text.size() + 1;
block = block.next();
}
QChar quote;
QString rawEnd;
int line = 0;
for (int i = 0; i < source.size(); ++i) {
const QChar c = source.at(i);
const QChar next = i + 1 < source.size() ? source.at(i + 1) : QChar();
if (c == QLatin1Char('\n')) {
++line;
}
if (state == LineComment) {
if (c == QLatin1Char('\n') && (i == 0 || source.at(i - 1) != QLatin1Char('\\'))) {
state = Code;
}
} else if (state == BlockComment) {
if (c == QLatin1Char('*') && next == QLatin1Char('/')) {
state = Code;
++i;
}
} else if (state == Quoted) {
if (c == QLatin1Char('\\') && !next.isNull()) {
if (next == QLatin1Char('\n')) {
++line;
}
++i;
} else if (c == quote) {
state = Code;
}
} else if (state == RawString) {
if (source.mid(i, rawEnd.size()) == rawEnd) {
i += rawEnd.size() - 1;
state = Code;
}
} else if (c == QLatin1Char('/') && next == QLatin1Char('/')) {
state = LineComment;
++i;
} else if (c == QLatin1Char('/') && next == QLatin1Char('*')) {
state = BlockComment;
++i;
} else if (c == QLatin1Char('R') && next == QLatin1Char('"')) {
const int length = source.mid(i + 2, 17).indexOf(QLatin1Char('('));
const QString delimiter = source.mid(i + 2, qMax(0, length));
if (length >= 0 && std::none_of(delimiter.cbegin(), delimiter.cend(), [](QChar c) {
return c.isSpace() || c == QLatin1Char('\\') || c == QLatin1Char(')');
})) {
rawEnd = QLatin1Char(')') + delimiter + QLatin1Char('"');
i += length + 2;
state = RawString;
}
} else if (c == QLatin1Char('"') || c == QLatin1Char('\'')) {
quote = c;
state = Quoted;
} else if (c == QLatin1Char('{')) {
openings.append({line, i + lineOffsets.at(line)});
} else if (c == QLatin1Char('}') && !openings.isEmpty()) {
const auto opening = openings.takeLast();
const int closing = i + lineOffsets.at(line);
matchingBraces.insert(opening.second, closing);
matchingBraces.insert(closing, opening.second);
if (line > opening.first + 1) {
regions.append({opening.first, line});
}
}
}
std::sort(regions.begin(), regions.end(), [](const FoldRegion &a, const FoldRegion &b) {
return a.first != b.first ? a.first < b.first : a.last > b.last;
});
}
int DecompilerTextEdit::foldForLine(int line) const
{
auto it = std::lower_bound(
regions.cbegin(), regions.cend(), line, [](const FoldRegion &region, int line) {
return region.first < line;
});
return it != regions.cend() && it->first == line ? int(it - regions.cbegin()) : -1;
}
bool DecompilerTextEdit::isFolded(int line) const
{
const int index = foldForLine(line);
return index >= 0 && regions.at(index).collapsed;
}
bool DecompilerTextEdit::isFoldable(int line) const
{
return foldForLine(line) >= 0;
}
void DecompilerTextEdit::toggleFold(int line)
{
const int index = foldForLine(line);
if (index >= 0) {
const bool collapsed = !regions.at(index).collapsed;
// One gutter arrow controls all braces opened on the same line.
for (int i = index; i < regions.size() && regions.at(i).first == line; ++i) {
regions[i].collapsed = collapsed;
}
applyFolding();
}
}
void DecompilerTextEdit::toggleFoldAtCursor()
{
const int line = textCursor().blockNumber();
int index = foldForLine(line);
if (index < 0) {
for (int i = 0; i < regions.size(); ++i) {
if (regions.at(i).first < line && regions.at(i).last >= line) {
index = i;
}
}
}
if (index >= 0) {
toggleFold(regions.at(index).first);
}
}
void DecompilerTextEdit::jumpToMatchingBrace()
{
const QTextCursor cursor = textCursor();
const auto match = matchingBraces.lowerBound(cursor.position());
if (match == matchingBraces.cend()
|| match.key() >= cursor.block().position() + cursor.block().length() - 1) {
return;
}
QTextCursor target(document());
target.setPosition(match.value());
setTextCursor(target);
ensureCursorVisible();
}
void DecompilerTextEdit::collapseAll()
{
for (FoldRegion &region : regions) {
region.collapsed = true;
}
applyFolding();
}
void DecompilerTextEdit::expandAll()
{
for (FoldRegion &region : regions) {
region.collapsed = false;
}
applyFolding();
}
void DecompilerTextEdit::applyFolding()
{
QVector<int> depth(blockCount() + 1, 0);
for (const FoldRegion &region : regions) {
if (region.collapsed && region.last < blockCount()) {
++depth[region.first + 1];
--depth[region.last];
}
}
int hidden = 0;
for (QTextBlock block = document()->begin(); block.isValid(); block = block.next()) {
hidden += depth.at(block.blockNumber());
block.setVisible(hidden == 0);
block.setLineCount(hidden ? 0 : 1);
}
QTextBlock cursorBlock = textCursor().block();
if (!cursorBlock.isVisible()) {
while (cursorBlock.isValid() && !cursorBlock.isVisible()) {
cursorBlock = cursorBlock.previous();
}
QSignalBlocker blocker(this);
setTextCursor(QTextCursor(cursorBlock));
}
document()->markContentsDirty(0, document()->characterCount());
auto *layout = document()->documentLayout();
emit layout->documentSizeChanged(layout->documentSize());
viewport()->update();
foldArea->update();
emit foldingChanged();
}
void DecompilerTextEdit::revealCursor()
{
if (updatingCode) {
return;
}
const int line = textCursor().blockNumber();
QSet<int> openings;
for (const FoldRegion &region : regions) {
if (region.collapsed && region.first < line && region.last > line) {
openings.insert(region.first);
}
}
if (!openings.isEmpty()) {
for (FoldRegion &region : regions) {
if (openings.contains(region.first)) {
region.collapsed = false;
}
}
applyFolding();
}
}
void DecompilerTextEdit::updateFoldAreaGeometry()
{
const int width = qMax(16, fontMetrics().height());
if (viewportMargins().left() != width) {
setViewportMargins(width, 0, 0, 0);
}
const QRect rect = viewport()->geometry();
foldArea->setGeometry(contentsRect().left(), rect.top(), width, rect.height());
}
void DecompilerTextEdit::resizeEvent(QResizeEvent *event)
{
QPlainTextEdit::resizeEvent(event);
updateFoldAreaGeometry();
}
void DecompilerTextEdit::changeEvent(QEvent *event)
{
QPlainTextEdit::changeEvent(event);
if (event->type() == QEvent::FontChange) {
updateFoldAreaGeometry();
}
foldArea->update();
}
int DecompilerTextEdit::foldAt(const QPoint &point) const
{
const QPoint viewportPoint = foldArea->mapTo(this, point) - viewport()->pos();
const QTextCursor cursor = cursorForPosition(QPoint(0, viewportPoint.y()));
const QRect rect = cursorRect(cursor);
if (!cursor.block().isVisible() || viewportPoint.y() < rect.top()
|| viewportPoint.y() > rect.bottom()) {
return -1;
}
return foldForLine(cursor.blockNumber());
}
bool DecompilerTextEdit::eventFilter(QObject *object, QEvent *event)
{
if (object == foldArea) {
if (event->type() == QEvent::Paint) {
paintFoldArea();
return true;
}
if (event->type() == QEvent::MouseMove) {
auto *mouse = static_cast<QMouseEvent *>(event);
foldArea->setCursor(
foldAt(mouse->pos()) >= 0 ? Qt::PointingHandCursor : Qt::ArrowCursor);
return true;
}
if (event->type() == QEvent::MouseButtonPress) {
auto *mouse = static_cast<QMouseEvent *>(event);
const int index = foldAt(mouse->pos());
if (mouse->button() == Qt::LeftButton && index >= 0) {
toggleFold(regions.at(index).first);
}
return true;
}
if (event->type() == QEvent::MouseButtonRelease
|| event->type() == QEvent::MouseButtonDblClick) {
return true;
}
}
return QPlainTextEdit::eventFilter(object, event);
}
void DecompilerTextEdit::paintFoldArea()
{
QPainter painter(foldArea);
painter.fillRect(foldArea->rect(), palette().alternateBase());
for (QTextBlock block = firstVisibleBlock(); block.isValid(); block = block.next()) {
if (!block.isVisible()) {
continue;
}
const QRectF rect = blockBoundingGeometry(block).translated(contentOffset());
if (rect.top() >= viewport()->height()) {
break;
}
const int index = foldForLine(block.blockNumber());
if (index < 0) {
continue;
}
QStyleOption option;
option.initFrom(foldArea);
option.rect = QRect(
0, qRound(rect.top()), foldArea->width(), qRound(blockBoundingRect(block).height()));
option.state |= QStyle::State_Children;
if (!regions.at(index).collapsed) {
option.state |= QStyle::State_Open;
}
style()->drawPrimitive(QStyle::PE_IndicatorBranch, &option, &painter, foldArea);
}
}
void DecompilerTextEdit::paintEvent(QPaintEvent *event)
{
QPlainTextEdit::paintEvent(event);
QPainter painter(viewport());
painter.setPen(palette().color(QPalette::Mid));
for (const FoldRegion &region : regions) {
if (!region.collapsed) {
continue;
}
const QTextBlock block = document()->findBlockByNumber(region.first);
if (!block.isVisible()) {
continue;
}
QTextCursor cursor(block);
cursor.movePosition(QTextCursor::EndOfBlock);
const QRect rect = cursorRect(cursor);
if (rect.intersects(viewport()->rect())) {
painter
.drawText(rect.topLeft() + QPoint(3, fontMetrics().ascent()), QStringLiteral(" ..."));
}
}
}
+58
View File
@@ -0,0 +1,58 @@
#ifndef DECOMPILERTEXTEDIT_H
#define DECOMPILERTEXTEDIT_H
#include <QMap>
#include <QPlainTextEdit>
#include <QVector>
class DecompilerTextEdit : public QPlainTextEdit
{
Q_OBJECT
public:
explicit DecompilerTextEdit(QWidget *parent = nullptr);
void setDecompiledCode(const QString &text, const QString &source, bool preserveFolds);
bool isFolded(int line) const;
bool isFoldable(int line) const;
public slots:
void toggleFold(int line);
void toggleFoldAtCursor();
void jumpToMatchingBrace();
void collapseAll();
void expandAll();
signals:
void foldingChanged();
protected:
bool eventFilter(QObject *object, QEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void changeEvent(QEvent *event) override;
void paintEvent(QPaintEvent *event) override;
private:
struct FoldRegion
{
int first;
int last;
bool collapsed = false;
};
QWidget *foldArea;
QVector<FoldRegion> regions;
QMap<int, int> matchingBraces;
QString foldingSource;
bool updatingCode = false;
int foldForLine(int line) const;
int foldAt(const QPoint &point) const;
void findFoldRegions(const QString &source);
void applyFolding();
void revealCursor();
void updateFoldAreaGeometry();
void paintFoldArea();
};
#endif // DECOMPILERTEXTEDIT_H
+541 -50
View File
@@ -1,5 +1,7 @@
#include "DecompilerWidget.h"
#include "DecompilerTextEdit.h"
#include "common/ShortcutManager.h"
#include "menus/ContextMenuIcons.h"
#include "menus/DecompilerContextMenu.h"
#include "ui_DecompilerWidget.h"
@@ -18,7 +20,9 @@
#include <QPushButton>
#include "dialogs/ShortcutKeysDialog.h"
#include <algorithm>
#include <QAbstractSlider>
#include <QApplication>
#include <QClipboard>
#include <QDesktopServices>
#include <QDir>
@@ -31,11 +35,13 @@
#include <QLineEdit>
#include <QObject>
#include <QPlainTextEdit>
#include <QRegularExpression>
#include <QScrollBar>
#include <QSet>
#include <QTextBlock>
#include <QTextBlockUserData>
#include <QTextEdit>
#include <QTextLayout>
#include <QUrl>
#include <QWheelEvent>
@@ -55,7 +61,12 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
, code(r_codemeta_new(""), &r_codemeta_free)
{
ui->setupUi(this);
// Keep a keyboard cursor in the read-only decompilation listing.
ui->textEdit->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
setFocusProxy(ui->textEdit);
setupSeekHistoryBar(true);
showAddressColumn = Config()->getDecompilerShowAddresses();
showDisassemblyColumn = Config()->getDecompilerShowDisassembly();
setObjectName(main ? main->getUniqueObjectName(getWidgetType()) : getWidgetType());
updateWindowTitle();
@@ -63,8 +74,19 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
// Event filter to intercept key, mouse double click, and right click in the textbox
ui->textEdit->viewport()->installEventFilter(this);
ui->textEdit->installEventFilter(this);
// Needed so hovering a deep link shows a pointing-hand cursor without a press
ui->textEdit->setCursor(Qt::ArrowCursor);
ui->textEdit->viewport()->setCursor(Qt::ArrowCursor);
ui->textEdit->setMouseTracking(true);
ui->textEdit->viewport()->setMouseTracking(true);
for (QScrollBar *scrollBar :
{ui->textEdit->horizontalScrollBar(), ui->textEdit->verticalScrollBar()}) {
connect(
scrollBar,
&QScrollBar::valueChanged,
this,
&DecompilerWidget::refreshDecompilerHover,
Qt::QueuedConnection);
}
setupFonts();
colorsUpdatedSlot();
@@ -117,8 +139,55 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
connect(Core(), &IaitoCore::breakpointsChanged, this, &DecompilerWidget::updateBreakpoints);
mCtxMenu->addSeparator();
using ContextMenuIcons::Icon;
using ContextMenuIcons::setIcon;
auto bindEditorAction = [this](const char *id, QAction *action) {
ShortcutMgr()->bindAction(id, action);
action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
action->setAutoRepeat(false);
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
action->setShortcutVisibleInContextMenu(true);
#endif
ui->textEdit->addAction(action);
};
auto *followAction = new QAction(tr("Follow reference"), this);
bindEditorAction("decompiler.followReference", followAction);
setIcon(this, followAction, Icon::Navigation, QColor(103, 80, 164));
connect(followAction, &QAction::triggered, this, &DecompilerWidget::seekToReference);
mCtxMenu->insertAction(mCtxMenu->actions().first(), followAction);
const QColor foldingColor(191, 128, 32);
QMenu *foldingMenu = mCtxMenu->addMenu(tr("Code folding"));
setIcon(this, foldingMenu->menuAction(), Icon::Fold, foldingColor);
QAction *toggleFoldAction = foldingMenu->addAction(
tr("Collapse/expand block"), ui->textEdit, &DecompilerTextEdit::toggleFoldAtCursor);
setIcon(this, toggleFoldAction, Icon::Fold, foldingColor);
bindEditorAction("decompiler.toggleFold", toggleFoldAction);
auto *matchBraceAction = foldingMenu->addAction(
tr("Jump to matching brace"), ui->textEdit, &DecompilerTextEdit::jumpToMatchingBrace);
bindEditorAction("decompiler.matchBrace", matchBraceAction);
setIcon(this, matchBraceAction, Icon::Navigation, QColor(103, 80, 164));
setIcon(
this,
foldingMenu
->addAction(tr("Collapse all blocks"), ui->textEdit, &DecompilerTextEdit::collapseAll),
Icon::Collapse,
foldingColor);
setIcon(
this,
foldingMenu->addAction(tr("Expand all blocks"), ui->textEdit, &DecompilerTextEdit::expandAll),
Icon::Expand,
foldingColor);
connect(ui->textEdit, &DecompilerTextEdit::foldingChanged, this, [this]() {
updateCursorContext();
updateSelection();
refreshDecompilerHover();
});
mCtxMenu->addAction(&syncAction);
addActions(mCtxMenu->actions());
setIcon(this, &syncAction, Icon::Sync, QColor(103, 80, 164));
auto menuActions = mCtxMenu->actions();
menuActions.removeOne(followAction);
addActions(menuActions);
ui->progressLabel->setVisible(false);
ui->decompileButton->setVisible(false);
@@ -160,6 +229,10 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
connect(Core(), &IaitoCore::commentsChanged, this, &DecompilerWidget::refreshIfChanged);
connect(Core(), &IaitoCore::instructionChanged, this, &DecompilerWidget::refreshIfChanged);
connect(Core(), &IaitoCore::refreshCodeViews, this, &DecompilerWidget::doRefresh);
connect(Core(), &IaitoCore::refreshAll, this, &DecompilerWidget::invalidateTokenTargets);
connect(Core(), &IaitoCore::flagsChanged, this, &DecompilerWidget::invalidateTokenTargets);
connect(Core(), &IaitoCore::functionsChanged, this, &DecompilerWidget::invalidateTokenTargets);
connect(Core(), &IaitoCore::functionRenamed, this, &DecompilerWidget::invalidateTokenTargets);
// Esc to seek backward
QAction *seekPrevAction = new QAction(this);
@@ -167,10 +240,23 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
seekPrevAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
addAction(seekPrevAction);
connect(seekPrevAction, &QAction::triggered, seekable, &IaitoSeekable::seekPrev);
QAction *switchAction = new QAction(this);
ShortcutMgr()->bindAction("decompiler.switchToDisassembly", switchAction);
switchAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
addAction(switchAction);
connect(switchAction, &QAction::triggered, this, [this] {
mainWindow->showMemoryWidget(MemoryWidgetType::Disassembly);
});
}
DecompilerWidget::~DecompilerWidget() = default;
QWidget *DecompilerWidget::widgetToFocusOnRaise()
{
return ui->textEdit;
}
QString DecompilerWidget::getWidgetType()
{
return "DecompilerWidget";
@@ -183,10 +269,186 @@ Decompiler *DecompilerWidget::getCurrentDecompiler()
QString DecompilerWidget::deepLinkAt(const QPoint &pos)
{
const QTextCursor cursor = ui->textEdit->cursorForPosition(pos);
const QTextCursor cursor = cursorOnCharacter(pos);
return DeepLink::linkAt(cursor.block().text(), cursor.positionInBlock());
}
QTextCursor DecompilerWidget::cursorOnCharacter(const QPoint &pos) const
{
if (!ui->textEdit->viewport()->rect().contains(pos)) {
return {};
}
QTextCursor cursor = ui->textEdit->cursorForPosition(pos);
const QTextBlock block = cursor.block();
const QTextLine line = block.layout()->lineForTextPosition(cursor.positionInBlock());
if (!line.isValid()) {
return {};
}
const QRect caret = ui->textEdit->cursorRect(cursor);
const qreal x = pos.x() - caret.left() + line.cursorToX(cursor.positionInBlock());
const QRectF bounds = line.naturalTextRect();
if (x < bounds.left() || x >= bounds.right() || pos.y() < caret.top()
|| pos.y() >= caret.top() + caret.height()) {
return {};
}
const int column = line.xToCursor(x, QTextLine::CursorOnCharacter);
const QString text = block.text();
if (column >= text.size() || text.at(column).isSpace()) {
return {};
}
cursor.setPosition(block.position() + column);
return cursor;
}
static QString decompilerSymbolName(QString name)
{
// Plain-text decompilers strip r2 prefixes and turn punctuation into underscores.
static const QRegularExpression prefix(R"(^(?:(?:sym|imp|reloc)[._])+)");
static const QRegularExpression punctuation(R"([^\p{L}\p{N}_])");
name = name.section(QLatin1Char('('), 0, 0).trimmed();
name.remove(prefix);
name.replace(punctuation, QStringLiteral("_"));
while (name.startsWith(QLatin1Char('_'))) {
name.remove(0, 1);
}
return name;
}
void DecompilerWidget::invalidateTokenTargets()
{
symbolTargets.clear();
outgoingXRefsCache.clear();
symbolTargetsReady = false;
refreshDecompilerHover();
}
void DecompilerWidget::cacheSymbolTarget(const QString &name, RVA address)
{
const QString key = decompilerSymbolName(name);
if (key.isEmpty()) {
return;
}
auto it = symbolTargets.find(key);
if (it == symbolTargets.end()) {
symbolTargets.insert(key, address);
} else if (it.value() != address) {
// A shortened name must not silently navigate to the wrong overload/import.
it.value() = RVA_INVALID;
}
}
RVA DecompilerWidget::symbolTarget(const QString &name, RVA offset)
{
if (!symbolTargetsReady) {
auto core = Core()->core();
r_flag_foreach(
core->flags,
[](RFlagItem *flag, void *user) {
auto *widget = static_cast<DecompilerWidget *>(user);
widget->cacheSymbolTarget(QString::fromUtf8(flag->name), flag->addr);
widget->cacheSymbolTarget(QString::fromUtf8(flag->realname), flag->addr);
return true;
},
this);
RListIter *iter;
RAnalFunction *function;
IaitoRListForeach(core->anal->fcns, iter, RAnalFunction, function)
{
cacheSymbolTarget(QString::fromUtf8(function->name), function->addr);
}
symbolTargetsReady = true;
}
const QString key = decompilerSymbolName(name);
const RVA target = symbolTargets.value(key, RVA_INVALID);
if (target != RVA_INVALID || offset == RVA_INVALID) {
return target;
}
auto refs = outgoingXRefsCache.constFind(offset);
if (refs == outgoingXRefsCache.cend()) {
refs = outgoingXRefsCache.insert(offset, Core()->getXRefs(offset, false, false));
}
RVA referenceTarget = RVA_INVALID;
for (const XrefDescription &ref : refs.value()) {
if (decompilerSymbolName(ref.to_str) == key) {
if (referenceTarget != RVA_INVALID && referenceTarget != ref.to) {
return RVA_INVALID;
}
referenceTarget = ref.to;
}
}
return referenceTarget;
}
RVA DecompilerWidget::tokenTarget(QTextCursor &cursor)
{
if (cursor.isNull()) {
return RVA_INVALID;
}
const size_t pos = static_cast<size_t>(cursor.position());
RVA offset = RVA_INVALID;
size_t offsetStart = 0;
#if R2_ABIVERSION >= 40
RCodeMetaItem *annotation;
R_VEC_FOREACH(&code->annotations, annotation)
{
#else
void *iter;
r_vector_foreach(&code->annotations, iter)
{
RCodeMetaItem *annotation = reinterpret_cast<RCodeMetaItem *>(iter);
#endif
if (annotation->start > pos || annotation->end <= pos) {
continue;
}
if (annotation->type == R_CODEMETA_TYPE_OFFSET && annotation->start >= offsetStart) {
offsetStart = annotation->start;
offset = annotation->offset.offset;
}
if (r_codemeta_item_is_reference(annotation)) {
cursor.setPosition(static_cast<int>(annotation->start));
cursor.setPosition(static_cast<int>(annotation->end), QTextCursor::KeepAnchor);
return annotation->reference.offset;
}
if (r_codemeta_item_is_variable(annotation)) {
return RVA_INVALID;
}
}
// Decompilers without reference annotations still expose addresses and symbols.
static const QRegularExpression tokens(
R"(\b(?:0[xX][0-9a-fA-F]+|[\p{L}_$][\p{L}\p{N}_.$]*(?:::[\p{L}_$~][\p{L}\p{N}_.$]*)*)\b)",
QRegularExpression::UseUnicodePropertiesOption);
auto matches = tokens.globalMatch(cursor.block().text());
while (matches.hasNext()) {
const QRegularExpressionMatch match = matches.next();
if (cursor.positionInBlock() < match.capturedStart()
|| cursor.positionInBlock() >= match.capturedEnd()) {
continue;
}
const QString token = match.captured();
const int start = cursor.block().position() + match.capturedStart();
cursor.setPosition(start);
cursor.setPosition(start + match.capturedLength(), QTextCursor::KeepAnchor);
if (token.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
bool ok = false;
const RVA address = token.toULongLong(&ok, 16);
return ok ? address : RVA_INVALID;
}
{
RCoreLocked core = Core()->core();
const QByteArray name = token.toUtf8();
if (RFlagItem *flag = r_flag_get(core->flags, name.constData())) {
return flag->addr;
}
if (RAnalFunction *function = r_anal_get_function_byname(core->anal, name.constData())) {
return function->addr;
}
}
return symbolTarget(token, offset);
}
return RVA_INVALID;
}
ut64 DecompilerWidget::offsetForPosition(size_t pos)
{
ut64 closestPos = UT64_MAX;
@@ -241,6 +503,14 @@ size_t DecompilerWidget::positionForOffset(ut64 offset)
closestPos = annotation->start;
closestOffset = annotation->offset.offset;
}
if (closestPos != UT64_MAX) {
// Some decompilers start offset annotations on the preceding newline.
QTextCursor cursor(ui->textEdit->document());
cursor.setPosition(static_cast<int>(closestPos));
if (cursor.atBlockEnd() && cursor.block().next().isValid()) {
closestPos++;
}
}
return closestPos;
}
@@ -290,8 +560,8 @@ void DecompilerWidget::gatherBreakpointInfo(RCodeMeta &codeDecompiled, size_t st
if (annotation->type != R_CODEMETA_TYPE_OFFSET) {
continue;
}
if ((startPos <= annotation->start && annotation->start < endPos)
|| (startPos < annotation->end && annotation->end < endPos)) {
if (annotation->start < endPos && annotation->end > startPos
&& annotation->offset.offset != RVA_INVALID) {
firstOffset = (annotation->offset.offset < firstOffset) ? annotation->offset.offset
: firstOffset;
}
@@ -311,7 +581,9 @@ void DecompilerWidget::gatherBreakpointInfo(RCodeMeta &codeDecompiled, size_t st
void DecompilerWidget::refreshIfChanged(RVA addr)
{
if (addressInRange(addr)) {
if (addressInRange(addr)
|| (decompiledFunctionAddr != RVA_INVALID
&& Core()->getFunctionStart(addr) == decompiledFunctionAddr)) {
doRefresh();
}
}
@@ -430,7 +702,7 @@ void DecompilerWidget::refreshDecompiler()
QTextCursor DecompilerWidget::getCursorForAddress(RVA addr)
{
size_t pos = positionForOffset(addr);
if (pos == UT64_MAX || pos == 0) {
if (pos == UT64_MAX) {
return QTextCursor();
}
QTextCursor cursor = ui->textEdit->textCursor();
@@ -441,10 +713,20 @@ QTextCursor DecompilerWidget::getCursorForAddress(RVA addr)
void DecompilerWidget::decompilationFinished(RCodeMeta *codeDecompiled)
{
bool isDisplayReset = false;
int cursorLineOffset = 0;
int cursorColumn = 0;
bool restoreCursor = false;
if (previousFunctionAddr == decompiledFunctionAddr) {
scrollerHorizontal = ui->textEdit->horizontalScrollBar()->sliderPosition();
scrollerVertical = ui->textEdit->verticalScrollBar()->sliderPosition();
isDisplayReset = true;
const QTextCursor cursor = ui->textEdit->textCursor();
const QTextCursor addressCursor = getCursorForAddress(seekable->getOffset());
if (!addressCursor.isNull()) {
cursorLineOffset = cursor.blockNumber() - addressCursor.blockNumber();
cursorColumn = cursor.positionInBlock();
restoreCursor = true;
}
}
ui->progressLabel->setVisible(false);
@@ -465,7 +747,24 @@ void DecompilerWidget::decompilationFinished(RCodeMeta *codeDecompiled)
highestOffsetInCode = 0;
return;
}
updateCursorPosition();
if (restoreCursor) {
const QTextCursor addressCursor = getCursorForAddress(seekable->getOffset());
const QTextBlock block = ui->textEdit->document()->findBlockByNumber(
addressCursor.blockNumber() + cursorLineOffset);
if (!addressCursor.isNull() && block.isValid()) {
connectCursorPositionChanged(false);
QTextCursor cursor(block);
cursor.setPosition(block.position() + qMin(cursorColumn, block.length() - 1));
ui->textEdit->setTextCursor(cursor);
updateCursorContext();
updateSelection();
connectCursorPositionChanged(true);
} else {
updateCursorPosition();
}
} else {
updateCursorPosition();
}
highlightPC();
highlightBreakpoints();
lowestOffsetInCode = RVA_MAX;
@@ -913,26 +1212,31 @@ void DecompilerWidget::connectCursorPositionChanged(bool connectPositionChange)
void DecompilerWidget::cursorPositionChanged()
{
const RVA offset = updateCursorContext();
// Do not perform seeks along with the cursor while selecting multiple lines
if (!ui->textEdit->textCursor().selectedText().isEmpty()) {
return;
}
size_t pos = ui->textEdit->textCursor().position();
setAnnotationsAtCursor(pos);
setInfoForBreakpoints();
RVA offset = offsetForPosition(pos);
if (offset != RVA_INVALID && offset != seekable->getOffset()) {
seekFromCursor = true;
seekable->seek(offset);
mCtxMenu->setOffset(offset);
seekFromCursor = false;
}
updateSourceControls();
updateSelection();
}
RVA DecompilerWidget::updateCursorContext()
{
const size_t pos = ui->textEdit->textCursor().position();
setAnnotationsAtCursor(pos);
setInfoForBreakpoints();
const RVA offset = offsetForPosition(pos);
mCtxMenu->setOffset(offset);
return offset;
}
void DecompilerWidget::seekChanged()
{
if (seekFromCursor) {
@@ -958,6 +1262,7 @@ void DecompilerWidget::updateCursorPosition()
QTextCursor cursor = ui->textEdit->textCursor();
cursor.setPosition(pos);
ui->textEdit->setTextCursor(cursor);
updateCursorContext();
updateSelection();
connectCursorPositionChanged(true);
}
@@ -985,7 +1290,7 @@ void DecompilerWidget::updateSelection()
QString scrHighlight = Core()->getConfig("scr.highlight");
extraSelections.append(createHighlightSelections(ui->textEdit, scrHighlight));
ui->textEdit->setExtraSelections(extraSelections);
setDecompilerSelections(extraSelections);
// Highlight PC after updating the selected line
highlightPC();
}
@@ -998,6 +1303,7 @@ QString DecompilerWidget::getWindowTitle() const
void DecompilerWidget::fontsUpdatedSlot()
{
setupFonts();
refreshDecompilerHover();
}
void DecompilerWidget::colorsUpdatedSlot()
@@ -1009,20 +1315,99 @@ void DecompilerWidget::colorsUpdatedSlot()
if (syntaxHighlighter) {
QMetaObject::invokeMethod(syntaxHighlighter, "rehighlight", Qt::QueuedConnection);
}
setDecompilerSelections(ui->textEdit->extraSelections());
}
void DecompilerWidget::showDecompilerContextMenu(const QPoint &pt)
{
mCtxMenu->exec(ui->textEdit->mapToGlobal(pt));
updateCursorContext();
mCtxMenu->exec(ui->textEdit->viewport()->mapToGlobal(pt));
}
void DecompilerWidget::seekToReference()
{
QTextCursor cursor = ui->textEdit->textCursor();
const QString link = DeepLink::linkAt(cursor.block().text(), cursor.positionInBlock());
if (!link.isEmpty()) {
DeepLink::handle(mainWindow, link);
return;
}
const RVA target = tokenTarget(cursor);
if (target != RVA_INVALID) {
seekable->seek(target);
return;
}
size_t pos = ui->textEdit->textCursor().position();
RVA offset = offsetForPosition(pos);
seekable->seekToReference(offset);
}
void DecompilerWidget::setDecompilerSelections(QList<QTextEdit::ExtraSelection> selections)
{
constexpr int hoverProperty = QTextFormat::UserProperty;
selections.erase(
std::remove_if(
selections.begin(),
selections.end(),
[](const QTextEdit::ExtraSelection &s) { return s.format.boolProperty(hoverProperty); }),
selections.end());
if (hoveredToken.hasSelection()) {
QTextEdit::ExtraSelection hover;
hover.cursor = hoveredToken;
hover.format.setBackground(ConfigColor("wordHighlight"));
hover.format.setProperty(hoverProperty, true);
selections.append(hover);
}
ui->textEdit->setExtraSelections(selections);
}
void DecompilerWidget::setHoveredToken(const QTextCursor &cursor)
{
if (hoveredToken == cursor) {
return;
}
hoveredToken = cursor;
setDecompilerSelections(ui->textEdit->extraSelections());
}
void DecompilerWidget::updateDecompilerHover(const QPoint &pos, Qt::MouseButtons buttons)
{
QTextCursor hover;
Qt::CursorShape shape = Qt::ArrowCursor;
if (buttons & Qt::LeftButton) {
shape = Qt::IBeamCursor;
} else {
QTextCursor cursor = cursorOnCharacter(pos);
const QString text = cursor.block().text();
const QString link = DeepLink::linkAt(text, cursor.positionInBlock());
if (!link.isEmpty()) {
const int start = cursor.block().position()
+ text.lastIndexOf(link, cursor.positionInBlock());
cursor.setPosition(start);
cursor.setPosition(start + link.size(), QTextCursor::KeepAnchor);
hover = cursor;
} else if (tokenTarget(cursor) != RVA_INVALID) {
hover = cursor;
}
if (hover.hasSelection()) {
shape = Qt::PointingHandCursor;
}
}
ui->textEdit->viewport()->setCursor(shape);
setHoveredToken(hover);
}
void DecompilerWidget::refreshDecompilerHover()
{
if (ui->textEdit->viewport()->underMouse()) {
updateDecompilerHover(
ui->textEdit->viewport()->mapFromGlobal(QCursor::pos()), QApplication::mouseButtons());
} else {
ui->textEdit->viewport()->setCursor(Qt::ArrowCursor);
setHoveredToken({});
}
}
bool DecompilerWidget::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::Wheel && (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
@@ -1042,6 +1427,23 @@ bool DecompilerWidget::eventFilter(QObject *obj, QEvent *event)
if (event->type() == QEvent::KeyPress
&& (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
static const struct
{
const char *shortcut;
QTextCursor::MoveOperation movement;
} movements[] = {
{"decompiler.cursorLeft", QTextCursor::Left},
{"decompiler.cursorDown", QTextCursor::Down},
{"decompiler.cursorUp", QTextCursor::Up},
{"decompiler.cursorRight", QTextCursor::Right},
};
for (const auto &movement : movements) {
if (ShortcutMgr()->matches(movement.shortcut, keyEvent)) {
ui->textEdit->moveCursor(movement.movement);
keyEvent->accept();
return true;
}
}
if (keyEvent->modifiers() == Qt::NoModifier && keyEvent->key() == Qt::Key_M) {
// Prepare context and compute offset for current decompiler line
size_t pos = ui->textEdit->textCursor().position();
@@ -1068,6 +1470,10 @@ bool DecompilerWidget::eventFilter(QObject *obj, QEvent *event)
if (event->type() == QEvent::MouseButtonDblClick
&& (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
pressedTokenTarget = RVA_INVALID;
if (mouseEvent->button() != Qt::LeftButton || followedTokenOnClick) {
return true;
}
ui->textEdit->setTextCursor(ui->textEdit->cursorForPosition(mouseEvent->pos()));
seekToReference();
return true;
@@ -1075,29 +1481,57 @@ bool DecompilerWidget::eventFilter(QObject *obj, QEvent *event)
if (event->type() == QEvent::MouseButtonPress
&& (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
pressedTokenTarget = RVA_INVALID;
followedTokenOnClick = false;
if (mouseEvent->button() == Qt::LeftButton && mouseEvent->modifiers() == Qt::NoModifier) {
tokenPressPosition = mouseEvent->pos();
QTextCursor cursor = cursorOnCharacter(tokenPressPosition);
pressedTokenTarget = tokenTarget(cursor);
}
updateDecompilerHover(mouseEvent->pos(), mouseEvent->buttons());
if (mouseEvent->button() == Qt::RightButton && !ui->textEdit->textCursor().hasSelection()) {
ui->textEdit->setTextCursor(ui->textEdit->cursorForPosition(mouseEvent->pos()));
size_t pos = ui->textEdit->textCursor().position();
this->currentOffset = offsetForPosition(pos);
updateCursorContext();
return true;
}
}
if (event->type() == QEvent::MouseButtonRelease
&& (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
const RVA target = pressedTokenTarget;
pressedTokenTarget = RVA_INVALID;
updateDecompilerHover(mouseEvent->pos(), mouseEvent->buttons());
if (mouseEvent->button() == Qt::LeftButton && !ui->textEdit->textCursor().hasSelection()) {
const QString link = deepLinkAt(mouseEvent->pos());
if (!link.isEmpty()) {
DeepLink::handle(mainWindow, link);
return true;
}
QTextCursor cursor = cursorOnCharacter(mouseEvent->pos());
if (target != RVA_INVALID
&& (mouseEvent->pos() - tokenPressPosition).manhattanLength()
< QApplication::startDragDistance()
&& target == tokenTarget(cursor)) {
followedTokenOnClick = true;
seekable->seek(target);
refreshDecompilerHover();
return true;
}
}
}
if (event->type() == QEvent::MouseMove
&& (obj == ui->textEdit || obj == ui->textEdit->viewport())) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
const bool overLink = !deepLinkAt(mouseEvent->pos()).isEmpty();
ui->textEdit->viewport()->setCursor(overLink ? Qt::PointingHandCursor : Qt::IBeamCursor);
if ((mouseEvent->buttons() & Qt::LeftButton)
&& (mouseEvent->pos() - tokenPressPosition).manhattanLength()
>= QApplication::startDragDistance()) {
pressedTokenTarget = RVA_INVALID;
}
updateDecompilerHover(mouseEvent->pos(), mouseEvent->buttons());
}
if (event->type() == QEvent::Leave && obj == ui->textEdit->viewport()) {
ui->textEdit->viewport()->setCursor(Qt::ArrowCursor);
setHoveredToken({});
}
return MemoryDockWidget::eventFilter(obj, event);
}
@@ -1139,7 +1573,7 @@ bool DecompilerWidget::colorLine(QTextEdit::ExtraSelection extraSelection)
{
QList<QTextEdit::ExtraSelection> extraSelections = ui->textEdit->extraSelections();
extraSelections.append(extraSelection);
ui->textEdit->setExtraSelections(extraSelections);
setDecompilerSelections(extraSelections);
return true;
}
@@ -1270,7 +1704,7 @@ static RCodeMeta *copyCodeMeta(RCodeMeta *source)
return copy;
}
struct DecompilerAddressLine
struct DecompilerColumnLine
{
size_t start = 0;
size_t end = 0;
@@ -1279,11 +1713,12 @@ struct DecompilerAddressLine
size_t shiftBefore = 0;
size_t shiftInLine = 0;
RVA address = RVA_INVALID;
QString disassembly = {};
};
static QVector<DecompilerAddressLine> collectAddressLines(const QString &text)
static QVector<DecompilerColumnLine> collectAddressLines(const QString &text)
{
QVector<DecompilerAddressLine> lines;
QVector<DecompilerColumnLine> lines;
int start = 0;
while (start < text.size()) {
int newline = text.indexOf(QLatin1Char('\n'), start);
@@ -1296,7 +1731,7 @@ static QVector<DecompilerAddressLine> collectAddressLines(const QString &text)
return lines;
}
static bool assignLineAddresses(QVector<DecompilerAddressLine> &lines, RCodeMeta *code)
static bool assignLineAddresses(QVector<DecompilerColumnLine> &lines, RCodeMeta *code)
{
bool hasAddress = false;
#if R2_ABIVERSION >= 40
@@ -1312,8 +1747,8 @@ static bool assignLineAddresses(QVector<DecompilerAddressLine> &lines, RCodeMeta
if (annotation->type != R_CODEMETA_TYPE_OFFSET || annotation->offset.offset == RVA_INVALID) {
continue;
}
for (DecompilerAddressLine &line : lines) {
if (annotation->start >= line.nextStart || annotation->end <= line.start) {
for (DecompilerColumnLine &line : lines) {
if (annotation->start >= line.end || annotation->end <= line.start) {
continue;
}
if (annotation->start < line.offsetAnnotationStart) {
@@ -1321,15 +1756,14 @@ static bool assignLineAddresses(QVector<DecompilerAddressLine> &lines, RCodeMeta
line.offsetAnnotationStart = annotation->start;
hasAddress = true;
}
break;
}
}
return hasAddress;
}
static size_t shiftForStartPosition(const QVector<DecompilerAddressLine> &lines, size_t pos)
static size_t shiftForStartPosition(const QVector<DecompilerColumnLine> &lines, size_t pos)
{
for (const DecompilerAddressLine &line : lines) {
for (const DecompilerColumnLine &line : lines) {
if (pos < line.nextStart) {
return line.shiftInLine;
}
@@ -1337,9 +1771,9 @@ static size_t shiftForStartPosition(const QVector<DecompilerAddressLine> &lines,
return lines.isEmpty() ? 0 : lines.last().shiftInLine;
}
static size_t shiftForEndPosition(const QVector<DecompilerAddressLine> &lines, size_t pos)
static size_t shiftForEndPosition(const QVector<DecompilerColumnLine> &lines, size_t pos)
{
for (const DecompilerAddressLine &line : lines) {
for (const DecompilerColumnLine &line : lines) {
if (pos == line.start) {
return line.shiftBefore;
}
@@ -1350,33 +1784,62 @@ static size_t shiftForEndPosition(const QVector<DecompilerAddressLine> &lines, s
return lines.isEmpty() ? 0 : lines.last().shiftInLine;
}
static RCodeMeta *copyCodeMetaWithAddressColumn(RCodeMeta *source)
static RCodeMeta *copyCodeMetaWithColumns(
RCodeMeta *source,
bool showAddresses,
bool showDisassembly,
QHash<RVA, QString> &disassemblyCache)
{
RCodeMeta *display = copyCodeMeta(source);
QString text(display->code);
QVector<DecompilerAddressLine> lines = collectAddressLines(text);
QVector<DecompilerColumnLine> lines = collectAddressLines(text);
if (!assignLineAddresses(lines, display)) {
return display;
}
int addressWidth = RAddressString(0).size();
for (const DecompilerAddressLine &line : lines) {
int disassemblyWidth = 0;
for (DecompilerColumnLine &line : lines) {
if (line.address != RVA_INVALID) {
addressWidth
= std::max(addressWidth, static_cast<int>(RAddressString(line.address).size()));
if (showDisassembly) {
auto instruction = disassemblyCache.constFind(line.address);
if (instruction == disassemblyCache.constEnd()) {
const QString assembly
= Core()
->cmdRawAt(
"pi 1 @e:scr.color=0,scr.html=false,asm.pseudo=false,"
"asm.esil=false,asm.addr=false",
line.address)
.simplified();
instruction = disassemblyCache.insert(line.address, assembly);
}
line.disassembly = instruction.value();
disassemblyWidth
= std::max(disassemblyWidth, static_cast<int>(line.disassembly.size()));
}
}
}
QString rendered;
const int prefixWidth = addressWidth + 2;
const int prefixWidth = (showAddresses ? addressWidth + 2 : 0)
+ (showDisassembly ? disassemblyWidth + 3 : 0);
rendered.reserve(text.size() + lines.size() * prefixWidth);
size_t inserted = 0;
for (DecompilerAddressLine &line : lines) {
QString prefix
= line.address == RVA_INVALID
? QString(addressWidth, QLatin1Char(' '))
: RAddressString(line.address).rightJustified(addressWidth, QLatin1Char(' '));
prefix.append(QStringLiteral(" "));
for (DecompilerColumnLine &line : lines) {
QString prefix;
if (showAddresses) {
prefix
= line.address == RVA_INVALID
? QString(addressWidth, QLatin1Char(' '))
: RAddressString(line.address).rightJustified(addressWidth, QLatin1Char(' '));
prefix.append(QStringLiteral(" "));
}
if (showDisassembly) {
prefix.append(line.disassembly.leftJustified(disassemblyWidth, QLatin1Char(' ')));
prefix.append(QStringLiteral(" | "));
}
line.shiftBefore = inserted;
line.shiftInLine = inserted + static_cast<size_t>(prefix.size());
inserted += static_cast<size_t>(prefix.size());
@@ -1400,8 +1863,10 @@ static RCodeMeta *copyCodeMetaWithAddressColumn(RCodeMeta *source)
RCodeMetaItem *annotation = reinterpret_cast<RCodeMetaItem *>(iter);
#endif
if (annotation->type == R_CODEMETA_TYPE_OFFSET) {
for (const DecompilerAddressLine &line : lines) {
if (annotation->start >= line.nextStart || annotation->end <= line.start) {
for (const DecompilerColumnLine &line : lines) {
// pdc ranges can start on the preceding newline. Only extend
// the range to the prefix of a line with annotated text.
if (annotation->start >= line.end || annotation->end <= line.start) {
continue;
}
annotation->start = line.start + line.shiftBefore;
@@ -1422,19 +1887,28 @@ static RCodeMeta *copyCodeMetaWithAddressColumn(RCodeMeta *source)
void DecompilerWidget::setCode(RCodeMeta *newCode)
{
symbolTargets.clear();
outgoingXRefsCache.clear();
symbolTargetsReady = false;
sourceCode.reset(newCode);
updateDisplayedCode();
disassemblyCache.clear();
updateDisplayedCode(previousFunctionAddr == decompiledFunctionAddr);
}
void DecompilerWidget::updateDisplayedCode()
void DecompilerWidget::updateDisplayedCode(bool preserveFolds)
{
hoveredToken = {};
pressedTokenTarget = RVA_INVALID;
connectCursorPositionChanged(false);
mCtxMenu->setAnnotationHere(nullptr);
if (syntaxHighlighter) {
syntaxHighlighter->setDocument(nullptr);
}
this->code.reset(
showAddressColumn ? copyCodeMetaWithAddressColumn(sourceCode.get())
: copyCodeMeta(sourceCode.get()));
showAddressColumn || showDisassemblyColumn
? copyCodeMetaWithColumns(
sourceCode.get(), showAddressColumn, showDisassemblyColumn, disassemblyCache)
: copyCodeMeta(sourceCode.get()));
QString text = remapAnnotationOffsetsToQString(*this->code);
setHighlighter(hasCodeMetaSyntaxHighlights(this->code.get()));
@@ -1443,7 +1917,8 @@ void DecompilerWidget::updateDisplayedCode()
// issues), temporarily detach the highlighter, update the document, then
// reattach and schedule a queued rehighlight. This minimizes the risk of
// crashes in the regex engine during synchronous document updates.
this->ui->textEdit->setPlainText(text);
ui->textEdit->setDecompiledCode(text, QString::fromUtf8(sourceCode->code), preserveFolds);
updateCursorContext();
connectCursorPositionChanged(true);
if (syntaxHighlighter) {
@@ -1452,6 +1927,7 @@ void DecompilerWidget::updateDisplayedCode()
// event has been processed to avoid re-entrancy.
QMetaObject::invokeMethod(syntaxHighlighter, "rehighlight", Qt::QueuedConnection);
}
refreshDecompilerHover();
}
void DecompilerWidget::setAddressColumnVisible(bool visible)
@@ -1461,11 +1937,26 @@ void DecompilerWidget::setAddressColumnVisible(bool visible)
}
showAddressColumn = visible;
Config()->setDecompilerShowAddresses(visible);
refreshDisplayColumns();
}
void DecompilerWidget::setDisassemblyColumnVisible(bool visible)
{
if (showDisassemblyColumn == visible) {
return;
}
showDisassemblyColumn = visible;
Config()->setDecompilerShowDisassembly(visible);
refreshDisplayColumns();
}
void DecompilerWidget::refreshDisplayColumns()
{
int horizontalPosition = ui->textEdit->horizontalScrollBar()->sliderPosition();
int verticalPosition = ui->textEdit->verticalScrollBar()->sliderPosition();
updateDisplayedCode();
updateCursorPosition();
setAnnotationsAtCursor(ui->textEdit->textCursor().position());
setInfoForBreakpoints();
highlightPC();
highlightBreakpoints();
ui->textEdit->horizontalScrollBar()->setSliderPosition(horizontalPosition);
+27 -3
View File
@@ -2,6 +2,7 @@
#define DECOMPILERWIDGET_H
#include <memory>
#include <QHash>
#include <QPointer>
#include <QSyntaxHighlighter>
#include <QTextEdit>
@@ -37,6 +38,8 @@ public slots:
void highlightPC();
void setAddressColumnVisible(bool visible);
bool isAddressColumnVisible() const { return showAddressColumn; }
void setDisassemblyColumnVisible(bool visible);
bool isDisassemblyColumnVisible() const { return showDisassemblyColumn; }
private slots:
/**
* @brief Copy to clipboard what's needed depending on the state of text
@@ -90,6 +93,15 @@ private:
QPointer<QSyntaxHighlighter> syntaxHighlighter;
bool decompilerSelectionEnabled;
bool showAddressColumn = false;
bool showDisassemblyColumn = false;
QHash<RVA, QString> disassemblyCache;
QHash<QString, RVA> symbolTargets;
QHash<RVA, QList<XrefDescription>> outgoingXRefsCache;
bool symbolTargetsReady = false;
QTextCursor hoveredToken;
QPoint tokenPressPosition;
RVA pressedTokenTarget = RVA_INVALID;
bool followedTokenOnClick = false;
ut64 currentOffset = UT64_MAX;
/**
@@ -114,12 +126,12 @@ private:
* Specifies the lowest offset of instructions among all the instructions in
* the decompiled function.
*/
RVA lowestOffsetInCode;
RVA lowestOffsetInCode = RVA_MAX;
/**
* Specifies the highest offset of instructions among all the instructions
* in the decompiled function.
*/
RVA highestOffsetInCode;
RVA highestOffsetInCode = 0;
/**
* @brief Gets the current decompiler selected by the user.
@@ -129,6 +141,7 @@ private:
Decompiler *getCurrentDecompiler();
void refreshOptionsCombo();
void refreshDisplayColumns();
/**
* @brief Calls the function doRefresh() if the address specified is a part
@@ -164,6 +177,15 @@ private:
* - Program Counter(PC) while debugging
*/
void updateSelection();
void setDecompilerSelections(QList<QTextEdit::ExtraSelection> selections);
void setHoveredToken(const QTextCursor &cursor);
void updateDecompilerHover(const QPoint &pos, Qt::MouseButtons buttons);
void refreshDecompilerHover();
QTextCursor cursorOnCharacter(const QPoint &pos) const;
RVA tokenTarget(QTextCursor &cursor);
void invalidateTokenTargets();
void cacheSymbolTarget(const QString &name, RVA address);
RVA symbolTarget(const QString &name, RVA offset);
/**
* @brief Connect/Disconnect SIGNAL-SLOT connection that deals with changes
* in cursor position.
@@ -175,6 +197,7 @@ private:
* @param connectPositionChange
*/
void connectCursorPositionChanged(bool connectPositionChange);
RVA updateCursorContext();
/**
* @brief Find the current global offset in sync and update cursor
* to the position specified by this offset (found using positionForOffset()
@@ -183,6 +206,7 @@ private:
void updateCursorPosition();
QString getWindowTitle() const override;
QWidget *widgetToFocusOnRaise() override;
/**
* @brief Event filter that intercept the following events:
@@ -290,7 +314,7 @@ private:
bool addressInRange(RVA addr);
void setCode(RCodeMeta *code);
void updateDisplayedCode();
void updateDisplayedCode(bool preserveFolds = true);
void setHighlighter(bool codeMetaHighlighter);
bool isSourceDecompilerSelected() const;
+8 -1
View File
@@ -31,7 +31,7 @@
<number>0</number>
</property>
<item>
<widget class="QPlainTextEdit" name="textEdit">
<widget class="DecompilerTextEdit" name="textEdit">
<property name="lineWrapMode">
<enum>QPlainTextEdit::NoWrap</enum>
</property>
@@ -150,6 +150,13 @@
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>DecompilerTextEdit</class>
<extends>QPlainTextEdit</extends>
<header>widgets/DecompilerTextEdit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
+222 -9
View File
@@ -11,6 +11,7 @@
#include "common/TempConfig.h"
#include "core/Iaito.h"
#include "core/MainWindow.h"
#include "widgets/ExceptionBar.h"
#include <QAction>
#include <QActionGroup>
@@ -117,6 +118,11 @@ DisassemblerGraphView::DisassemblerGraphView(
{
highlight_token = nullptr;
auto *layout = new QVBoxLayout(this);
exceptionBar = new ExceptionBar(this);
layout->addWidget(exceptionBar);
connect(exceptionBar, &ExceptionBar::seekRequested, this, [this](quint64 address) {
this->seekable->seek(address);
});
// Signals that require a refresh all
connect(Core(), &IaitoCore::refreshAll, this, &DisassemblerGraphView::refreshView);
connect(Core(), &IaitoCore::commentsChanged, this, &DisassemblerGraphView::refreshView);
@@ -153,6 +159,13 @@ DisassemblerGraphView::DisassemblerGraphView(
contextMenu->addAction(&actionExportGraph);
contextMenu->addMenu(layoutMenu);
addBasicBlockContentMenu();
showExceptionsAction = contextMenu->addAction(tr("Show exception regions"));
showExceptionsAction->setCheckable(true);
showExceptionsAction->setChecked(true);
showExceptionsAction->setToolTip(tr(
"Colored bubbles mark protected blocks. Dashed arrows show possible exception transfers."));
connect(showExceptionsAction, &QAction::toggled, this, &DisassemblerGraphView::refreshView);
exceptionMenu = blockMenu->addMenu(tr("Exception handlers"));
if (mainWindow) {
if (QAction *overviewAction = mainWindow->getOverviewAction()) {
contextMenu->addAction(overviewAction);
@@ -338,6 +351,12 @@ void DisassemblerGraphView::loadCurrentGraph()
disassembly_blocks.clear();
blocks.clear();
outgoingXRefsCache.clear();
exceptionEdges.clear();
exceptionShapes.clear();
exceptionRegions = fcn ? Core()->getExceptionRegions(currentFcnAddr) : QList<ExceptionRegion>();
exceptionBar->setRegions(exceptionRegions, seekable->getOffset());
setViewportMargins(0, exceptionRegions.isEmpty() ? 0 : exceptionBar->sizeHint().height(), 0, 0);
showExceptionsAction->setVisible(!exceptionRegions.isEmpty());
if (highlight_token) {
delete highlight_token;
@@ -426,6 +445,9 @@ void DisassemblerGraphView::loadCurrentGraph()
addBlock(gb);
}
if (showExceptionsAction->isChecked()) {
addExceptionBlocks();
}
cleanupEdges(blocks);
if (!func["blocks"].toArray().isEmpty()) {
@@ -433,6 +455,146 @@ void DisassemblerGraphView::loadCurrentGraph()
}
}
void DisassemblerGraphView::addExceptionBlocks()
{
for (const auto &region : exceptionRegions) {
if (!blockForAddress(region.handler)) {
// The handler may be in another function or not analyzed yet. Show
// a navigation node without creating or changing analysis blocks.
DisassemblyBlock db;
db.entry = region.handler;
db.size = 1;
db.externalHandler = true;
appendBlockContentLine(db, tr("Handler outside this function"), region.handler, 1);
appendBlockContentLine(db, tr("Double-click to open"), region.handler, 1);
disassembly_blocks[db.entry] = db;
GraphBlock block;
block.entry = db.entry;
addBlock(block);
}
}
for (auto &item : disassembly_blocks) {
auto &db = item.second;
QStringList tags;
for (const auto &region : exceptionRegions) {
if (!db.externalHandler && region.protects(db.entry, db.size)) {
tags << tr("TRY #%1").arg(region.index);
const auto *handler = blockForAddress(region.handler);
auto &edges = blocks.at(db.entry).edges;
if (handler && handler->entry != db.entry
&& std::none_of(edges.begin(), edges.end(), [handler](const GraphEdge &edge) {
return edge.target == handler->entry;
})) {
edges.emplace_back(handler->entry);
exceptionEdges[{db.entry, handler->entry}] = region.color();
}
}
if (region.handler >= db.entry && region.handler - db.entry < db.size) {
tags << tr("HANDLER #%1").arg(region.index);
}
}
if (!tags.isEmpty()) {
db.header_text = Text(
QStringLiteral("[%1] %2")
.arg(RAddressString(db.entry), tags.join(QStringLiteral(" · "))),
ConfigColor(ADDR),
Qt::transparent);
}
prepareGraphNode(blocks.at(db.entry));
}
}
void DisassemblerGraphView::graphLayoutChanged()
{
exceptionShapes.clear();
if (exceptionRegions.isEmpty() || !showExceptionsAction->isChecked()) {
return;
}
// Keep the bubbles inside image export and scrollable graph bounds.
for (auto &item : blocks) {
item.second.x += 12;
item.second.y += 12;
for (auto &edge : item.second.edges) {
edge.polyline.translate(12, 12);
}
}
width += 24;
height += 24;
QSet<QPair<quint64, quint64>> seen;
for (const auto &region : exceptionRegions) {
const auto range = qMakePair(region.from, region.to);
if (seen.contains(range)) {
continue;
}
seen.insert(range);
QRectF bounds;
for (const auto &item : blocks) {
const auto &block = item.second;
const auto &db = disassembly_blocks.at(block.entry);
if (db.externalHandler || !region.protects(db.entry, db.size)) {
continue;
}
bounds = bounds.united(QRectF(block.x, block.y, block.width, block.height));
}
if (bounds.isEmpty()) {
continue;
}
// One envelope covers every protected block, including disconnected
// blocks and the space between them. Membership comes from the entire
// half-open metadata range, not just its boundary instructions.
QPainterPath shape;
shape.addRoundedRect(bounds.adjusted(-8, -8, 8, 8), 12, 12);
for (const auto &item : blocks) {
const auto &block = item.second;
const auto &db = disassembly_blocks.at(block.entry);
if (db.externalHandler || !region.protects(db.entry, db.size)) {
QPainterPath excluded;
excluded.addRect(
QRectF(block.x - 2, block.y - 2, block.width + 4, block.height + 4));
shape = shape.subtracted(excluded);
}
}
exceptionShapes.append({region, shape});
}
// Keep inner regions readable over their enclosing regions.
std::stable_sort(
exceptionShapes.begin(),
exceptionShapes.end(),
[](const ExceptionShape &a, const ExceptionShape &b) {
const QRectF ar = a.path.boundingRect();
const QRectF br = b.path.boundingRect();
return ar.width() * ar.height() > br.width() * br.height();
});
}
void DisassemblerGraphView::drawBackground(QPainter &p, bool interactive)
{
p.setRenderHint(QPainter::Antialiasing);
for (const auto &shape : exceptionShapes) {
const bool active = interactive && shape.region.contains(seekable->getOffset());
QColor fill = shape.region.color();
fill.setAlpha(active ? 48 : 22);
p.setBrush(fill);
QPen outline(shape.region.color(), active ? 2 : 1);
outline.setStyle(Qt::DashLine);
p.setPen(outline);
p.drawPath(shape.path);
}
}
QString DisassemblerGraphView::exceptionTooltip(RVA address, RVA size) const
{
QStringList details;
for (const auto &region : exceptionRegions) {
if (region.touches(address, size)) {
details << region.description();
}
}
return details.join(QStringLiteral("\n\n"))
.toHtmlEscaped()
.replace(QLatin1Char('\n'), QStringLiteral("<br>"));
}
DisassemblerGraphView::EdgeConfigurationMapping DisassemblerGraphView::getEdgeConfigurations()
{
EdgeConfigurationMapping result;
@@ -478,8 +640,7 @@ DisassemblerGraphView::MinimapBars DisassemblerGraphView::getMinimapBars()
}
}
};
const bool hasTitleBar = Config()->getGraphBlockEntryOffset()
&& !db.header_text.lines.empty();
const bool hasTitleBar = !db.header_text.lines.empty();
if (hasTitleBar) {
y += int(db.header_text.lines.size()) * charHeight;
} else {
@@ -682,7 +843,7 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block,
free(s);
}
const bool hasTitleBar = Config()->getGraphBlockEntryOffset() && !db.header_text.lines.empty();
const bool hasTitleBar = !db.header_text.lines.empty();
// Draw basic block body with the default background.
p.drawRect(blockRect);
@@ -733,7 +894,7 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block,
const int luma = (titleBg.red() * 299 + titleBg.green() * 587 + titleBg.blue() * 114)
/ 1000;
const QColor titleTextColor = (luma > 128) ? QColor(Qt::black) : QColor(Qt::white);
const QString titleText = "[" + RAddressString(db.entry) + "]";
const QString titleText = db.header_text.ToQString();
QFont titleFont = Config()->getFont();
titleFont.setBold(true);
p.setPen(titleTextColor);
@@ -907,7 +1068,11 @@ GraphView::EdgeConfiguration DisassemblerGraphView::edgeConfiguration(
{
EdgeConfiguration ec;
DisassemblyBlock &db = disassembly_blocks[from.entry];
if (to->entry == db.true_path) {
const auto exception = exceptionEdges.find({from.entry, to->entry});
if (exception != exceptionEdges.end()) {
ec.color = exception->second;
ec.lineStyle = Qt::DashLine;
} else if (to->entry == db.true_path) {
ec.color = brtrueColor;
} else if (to->entry == db.false_path) {
ec.color = brfalseColor;
@@ -1144,6 +1309,7 @@ void DisassemblerGraphView::applyAddressRangeSelection(RVA start, RVA end)
void DisassemblerGraphView::onSeekChanged(RVA addr)
{
exceptionBar->setAddress(addr);
blockMenu->setOffset(addr);
DisassemblyBlock *db = blockForAddress(addr);
bool switchFunction = false;
@@ -1172,8 +1338,13 @@ void DisassemblerGraphView::takeTrue()
if (db->true_path != RVA_INVALID) {
seekable->seek(db->true_path);
} else if (!blocks[db->entry].edges.empty()) {
seekable->seek(blocks[db->entry].edges[0].target);
} else {
for (const auto &edge : blocks[db->entry].edges) {
if (!exceptionEdges.count({db->entry, edge.target})) {
seekable->seek(edge.target);
break;
}
}
}
}
@@ -1186,8 +1357,13 @@ void DisassemblerGraphView::takeFalse()
if (db->false_path != RVA_INVALID) {
seekable->seek(db->false_path);
} else if (!blocks[db->entry].edges.empty()) {
seekable->seek(blocks[db->entry].edges[0].target);
} else {
for (const auto &edge : blocks[db->entry].edges) {
if (!exceptionEdges.count({db->entry, edge.target})) {
seekable->seek(edge.target);
break;
}
}
}
}
@@ -1538,6 +1714,7 @@ void DisassemblerGraphView::blockContextMenuRequested(
? offset
: static_cast<RVA>(block.entry);
blockMenu->setOffset(menuOffset);
exceptionBar->populateMenu(exceptionMenu, block.entry, disassembly_blocks.at(block.entry).size);
actionUnhighlight.setVisible(Core()->getBBHighlighter()->getBasicBlock(block.entry));
actionUnhighlightInstruction.setVisible(
instrForAddress(menuOffset) && Core()->getBIHighlighter()->getBasicInstruction(menuOffset));
@@ -1574,6 +1751,11 @@ void DisassemblerGraphView::blockDoubleClicked(
GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos)
{
Q_UNUSED(event);
if (disassembly_blocks.at(block.entry).externalHandler) {
seekable->seek(block.entry);
refreshView();
return;
}
seekable->seekToReference(getAddrForMouseEvent(block, &pos));
}
@@ -1581,8 +1763,15 @@ void DisassemblerGraphView::blockHelpEvent(
GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos)
{
Instr *instr = getInstrForMouseEvent(block, &pos);
const auto &db = disassembly_blocks.at(block.entry);
const QString exceptions = exceptionTooltip(
instr ? instr->addr : db.entry, instr ? qMax<ut64>(instr->size, 1) : db.size);
if (!instr || instr->fullText.lines.empty()) {
if (!instr) {
if (!exceptions.isEmpty()) {
QToolTip::showText(event->globalPos(), exceptions, viewport());
return;
}
QToolTip::hideText();
event->ignore();
return;
@@ -1604,6 +1793,10 @@ void DisassemblerGraphView::blockHelpEvent(
}
}
if (!exceptions.isEmpty()) {
QToolTip::showText(event->globalPos(), exceptions, viewport());
return;
}
if (instr->fullText.lines.empty()) {
QToolTip::hideText();
event->ignore();
@@ -1616,6 +1809,26 @@ void DisassemblerGraphView::blockHelpEvent(
bool DisassemblerGraphView::helpEvent(QHelpEvent *event)
{
if (!GraphView::helpEvent(event)) {
const QPoint position = viewToLogicalCoordinates(event->pos());
QStringList details;
for (const auto &shape : exceptionShapes) {
if (shape.path.contains(position)) {
for (const auto &region : exceptionRegions) {
if (region.from == shape.region.from && region.to == shape.region.to) {
details << region.description();
}
}
}
}
if (!details.isEmpty()) {
QToolTip::showText(
event->globalPos(),
details.join(QStringLiteral("\n\n"))
.toHtmlEscaped()
.replace(QLatin1Char('\n'), QStringLiteral("<br>")),
viewport());
return true;
}
QToolTip::hideText();
event->ignore();
}
+18
View File
@@ -6,6 +6,7 @@
#include <QHash>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
#include <QShortcut>
#include <QStringList>
#include <QWidget>
@@ -18,6 +19,7 @@
class QTextEdit;
class FallbackSyntaxHighlighter;
class QJsonArray;
class ExceptionBar;
class DisassemblerGraphView : public IaitoGraphView
{
@@ -90,6 +92,7 @@ class DisassemblerGraphView : public IaitoGraphView
ut64 false_path = 0;
bool terminal = false;
bool indirectcall = false;
bool externalHandler = false;
};
enum class BasicBlockContent {
@@ -159,6 +162,8 @@ public slots:
void applyAddressRangeSelection(RVA start, RVA end);
protected:
void drawBackground(QPainter &p, bool interactive) override;
void graphLayoutChanged() override;
void paintEvent(QPaintEvent *event) override;
void blockContextMenuRequested(
GraphView::GraphBlock &block, QContextMenuEvent *event, QPoint pos) override;
@@ -188,6 +193,19 @@ private:
QMenu *contextMenu;
BasicBlockContent basicBlockContent = BasicBlockContent::Disassembly;
QHash<RVA, QList<XrefDescription>> outgoingXRefsCache;
QList<ExceptionRegion> exceptionRegions;
struct ExceptionShape
{
ExceptionRegion region;
QPainterPath path;
};
QList<ExceptionShape> exceptionShapes;
std::map<std::pair<ut64, ut64>, QColor> exceptionEdges;
ExceptionBar *exceptionBar;
QMenu *exceptionMenu;
QAction *showExceptionsAction;
void addExceptionBlocks();
QString exceptionTooltip(RVA address, RVA size) const;
void connectSeekChanged(bool disconnect);
+301 -96
View File
@@ -10,8 +10,10 @@
#include "common/TempConfig.h"
#include "core/MainWindow.h"
#include "dialogs/ShortcutKeysDialog.h"
#include "menus/ContextMenuIcons.h"
#include "menus/DisassemblyContextMenu.h"
#include "widgets/ExceptionBar.h"
#include <algorithm>
#include <cmath>
#include <limits>
@@ -29,6 +31,7 @@
#include <QSplitter>
#include <QTextBlock>
#include <QTextBlockUserData>
#include <QTextLayout>
#include <QToolTip>
#include <QVBoxLayout>
#include <QWheelEvent>
@@ -67,22 +70,29 @@ static int getMaxVisibleDisassemblyLines(QPlainTextEdit *textEdit)
return qMax(1, int(std::ceil(availableHeight / lineHeight)));
}
static QString tokenAtCursor(QTextCursor cursor)
static QTextCursor tokenRangeAtCursor(QTextCursor cursor)
{
const int clickedCharPos = cursor.positionInBlock();
cursor.select(QTextCursor::BlockUnderCursor);
const QString line = cursor.selectedText().replace("\xc2\xa0", " ");
const QString line = cursor.block().text().replace("\xc2\xa0", " ");
static const QRegularExpression tokenRegExp(R"(\b(?<!\.)([^\s]+)\b(?!\.))");
QRegularExpressionMatchIterator i = tokenRegExp.globalMatch(line);
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
if (match.capturedStart() <= clickedCharPos && match.capturedEnd() > clickedCharPos) {
return match.captured();
const int start = cursor.block().position() + match.capturedStart();
cursor.setPosition(start);
cursor.setPosition(start + match.capturedLength(), QTextCursor::KeepAnchor);
return cursor;
}
}
return QString();
return {};
}
static QString tokenAtCursor(const QTextCursor &cursor)
{
return tokenRangeAtCursor(cursor).selectedText();
}
static QString wordAtCursor(QTextCursor cursor)
@@ -176,7 +186,17 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
auto *container = new QWidget;
container->setMinimumHeight(0);
container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored);
auto *containerLayout = new QHBoxLayout(container);
auto *outerLayout = new QVBoxLayout(container);
outerLayout->setContentsMargins(0, 0, 0, 0);
outerLayout->setSpacing(0);
exceptionBar = new ExceptionBar(container);
outerLayout->addWidget(exceptionBar);
connect(exceptionBar, &ExceptionBar::seekRequested, this, [this](quint64 address) {
seekable->seek(address);
});
exceptionMenu = mCtxMenu->addMenu(tr("Exception handlers"));
auto *containerLayout = new QHBoxLayout;
outerLayout->addLayout(containerLayout, 1);
containerLayout->setContentsMargins(0, 0, 0, 0);
containerLayout->setSpacing(0);
containerLayout->addWidget(splitter);
@@ -221,6 +241,11 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
// Event filter to intercept double clicks in the textbox
// and showing tooltips when hovering above those offsets
mDisasTextEdit->viewport()->installEventFilter(this);
connect(
mDisasTextEdit->horizontalScrollBar(),
&QScrollBar::valueChanged,
this,
&DisassemblyWidget::refreshDisassemblyHover);
// Set Disas context menu
mDisasTextEdit->setContextMenuPolicy(Qt::CustomContextMenu);
@@ -283,6 +308,25 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
this,
&DisassemblyWidget::on_refreshContents);
connect(
mCtxMenu,
&DisassemblyContextMenu::flagAboutToChange,
this,
&DisassemblyWidget::updateContextMenuState);
mCtxMenu->registerFlagShortcut();
auto *followAction = new QAction(tr("Follow reference"), this);
ShortcutMgr()->bindAction("disasm.jumpToOffset", followAction);
followAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
followAction->setAutoRepeat(false);
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
followAction->setShortcutVisibleInContextMenu(true);
#endif
ContextMenuIcons::setIcon(
this, followAction, ContextMenuIcons::Icon::Navigation, QColor(103, 80, 164));
connect(followAction, &QAction::triggered, this, [this]() {
jumpToOffsetUnderCursor(mDisasTextEdit->textCursor());
});
mCtxMenu->insertAction(mCtxMenu->actions().first(), followAction);
addActions(mCtxMenu->actions());
#define ADD_ACTION(id, ctx, slot) \
@@ -298,8 +342,18 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
mainWindow->showMemoryWidget(MemoryWidgetType::Graph);
})
ADD_ACTION("disasm.switchToDecompiler", Qt::WidgetWithChildrenShortcut, [this] {
mainWindow->showMemoryWidget(MemoryWidgetType::Decompiler);
})
ADD_ACTION("disasm.seekPrev", Qt::WidgetWithChildrenShortcut, &DisassemblyWidget::seekPrev)
ADD_ACTION("disasm.cursorLeft", Qt::WidgetWithChildrenShortcut, [this]() {
mDisasTextEdit->moveCursor(QTextCursor::Left);
})
ADD_ACTION("disasm.cursorRight", Qt::WidgetWithChildrenShortcut, [this]() {
mDisasTextEdit->moveCursor(QTextCursor::Right);
})
ADD_ACTION("disasm.cursorDown", Qt::WidgetWithChildrenShortcut, [this]() {
moveCursorRelative(false, false);
})
@@ -317,13 +371,15 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
void DisassemblyWidget::setPreviewMode(bool previewMode)
{
setSeekHistoryBarEnabled(!previewMode);
mDisasTextEdit->setContextMenuPolicy(previewMode ? Qt::NoContextMenu : Qt::CustomContextMenu);
mCtxMenu->setEnabled(!previewMode);
for (auto action : mCtxMenu->actions()) {
action->setEnabled(!previewMode);
}
for (auto action : actions()) {
if (action->shortcut() == Qt::Key_Space || action->shortcut() == Qt::Key_Escape) {
if (action->shortcut() == Qt::Key_Space || action->shortcut() == Qt::Key_Tab
|| action->shortcut() == Qt::Key_Escape) {
action->setEnabled(!previewMode);
}
}
@@ -498,6 +554,7 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
return;
}
setHoveredToken({});
topOffset = Core()->alignInstructionAddress(topOffset);
if (maxLines <= 0) {
@@ -511,6 +568,9 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
breakpoints = Core()->getBreakpointsAddresses();
// Retrieve disassembly lines
if (!cached) {
exceptionRegions = Core()->getExceptionRegions();
}
outgoingXRefsCache.clear();
const QList<DisassemblyLine> oldLines = lines;
if (cached) {
@@ -566,6 +626,7 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
}
}
updateExceptionBar(!cached);
if (!cached || !updateDocumentIncrementally(oldLines, newVisible)) {
rebuildDocument(newVisible);
}
@@ -573,6 +634,24 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
addressScrollBar->setViewport(maxLines, bottomOffset > topOffset ? bottomOffset - topOffset : 1);
addressScrollBar->setAddress(topOffset);
addressScrollBar->setSeekAddress(seekable->getOffset());
refreshDisassemblyHover();
}
void DisassemblyWidget::updateExceptionBar(bool force)
{
const RVA address = seekable->getOffset();
const RVA function = Core()->getFunctionStart(address);
if (force || exceptionFunction != function) {
exceptionFunction = function;
QList<ExceptionRegion> regions;
if (function != RVA_INVALID && !exceptionRegions.isEmpty()) {
const auto blocks = Core()->cmdj("afbj @ " + RAddressString(function)).array();
regions = ExceptionRegion::forBlocks(exceptionRegions, blocks);
}
exceptionBar->setRegions(regions, address);
} else {
exceptionBar->setAddress(address);
}
}
void DisassemblyWidget::decorateBlock(
@@ -580,6 +659,14 @@ void DisassemblyWidget::decorateBlock(
{
QTextCursor cursor(block);
QTextBlockFormat f;
for (const auto &region : exceptionRegions) {
if (region.contains(line.offset) || region.handler == line.offset) {
QColor color = region.color();
color.setAlpha(region.handler == line.offset ? 65 : 26);
f.setBackground(color);
break;
}
}
if (Core()->isBreakpoint(breakpoints, line.offset)) {
f.setBackground(ConfigColor("gui.breakpoint_background"));
} else {
@@ -644,7 +731,7 @@ bool DisassemblyWidget::documentMatchesLines(const QList<DisassemblyLine> &ref,
return true;
}
// Scrolls by editing only the rows that changed and blitting the rest of the view
// Scrolls by editing only the rows that changed, retaining the other text layouts.
bool DisassemblyWidget::updateDocumentIncrementally(
const QList<DisassemblyLine> &oldLines, int newVisible)
{
@@ -682,19 +769,15 @@ bool DisassemblyWidget::updateDocumentIncrementally(
return false;
}
QAbstractTextDocumentLayout *layout = doc->documentLayout();
const bool layoutSignalsBlocked = layout->blockSignals(true);
connectCursorPositionChanged(true);
mDisasTextEdit->setLockScroll(true);
mDisasTextEdit->setUpdatesEnabled(false);
QTextCursor cursor(doc);
const QTextBlockFormat regular = cursor.blockFormat();
BasicBlockColor bbColor;
qreal delta = 0;
cursor.beginEditBlock();
if (shift > 0) {
delta = mDisasTextEdit->blockTop(doc->findBlockByNumber(shift))
- mDisasTextEdit->blockTop(doc->firstBlock());
cursor.movePosition(QTextCursor::Start);
cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, shift);
cursor.removeSelectedText();
@@ -735,29 +818,18 @@ bool DisassemblyWidget::updateDocumentIncrementally(
doc->findBlockByNumber(i).clearLayout();
}
doc->lastBlock().clearLayout();
if (shift < 0) {
delta = -(
mDisasTextEdit->blockTop(doc->findBlockByNumber(-shift))
- mDisasTextEdit->blockTop(doc->firstBlock()));
}
layout->blockSignals(layoutSignalsBlocked);
mDisasTextEdit->setLockScroll(false);
connectCursorPositionChanged(false);
// Editing the document also invalidates layouts, selections and dirty regions.
// Repaint from the updated document instead of moving stale viewport pixels.
mDisasTextEdit->setUpdatesEnabled(true);
visibleLines = newVisible;
if (!documentMatchesLines(lines, newVisible)) {
return false; // the full rebuild repairs whatever went wrong
}
// Blit everything below the document margin so only the new rows get painted
QWidget *viewport = mDisasTextEdit->viewport();
const int margin = qMax(0, qCeil(mDisasTextEdit->textOffset()));
if (qAbs(delta - qRound(delta)) > 0.01) {
viewport->update();
} else {
viewport->scroll(
0, -qRound(delta), QRect(0, margin, viewport->width(), viewport->height() - margin));
}
updateCursorPosition();
if (readCurrentDisassemblyOffset() == seekable->getOffset()) {
// The cursor did not move, extend the highlights to the new rows
@@ -987,28 +1059,13 @@ void DisassemblyWidget::clearBasicBlockColorCache()
void DisassemblyWidget::highlightCurrentLine()
{
updateExceptionBar();
QList<QTextEdit::ExtraSelection> extraSelections;
QColor highlightColor = ConfigColor("lineHighlight");
// Highlight the current word
QTextCursor cursor = mDisasTextEdit->textCursor();
auto clickedCharPos = cursor.positionInBlock();
// Select the line (BlockUnderCursor matches a line with current
// implementation)
cursor.select(QTextCursor::BlockUnderCursor);
// Remove any non-breakable space from the current line
QString searchString = cursor.selectedText().replace("\xc2\xa0", " ");
// Cut the line in "tokens" that can be highlighted
static const QRegularExpression tokenRegExp(R"(\b(?<!\.)([^\s]+)\b(?!\.))");
QRegularExpressionMatchIterator i = tokenRegExp.globalMatch(searchString);
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
// Current token is under our cursor, select this one
if (match.capturedStart() <= clickedCharPos && match.capturedEnd() > clickedCharPos) {
curHighlightedWord = match.captured();
break;
}
}
curHighlightedWord = tokenAtCursor(cursor);
// Highlight the current line
QTextEdit::ExtraSelection highlightSelection;
@@ -1039,7 +1096,7 @@ void DisassemblyWidget::highlightCurrentLine()
QString scrHighlight = Core()->getConfig("scr.highlight");
extraSelections.append(createHighlightSelections(mDisasTextEdit, scrHighlight));
mDisasTextEdit->setExtraSelections(extraSelections);
setDisassemblySelections(extraSelections);
}
void DisassemblyWidget::highlightPCLine()
@@ -1076,7 +1133,7 @@ void DisassemblyWidget::highlightPCLine()
QList<QTextEdit::ExtraSelection> currentSelections = mDisasTextEdit->extraSelections();
currentSelections.append(pcSelections);
mDisasTextEdit->setExtraSelections(currentSelections);
setDisassemblySelections(currentSelections);
}
void DisassemblyWidget::showDisasContextMenu(const QPoint &pt)
@@ -1087,12 +1144,6 @@ void DisassemblyWidget::showDisasContextMenu(const QPoint &pt)
}
updateContextMenuState();
RVA selectionStart = RVA_INVALID;
int selectionSize = 0;
if (getSelectedInstructionRange(&selectionStart, &selectionSize)) {
mCtxMenu->setSelectionRange(selectionStart, selectionSize);
}
mCtxMenu->exec(mDisasTextEdit->mapToGlobal(pt));
}
@@ -1259,8 +1310,7 @@ void DisassemblyWidget::updateCursorPosition()
if (parked.position() != 0 || parked.hasSelection()) {
mDisasTextEdit->moveCursor(QTextCursor::Start);
}
mDisasTextEdit->setExtraSelections(
createSameWordsSelections(mDisasTextEdit, curHighlightedWord));
setDisassemblySelections(createSameWordsSelections(mDisasTextEdit, curHighlightedWord));
} else {
RVA currentCursorOffset = readCurrentDisassemblyOffset();
QTextCursor originalCursor = mDisasTextEdit->textCursor();
@@ -1285,7 +1335,7 @@ void DisassemblyWidget::updateCursorPosition()
break;
} else if (lineOffset != RVA_INVALID && lineOffset > offset) {
mDisasTextEdit->moveCursor(QTextCursor::Start);
mDisasTextEdit->setExtraSelections({});
setDisassemblySelections({});
break;
}
@@ -1354,11 +1404,19 @@ void DisassemblyWidget::cursorPositionChanged()
void DisassemblyWidget::updateContextMenuState()
{
const RVA offset = readCurrentDisassemblyOffset();
if (offset != RVA_INVALID) {
mCtxMenu->setOffset(offset);
}
exceptionBar->populateMenu(exceptionMenu, offset);
QTextCursor cursor = mDisasTextEdit->textCursor();
bool hasSelection = cursor.hasSelection();
mCtxMenu->setCanCopy(hasSelection);
mCtxMenu->setSelectionRange(RVA_INVALID, 0);
RVA selectionStart = RVA_INVALID;
int selectionSize = 0;
getSelectedInstructionRange(&selectionStart, &selectionSize);
mCtxMenu->setSelectionRange(selectionStart, selectionSize);
if (hasSelection) {
// A word is selected so use it
@@ -1449,8 +1507,18 @@ void DisassemblyWidget::copyBytes()
void DisassemblyWidget::jumpToOffsetUnderCursor(const QTextCursor &cursor)
{
RVA offset = readDisassemblyOffset(cursor);
seekable->seekToReference(offset);
const QString link = DeepLink::linkAt(cursor.block().text(), cursor.positionInBlock());
if (!link.isEmpty()) {
DeepLink::handle(mainWindow, link);
return;
}
const RVA target = tokenTarget(cursor);
const RVA offset = readDisassemblyOffset(cursor);
if (target != RVA_INVALID && target != offset) {
seekable->seek(target);
} else {
seekable->seekToReference(offset);
}
}
QList<XrefDescription> DisassemblyWidget::getOutgoingXRefs(RVA offset)
@@ -1490,36 +1558,109 @@ RVA DisassemblyWidget::xrefTargetForToken(RVA offset, const QString &token)
return RVA_INVALID;
}
bool DisassemblyWidget::isActionableTokenAt(const QPoint &pos)
RVA DisassemblyWidget::tokenTarget(const QTextCursor &cursor)
{
QTextCursor cursor = mDisasTextEdit->cursorForPosition(pos);
const RVA offset = readDisassemblyOffset(cursor);
if (offset == RVA_INVALID) {
return false;
return RVA_INVALID;
}
const QString token = tokenAtCursor(cursor);
if (token.trimmed().isEmpty()) {
return false;
return RVA_INVALID;
}
return xrefTargetForToken(offset, token) != RVA_INVALID;
if (token.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
bool isAddress = false;
const RVA address = token.toULongLong(&isAddress, 16);
if (isAddress) {
return address;
}
}
{
RCoreLocked core = Core()->core();
const QByteArray name = token.toUtf8();
if (RFlagItem *flag = r_flag_get(core->flags, name.constData())) {
return flag->addr;
}
if (RAnalFunction *function = r_anal_get_function_byname(core->anal, name.constData())) {
return function->addr;
}
}
return xrefTargetForToken(offset, token);
}
QString DisassemblyWidget::deepLinkAt(const QPoint &pos)
{
const QTextCursor cursor = mDisasTextEdit->cursorForPosition(pos);
const QTextCursor cursor = mDisasTextEdit->cursorOnCharacter(pos);
return DeepLink::linkAt(cursor.block().text(), cursor.positionInBlock());
}
void DisassemblyWidget::updateDisassemblyCursor(const QPoint &pos, Qt::MouseButtons buttons)
void DisassemblyWidget::setDisassemblySelections(QList<QTextEdit::ExtraSelection> selections)
{
constexpr int hoverProperty = QTextFormat::UserProperty;
selections.erase(
std::remove_if(
selections.begin(),
selections.end(),
[](const QTextEdit::ExtraSelection &s) { return s.format.boolProperty(hoverProperty); }),
selections.end());
if (hoveredToken.hasSelection()) {
QTextEdit::ExtraSelection hover;
hover.cursor = hoveredToken;
hover.format.setBackground(ConfigColor("wordHighlight"));
hover.format.setProperty(hoverProperty, true);
selections.append(hover);
}
mDisasTextEdit->setExtraSelections(selections);
}
void DisassemblyWidget::setHoveredToken(const QTextCursor &cursor)
{
if (hoveredToken == cursor) {
return;
}
hoveredToken = cursor;
setDisassemblySelections(mDisasTextEdit->extraSelections());
}
void DisassemblyWidget::updateDisassemblyHover(const QPoint &pos, Qt::MouseButtons buttons)
{
QTextCursor hover;
Qt::CursorShape shape = Qt::ArrowCursor;
if (buttons & Qt::LeftButton) {
mDisasTextEdit->viewport()->setCursor(Qt::IBeamCursor);
} else if (isActionableTokenAt(pos) || !deepLinkAt(pos).isEmpty()) {
mDisasTextEdit->viewport()->setCursor(Qt::PointingHandCursor);
shape = Qt::IBeamCursor;
} else {
QTextCursor cursor = mDisasTextEdit->cursorOnCharacter(pos);
const QString text = cursor.block().text();
const QString link = DeepLink::linkAt(text, cursor.positionInBlock());
if (!link.isEmpty()) {
const int start = cursor.block().position()
+ text.lastIndexOf(link, cursor.positionInBlock());
cursor.setPosition(start);
cursor.setPosition(start + link.size(), QTextCursor::KeepAnchor);
hover = cursor;
} else if (tokenTarget(cursor) != RVA_INVALID) {
hover = tokenRangeAtCursor(cursor);
}
if (hover.hasSelection()) {
shape = Qt::PointingHandCursor;
}
}
mDisasTextEdit->viewport()->setCursor(shape);
setHoveredToken(hover);
}
void DisassemblyWidget::refreshDisassemblyHover()
{
if (mDisasTextEdit->viewport()->underMouse()) {
updateDisassemblyHover(
mDisasTextEdit->viewport()->mapFromGlobal(QCursor::pos()), QApplication::mouseButtons());
} else {
mDisasTextEdit->viewport()->setCursor(Qt::ArrowCursor);
setHoveredToken({});
}
}
@@ -1527,40 +1668,68 @@ bool DisassemblyWidget::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::Resize && obj == mDisasTextEdit->viewport()) {
QMetaObject::invokeMethod(this, [this]() { updateMaxLines(); }, Qt::QueuedConnection);
} else if (event->type() == QEvent::MouseButtonPress && obj == mDisasTextEdit->viewport()) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
pressedTokenTarget = RVA_INVALID;
followedTokenOnClick = false;
if (mouseEvent->button() == Qt::LeftButton && mouseEvent->modifiers() == Qt::NoModifier) {
tokenPressPosition = mouseEvent->pos();
pressedTokenTarget = tokenTarget(mDisasTextEdit->cursorOnCharacter(tokenPressPosition));
}
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
} else if (event->type() == QEvent::MouseMove && obj == mDisasTextEdit->viewport()) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
updateDisassemblyCursor(mouseEvent->pos(), mouseEvent->buttons());
if ((mouseEvent->buttons() & Qt::LeftButton)
&& (mouseEvent->pos() - tokenPressPosition).manhattanLength()
>= QApplication::startDragDistance()) {
pressedTokenTarget = RVA_INVALID;
}
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
} else if (event->type() == QEvent::MouseButtonRelease && obj == mDisasTextEdit->viewport()) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
const RVA target = pressedTokenTarget;
pressedTokenTarget = RVA_INVALID;
if (mouseEvent->button() == Qt::LeftButton && !mDisasTextEdit->textCursor().hasSelection()) {
const QString link = deepLinkAt(mouseEvent->pos());
if (!link.isEmpty()) {
DeepLink::handle(mainWindow, link);
return true;
}
if (target != RVA_INVALID
&& (mouseEvent->pos() - tokenPressPosition).manhattanLength()
< QApplication::startDragDistance()
&& target == tokenTarget(mDisasTextEdit->cursorOnCharacter(mouseEvent->pos()))) {
followedTokenOnClick = true;
seekable->seek(target);
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
return true;
}
}
updateDisassemblyCursor(mouseEvent->pos(), mouseEvent->buttons());
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
} else if (event->type() == QEvent::Leave && obj == mDisasTextEdit->viewport()) {
mDisasTextEdit->viewport()->setCursor(Qt::ArrowCursor);
setHoveredToken({});
} else if (
event->type() == QEvent::MouseButtonDblClick
&& (obj == mDisasTextEdit || obj == mDisasTextEdit->viewport())) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
pressedTokenTarget = RVA_INVALID;
if (mouseEvent->button() != Qt::LeftButton || followedTokenOnClick) {
return true;
}
const QTextCursor &cursor = mDisasTextEdit->cursorForPosition(
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
mouseEvent->position().toPoint()
#else
QPoint(mouseEvent->x(), mouseEvent->y())
#endif
);
const QTextCursor cursor = mDisasTextEdit->cursorOnCharacter(mouseEvent->pos());
jumpToOffsetUnderCursor(cursor);
return true;
} else if (event->type() == QEvent::ToolTip && obj == mDisasTextEdit->viewport()) {
QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
QTextCursor cursorForWord = mDisasTextEdit->cursorForPosition(helpEvent->pos());
QTextCursor cursorForWord = mDisasTextEdit->cursorOnCharacter(helpEvent->pos());
if (cursorForWord.isNull()) {
QToolTip::hideText();
return true;
}
const QString registerTooltip = PreviewTooltip::buildRegisterPreview(
wordAtCursor(cursorForWord));
if (!registerTooltip.isEmpty()) {
@@ -1568,9 +1737,8 @@ bool DisassemblyWidget::eventFilter(QObject *obj, QEvent *event)
return true;
}
const QString token = tokenAtCursor(cursorForWord);
RVA offsetFrom = readDisassemblyOffset(cursorForWord);
RVA offsetTo = xrefTargetForToken(offsetFrom, token);
RVA offsetTo = tokenTarget(cursorForWord);
// Only if the offset we point *to* is different from the one the
// cursor is currently on *and* the former is a valid offset, we are
@@ -1582,6 +1750,22 @@ bool DisassemblyWidget::eventFilter(QObject *obj, QEvent *event)
}
}
if (offsetTo == RVA_INVALID || offsetTo == offsetFrom) {
QStringList details;
for (const auto &region : exceptionRegions) {
if (region.contains(offsetFrom) || region.handler == offsetFrom) {
details << region.description();
}
}
if (!details.isEmpty()) {
QToolTip::showText(
helpEvent->globalPos(),
details.join(QStringLiteral("\n\n"))
.toHtmlEscaped()
.replace(QLatin1Char('\n'), QStringLiteral("<br>")),
this);
}
}
return true;
}
@@ -1603,11 +1787,6 @@ void DisassemblyWidget::keyPressEvent(QKeyEvent *event)
}
return;
}
if (ShortcutMgr()->matches("disasm.jumpToOffset", event)) {
const QTextCursor cursor = mDisasTextEdit->textCursor();
jumpToOffsetUnderCursor(cursor);
}
MemoryDockWidget::keyPressEvent(event);
}
@@ -1654,6 +1833,7 @@ void DisassemblyWidget::colorsUpdatedSlot()
{
PreviewTooltip::applyStyleSheet(this);
setupColors();
setDisassemblySelections(mDisasTextEdit->extraSelections());
// Skip the expensive refreshDisasm() (which re-runs disassembleLines)
// when only the interface palette changed; the cached line HTML still
// carries valid radare2 colors, so a repaint is enough.
@@ -1682,9 +1862,36 @@ void DisassemblyWidget::setupColors()
.arg(ConfigColor("btext").name()));
}
qreal DisassemblyTextEdit::blockTop(const QTextBlock &block) const
// Mouse hits must land on text, rather than snap to a nearby caret position.
QTextCursor DisassemblyTextEdit::cursorOnCharacter(const QPoint &pos) const
{
return blockBoundingGeometry(block).top();
if (!viewport()->rect().contains(pos)) {
return {};
}
const QTextBlock block = cursorForPosition(pos).block();
if (!block.isValid()) {
return {};
}
const QPointF localPos = QPointF(pos)
- blockBoundingGeometry(block).translated(contentOffset()).topLeft();
const QTextLayout *layout = block.layout();
for (int i = 0; i < layout->lineCount(); ++i) {
const QTextLine line = layout->lineAt(i);
const QRectF rect = line.naturalTextRect();
if (localPos.x() < rect.left() || localPos.x() >= rect.right() || localPos.y() < rect.top()
|| localPos.y() >= rect.bottom()) {
continue;
}
const int column = line.xToCursor(localPos.x(), QTextLine::CursorOnCharacter);
const QString text = block.text();
if (column >= text.size() || text.at(column).isSpace()) {
return {};
}
QTextCursor cursor(block);
cursor.setPosition(block.position() + column);
return cursor;
}
return {};
}
// Viewport rectangles of every block, in document order
@@ -1698,11 +1905,6 @@ QList<QRectF> DisassemblyTextEdit::blockRects() const
return rects;
}
qreal DisassemblyTextEdit::textOffset() const
{
return (blockBoundingGeometry(document()->begin()).topLeft() + contentOffset()).y();
}
bool DisassemblyTextEdit::viewportEvent(QEvent *event)
{
switch (event->type()) {
@@ -1735,6 +1937,9 @@ void DisassemblyTextEdit::scrollContentsBy(int dx, int dy)
{
if (!lockScroll) {
QPlainTextEdit::scrollContentsBy(dx, dy);
// Qt scrolls existing pixels, including the function lines fixed to the viewport.
// Repaint the whole viewport to erase their shifted copies.
viewport()->update();
}
}
@@ -1755,7 +1960,7 @@ void DisassemblyTextEdit::mousePressEvent(QMouseEvent *event)
void DisassemblyTextEdit::paintEvent(QPaintEvent *event)
{
// The viewport is opaque so scrolling can blit it, paint its background
// The viewport is opaque, so every damaged area needs its background painted.
QPainter(viewport()).fillRect(event->rect(), backgroundColor);
QPlainTextEdit::paintEvent(event);
@@ -1794,7 +1999,7 @@ void DisassemblyTextEdit::paintEvent(QPaintEvent *event)
return;
}
// Endpoints only depend on visible rows so blitted lines match a fresh paint
// Derive endpoints from the visible rows.
const auto &functionRanges = disas->getFunctionRanges();
for (auto range = functionRanges.cbegin(); range != functionRanges.cend(); ++range) {
auto firstLine = rowSpan.lowerBound(range.key());
@@ -1813,7 +2018,7 @@ void DisassemblyTextEdit::paintEvent(QPaintEvent *event)
void DisassemblyWidget::seekPrev()
{
Core()->seekPrev();
seekable->seekPrev();
}
/*********************
+16 -4
View File
@@ -22,6 +22,7 @@ class DisassemblyContextMenu;
class DisassemblyLeftPanel;
class QWheelEvent;
class AddressScrollBar;
class ExceptionBar;
class DisassemblyWidget : public MemoryDockWidget
{
@@ -93,6 +94,7 @@ private:
qreal wheelRemainder;
QString curHighlightedWord;
QTextCursor hoveredToken;
struct BasicBlockColor
{
@@ -103,6 +105,11 @@ private:
QList<BasicBlockColor> basicBlockColorCache;
QHash<RVA, QList<XrefDescription>> outgoingXRefsCache;
QList<ExceptionRegion> exceptionRegions;
RVA exceptionFunction = RVA_INVALID;
ExceptionBar *exceptionBar;
QMenu *exceptionMenu;
void updateExceptionBar(bool force = false);
/**
* offset of lines below the first line of the current seek
@@ -110,6 +117,9 @@ private:
int cursorLineOffset;
int cursorCharOffset;
bool seekFromCursor;
QPoint tokenPressPosition;
RVA pressedTokenTarget = RVA_INVALID;
bool followedTokenOnClick = false;
bool applyingAddressRangeSelection = false;
bool publishingAddressRangeSelection = false;
@@ -148,8 +158,11 @@ private:
void jumpToOffsetUnderCursor(const QTextCursor &);
QList<XrefDescription> getOutgoingXRefs(RVA offset);
RVA xrefTargetForToken(RVA offset, const QString &token);
bool isActionableTokenAt(const QPoint &pos);
void updateDisassemblyCursor(const QPoint &pos, Qt::MouseButtons buttons);
RVA tokenTarget(const QTextCursor &cursor);
void setDisassemblySelections(QList<QTextEdit::ExtraSelection> selections);
void setHoveredToken(const QTextCursor &cursor);
void updateDisassemblyHover(const QPoint &pos, Qt::MouseButtons buttons);
void refreshDisassemblyHover();
QString deepLinkAt(const QPoint &pos);
};
@@ -167,8 +180,7 @@ public:
void setLockScroll(bool lock) { this->lockScroll = lock; }
void setBackgroundColor(const QColor &color) { backgroundColor = color; }
qreal textOffset() const;
qreal blockTop(const QTextBlock &block) const;
QTextCursor cursorOnCharacter(const QPoint &pos) const;
QList<QRectF> blockRects() const;
public:
+78
View File
@@ -0,0 +1,78 @@
#include "ExceptionBar.h"
#include <QHBoxLayout>
#include <QMenu>
#include <QToolButton>
ExceptionBar::ExceptionBar(QWidget *parent)
: QWidget(parent)
{
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
auto *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
navigator = new QToolButton(this);
navigator->setAutoRaise(true);
navigator->setPopupMode(QToolButton::InstantPopup);
navigator->setMenu(new QMenu(navigator));
navigator->setToolTip(tr("Navigate protected ranges and exception handlers (read-only)"));
layout->addWidget(navigator);
layout->addStretch();
hide();
}
void ExceptionBar::addRegion(QMenu *menu, const ExceptionRegion &region)
{
auto *entry = menu->addMenu(QStringLiteral("#%1 · %2").arg(region.index).arg(region.label()));
entry->setToolTipsVisible(true);
entry->menuAction()->setToolTip(region.description());
const auto add = [this, entry](const QString &title, quint64 address) {
auto *action = entry->addAction(QStringLiteral("%1 · 0x%2").arg(title).arg(address, 0, 16));
connect(action, &QAction::triggered, this, [this, address]() {
emit seekRequested(address);
});
};
add(tr("Go to handler"), region.handler);
add(tr("Go to try start"), region.from);
add(tr("Go to try end (exclusive)"), region.to);
// In PE metadata filter values 0 and 1 are constants, not code addresses.
if (region.filter > 1 && region.filter != ExceptionRegion::Invalid) {
add(tr("Go to filter"), region.filter);
}
}
void ExceptionBar::setRegions(const QList<ExceptionRegion> &value, quint64 address)
{
regions = value;
navigator->menu()->clear();
for (const auto &region : regions) {
addRegion(navigator->menu(), region);
}
navigator->setText(tr("Exceptions (%1)").arg(regions.size()));
setVisible(!regions.isEmpty());
setAddress(address);
}
void ExceptionBar::setAddress(quint64 address)
{
QStringList details;
details << tr("Navigate this function's protected ranges and exception handlers (read-only)");
for (const auto &region : regions) {
if (region.contains(address) || region.handler == address) {
details << region.description();
}
}
navigator->setToolTip(details.join(QStringLiteral("\n\n"))
.toHtmlEscaped()
.replace(QLatin1Char('\n'), QStringLiteral("<br>")));
}
void ExceptionBar::populateMenu(QMenu *menu, quint64 address, quint64 size)
{
menu->clear();
for (const auto &region : regions) {
if (region.touches(address, size)) {
addRegion(menu, region);
}
}
menu->menuAction()->setVisible(!menu->isEmpty());
}
+29
View File
@@ -0,0 +1,29 @@
#ifndef EXCEPTIONBAR_H
#define EXCEPTIONBAR_H
#include "common/ExceptionInfo.h"
#include <QWidget>
class QToolButton;
class QMenu;
// Shared, read-only exception navigator for code views.
class ExceptionBar : public QWidget
{
Q_OBJECT
public:
explicit ExceptionBar(QWidget *parent = nullptr);
void setRegions(const QList<ExceptionRegion> &regions, quint64 address);
void setAddress(quint64 address);
void populateMenu(QMenu *menu, quint64 address, quint64 size = 1);
signals:
void seekRequested(quint64 address);
private:
QList<ExceptionRegion> regions;
QToolButton *navigator;
void addRegion(QMenu *menu, const ExceptionRegion &region);
};
#endif
+4 -1
View File
@@ -95,6 +95,9 @@ ExportsProxyModel::ExportsProxyModel(ExportsModel *source_model, QObject *parent
bool ExportsProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
if (!filterAcceptsType(row, parent)) {
return false;
}
QModelIndex index = sourceModel()->index(row, 0, parent);
auto exp = index.data(ExportsModel::ExportDescriptionRole).value<ExportDescription>();
return exp.name.contains(FILTER_REGEX);
@@ -138,7 +141,7 @@ ExportsWidget::ExportsWidget(MainWindow *main)
exportsModel = new ExportsModel(&exports, this);
exportsProxyModel = new ExportsProxyModel(exportsModel, this);
setModels(exportsProxyModel);
setModels(exportsProxyModel, ExportsModel::NameColumn, ExportsModel::TypeColumn);
ui->treeView->sortByColumn(ExportsModel::OffsetColumn, Qt::AscendingOrder);
QShortcut *toggle_shortcut = ShortcutMgr()->registerShortcut("widget.toggleExports", main);
+385 -60
View File
@@ -1,12 +1,19 @@
#include "FilesystemWidget.h"
#include "common/Helpers.h"
#include "common/SourceLineReference.h"
#include "core/Iaito.h"
#include "core/MainWindow.h"
#include <algorithm>
#include <QApplication>
#include <QContextMenuEvent>
#include <QFontDatabase>
#include <QGridLayout>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
#include <QSet>
#include <QSignalBlocker>
#include <QStyle>
FilesystemTreeModel::FilesystemTreeModel(QObject *parent)
: QStandardItemModel(parent)
@@ -22,65 +29,90 @@ void FilesystemTreeModel::setRootPath(const QString &path)
void FilesystemTreeModel::refresh()
{
clear();
setHorizontalHeaderLabels(QStringList() << tr("Name") << tr("Size") << tr("Type"));
removeRows(0, rowCount());
if (!rootPath.isEmpty()) {
populateDirectory(invisibleRootItem(), rootPath);
}
}
QJsonDocument FilesystemTreeModel::parseMdCommand(const QString &path)
QList<FileInfo> FilesystemTreeModel::readDirectory(const QString &path)
{
QString sanitizedPath = IaitoCore::sanitizeStringForCommand(path);
QString output = Core()->cmdRaw(QString("mdj %1").arg(sanitizedPath));
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8(), &error);
if (error.error != QJsonParseError::NoError) {
qWarning() << "Failed to parse mdj output:" << error.errorString();
return QJsonDocument();
// Use the API so source paths, including spaces and command metacharacters, stay intact.
QList<FileInfo> entries;
RCoreLocked core = Core()->core();
RListIter *iter;
if (path == QStringLiteral("/")) {
RFSRoot *root;
IaitoRListForeach(core->fs->roots, iter, RFSRoot, root)
{
const QString mountPath = QString::fromUtf8(root->path);
if (mountPath != QStringLiteral("/")) {
entries.append({mountPath, QStringLiteral("directory"), 0});
}
}
}
return doc;
const QByteArray pathBytes = path.toUtf8();
RList *files = r_fs_dir(core->fs, pathBytes.constData());
RFSFile *file;
IaitoRListForeach(files, iter, RFSFile, file)
{
const QString name = QString::fromUtf8(file->name);
if (name.isEmpty() || name == QStringLiteral(".") || name == QStringLiteral("..")) {
continue;
}
entries.append(
{name,
file->type == 'd' || file->type == 'm' ? QStringLiteral("directory")
: QStringLiteral("file"),
file->size});
}
r_list_free(files);
std::sort(entries.begin(), entries.end(), [](const FileInfo &a, const FileInfo &b) {
if (a.type != b.type) {
return a.type == QStringLiteral("directory");
}
return QString::localeAwareCompare(a.name, b.name) < 0;
});
return entries;
}
void FilesystemTreeModel::populateDirectory(QStandardItem *parentItem, const QString &path)
{
QJsonDocument doc = parseMdCommand(path);
if (!doc.isArray()) {
return;
}
QJsonArray array = doc.array();
for (const QJsonValue &value : array) {
if (!value.isObject())
continue;
QJsonObject obj = value.toObject();
QString name = obj["name"].toString();
QString type = obj["type"].toString();
quint64 size = obj["size"].toVariant().toULongLong();
for (const auto &entry : readDirectory(path)) {
const QString &name = entry.name;
const QString &type = entry.type;
QList<QStandardItem *> items;
QStandardItem *nameItem = new QStandardItem(name);
nameItem->setData(path + "/" + name, Qt::UserRole); // Store full path
const QString fullPath = name.startsWith(QLatin1Char('/'))
? name
: QDir::cleanPath(path + QLatin1Char('/') + name);
nameItem->setData(fullPath, PathRole);
nameItem->setToolTip(fullPath);
items << nameItem;
items << new QStandardItem(QString::number(size));
items << new QStandardItem(QString::number(entry.size));
items << new QStandardItem(type);
if (type == "directory") {
nameItem->setIcon(qApp->style()->standardIcon(QStyle::SP_DirIcon));
nameItem->setData(false, LoadedRole);
// Add a dummy child to make it expandable
nameItem->appendRow(new QStandardItem("Loading..."));
auto *placeholder = new QStandardItem(tr("Loading..."));
placeholder->setEnabled(false);
nameItem->appendRow(placeholder);
} else {
nameItem->setIcon(qApp->style()->standardIcon(QStyle::SP_FileIcon));
}
for (auto *item : items) {
item->setEditable(false);
}
parentItem->appendRow(items);
}
}
FilesystemWidget::FilesystemWidget(MainWindow *main)
: IaitoDockWidget(main)
, mainWindow(main)
{
setWindowTitle(tr("Filesystem"));
setObjectName("FilesystemWidget");
@@ -90,7 +122,12 @@ FilesystemWidget::FilesystemWidget(MainWindow *main)
setupFilesystemTree();
setupActions();
refreshMountpoints();
refreshDeferrer = createRefreshDeferrer([this]() { refresh(); });
connect(Core(), &IaitoCore::refreshAll, this, &FilesystemWidget::refresh);
connect(Core(), &IaitoCore::functionsChanged, this, &FilesystemWidget::refresh);
connect(Core(), &IaitoCore::functionRenamed, this, &FilesystemWidget::refresh);
connect(Core(), &IaitoCore::codeRebased, this, &FilesystemWidget::refresh);
refresh();
}
FilesystemWidget::~FilesystemWidget() {}
@@ -107,9 +144,10 @@ void FilesystemWidget::setupUI()
QVBoxLayout *mountLayout = new QVBoxLayout(mountpointsGroup);
mountpointsList = new QListWidget(this);
mountpointsList->setMaximumHeight(80);
mountLayout->addWidget(mountpointsList);
QHBoxLayout *mountControlsLayout = new QHBoxLayout();
auto *mountControlsLayout = new QGridLayout();
fsTypeCombo = new QComboBox(this);
mountPathEdit = new QLineEdit(this);
mountPathEdit->setPlaceholderText(tr("Mount path (e.g., /mnt)"));
@@ -120,12 +158,12 @@ void FilesystemWidget::setupUI()
mountButton = new QPushButton(tr("Mount"), this);
umountButton = new QPushButton(tr("Umount"), this);
mountControlsLayout->addWidget(fsTypeCombo);
mountControlsLayout->addWidget(mountPathEdit);
mountControlsLayout->addWidget(offsetEdit);
mountControlsLayout->addWidget(optionsEdit);
mountControlsLayout->addWidget(mountButton);
mountControlsLayout->addWidget(umountButton);
mountControlsLayout->addWidget(fsTypeCombo, 0, 0);
mountControlsLayout->addWidget(mountPathEdit, 0, 1);
mountControlsLayout->addWidget(mountButton, 0, 2);
mountControlsLayout->addWidget(offsetEdit, 1, 0);
mountControlsLayout->addWidget(optionsEdit, 1, 1);
mountControlsLayout->addWidget(umountButton, 1, 2);
mountLayout->addLayout(mountControlsLayout);
@@ -135,11 +173,44 @@ void FilesystemWidget::setupUI()
filesystemGroup = new QGroupBox(tr("Filesystem Browser"), this);
QVBoxLayout *fsLayout = new QVBoxLayout(filesystemGroup);
auto *navigation = new QHBoxLayout();
auto *upButton = new QPushButton(tr("Up"), this);
auto *sourcesButton = new QPushButton(tr("Source Files"), this);
auto *refreshButton = new QPushButton(tr("Refresh"), this);
browserPathEdit = new QLineEdit(QStringLiteral("/"), this);
browserPathEdit->setObjectName(QStringLiteral("filesystemPath"));
navigation->addWidget(upButton);
navigation->addWidget(browserPathEdit, 1);
navigation->addWidget(sourcesButton);
navigation->addWidget(refreshButton);
fsLayout->addLayout(navigation);
connect(upButton, &QPushButton::clicked, this, [this]() {
setBrowserPath(QDir::cleanPath(treeModel->getRootPath() + QStringLiteral("/..")));
});
connect(browserPathEdit, &QLineEdit::returnPressed, this, [this]() {
setBrowserPath(browserPathEdit->text());
});
connect(sourcesButton, &QPushButton::clicked, this, &FilesystemWidget::showSourceFiles);
connect(refreshButton, &QPushButton::clicked, this, &FilesystemWidget::refresh);
filesystemTree = new QTreeView(this);
filesystemTree->setObjectName(QStringLiteral("filesystemTree"));
treeModel = new FilesystemTreeModel(this);
filesystemTree->setModel(treeModel);
filesystemTree->setContextMenuPolicy(Qt::CustomContextMenu);
filesystemTree->setSelectionBehavior(QAbstractItemView::SelectRows);
filesystemTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
filesystemTree->setUniformRowHeights(true);
filesystemTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
filesystemTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
filesystemTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
filesystemTree->header()->setStretchLastSection(false);
connect(filesystemTree, &QTreeView::expanded, this, &FilesystemWidget::onTreeExpanded);
connect(
filesystemTree->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
&FilesystemWidget::onTreeSelectionChanged);
fsLayout->addWidget(filesystemTree);
QHBoxLayout *fsControlsLayout = new QHBoxLayout();
@@ -153,6 +224,49 @@ void FilesystemWidget::setupUI()
splitter->addWidget(filesystemGroup);
previewGroup = new QGroupBox(tr("File Preview"), this);
auto *previewLayout = new QVBoxLayout(previewGroup);
previewStatus = new QLabel(tr("Select a file to preview it."), previewGroup);
previewStatus->setTextFormat(Qt::PlainText);
previewStatus->setWordWrap(true);
previewLayout->addWidget(previewStatus);
previewTabs = new QTabWidget(previewGroup);
sourceFunctions = new QTreeWidget(previewTabs);
sourceFunctions->setObjectName(QStringLiteral("sourceFunctions"));
sourceFunctions->setHeaderLabels({tr("Function"), tr("Address")});
sourceLines = new QTreeWidget(previewTabs);
sourceLines->setObjectName(QStringLiteral("sourceLines"));
sourceLines->setHeaderLabels({tr("Address"), tr("Line"), tr("Source")});
sourceLines->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
auto navigate = [](QTreeWidgetItem *item, int) {
const QVariant address = item->data(0, Qt::UserRole);
if (address.isValid()) {
Core()->seekAndShow(address.toULongLong());
}
};
for (auto *tree : {sourceFunctions, sourceLines}) {
tree->setRootIsDecorated(false);
tree->setSelectionBehavior(QAbstractItemView::SelectRows);
tree->setUniformRowHeights(true);
connect(tree, &QTreeWidget::itemClicked, this, navigate);
connect(tree, &QTreeWidget::itemActivated, this, navigate);
}
sourceLines->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
sourceLines->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
fileContents = new QPlainTextEdit(previewTabs);
fileContents->setObjectName(QStringLiteral("filesystemContents"));
fileContents->setReadOnly(true);
fileContents->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
fileContents->setLineWrapMode(QPlainTextEdit::NoWrap);
previewTabs->addTab(sourceFunctions, tr("Functions"));
previewTabs->addTab(sourceLines, tr("Source Lines"));
previewTabs->addTab(fileContents, tr("Contents"));
previewLayout->addWidget(previewTabs);
splitter->addWidget(previewGroup);
splitter->setStretchFactor(1, 2);
splitter->setStretchFactor(2, 1);
splitter->setSizes({170, 400, 230});
mainLayout->addWidget(splitter);
setWidget(container);
@@ -173,7 +287,9 @@ void FilesystemWidget::setupUI()
void FilesystemWidget::setupMountpointsList()
{
// Will be populated in refreshMountpoints
connect(mountpointsList, &QListWidget::itemClicked, this, [this](QListWidgetItem *item) {
setBrowserPath(item->data(Qt::UserRole).toString());
});
}
void FilesystemWidget::setupFilesystemTree()
@@ -189,6 +305,7 @@ void FilesystemWidget::setupActions()
connect(viewAction, &QAction::triggered, this, [this]() {
QModelIndex index = filesystemTree->currentIndex();
index = index.sibling(index.row(), 0);
if (index.isValid()) {
QString path = index.data(Qt::UserRole).toString();
if (path.isEmpty()) {
@@ -200,6 +317,7 @@ void FilesystemWidget::setupActions()
connect(deleteAction, &QAction::triggered, this, [this]() {
QModelIndex index = filesystemTree->currentIndex();
index = index.sibling(index.row(), 0);
if (index.isValid()) {
QString path = index.data(Qt::UserRole).toString();
if (path.isEmpty()) {
@@ -211,6 +329,7 @@ void FilesystemWidget::setupActions()
connect(mallocAction, &QAction::triggered, this, [this]() {
QModelIndex index = filesystemTree->currentIndex();
index = index.sibling(index.row(), 0);
if (index.isValid()) {
QString path = index.data(Qt::UserRole).toString();
if (path.isEmpty()) {
@@ -267,19 +386,16 @@ QList<PluginInfo> FilesystemWidget::parsePlugins(const QJsonDocument &doc)
void FilesystemWidget::refreshMountpoints()
{
QString output = Core()->cmdRaw("mj");
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(output.toUtf8(), &error);
if (error.error != QJsonParseError::NoError) {
qWarning() << "Failed to parse mj output:" << error.errorString();
const QJsonDocument doc = Core()->cmdj("mj");
if (!doc.isObject()) {
return;
}
mountpointsList->clear();
fsTypeCombo->clear();
const QList<MountpointInfo> mountpoints = parseMountpoints(doc);
for (const MountpointInfo &mountpoint : mountpoints) {
currentMountpoints = parseMountpoints(doc);
for (const MountpointInfo &mountpoint : currentMountpoints) {
QString details
= QString("%1 @ 0x%2").arg(mountpoint.plugin, QString::number(mountpoint.offset, 16));
if (!mountpoint.options.isEmpty()) {
@@ -300,6 +416,142 @@ void FilesystemWidget::refreshMountpoints()
}
}
void FilesystemWidget::refresh()
{
if (!refreshDeferrer->attemptRefresh(nullptr)) {
return;
}
const QString selectedPath = previewPath;
QSet<QString> expandedPaths;
auto visit = [this](auto &&self, const QModelIndex &parent, const auto &action) -> void {
for (int row = 0; row < treeModel->rowCount(parent); ++row) {
const QModelIndex index = treeModel->index(row, 0, parent);
action(index);
self(self, index, action);
}
};
visit(visit, QModelIndex(), [this, &expandedPaths](const QModelIndex &index) {
if (filesystemTree->isExpanded(index)) {
expandedPaths.insert(index.data(FilesystemTreeModel::PathRole).toString());
}
});
const QSignalBlocker selectionBlocker(filesystemTree->selectionModel());
refreshMountpoints();
treeModel->refresh();
bool selectedFileExists = false;
visit(
visit,
QModelIndex(),
[this, &expandedPaths, &selectedPath, &selectedFileExists](const QModelIndex &index) {
const QString path = index.data(FilesystemTreeModel::PathRole).toString();
if (expandedPaths.contains(path)) {
filesystemTree->expand(index);
}
if (!selectedPath.isEmpty() && path == selectedPath) {
filesystemTree->setCurrentIndex(index);
selectedFileExists = true;
}
});
if (selectedFileExists) {
viewFileContents(selectedPath);
} else {
clearPreview();
}
}
void FilesystemWidget::setBrowserPath(const QString &path)
{
QString absolutePath = path;
if (!absolutePath.startsWith(QLatin1Char('/'))) {
absolutePath.prepend(QLatin1Char('/'));
}
absolutePath = QDir::cleanPath(absolutePath);
browserPathEdit->setText(absolutePath);
clearPreview();
treeModel->setRootPath(absolutePath);
const bool sourceDirectory = isSourceFile(absolutePath + QLatin1Char('/'));
createDirButton->setEnabled(!sourceDirectory);
createFileButton->setEnabled(!sourceDirectory);
}
bool FilesystemWidget::isSourceFile(const QString &path) const
{
for (const auto &mount : currentMountpoints) {
const QString prefix = mount.path == QStringLiteral("/")
? QStringLiteral("/cl/")
: mount.path + QStringLiteral("/cl/");
if (mount.plugin == QStringLiteral("r2") && path.startsWith(prefix)) {
return true;
}
}
return false;
}
void FilesystemWidget::showSourceFiles()
{
refreshMountpoints();
QString mountPath;
QSet<QString> usedPaths;
for (const auto &mount : currentMountpoints) {
usedPaths.insert(mount.path);
if (mount.plugin == QStringLiteral("r2") && mountPath.isEmpty()) {
mountPath = mount.path;
}
}
if (mountPath.isEmpty()) {
mountPath = QStringLiteral("/r2");
for (int suffix = 1; usedPaths.contains(mountPath); ++suffix) {
mountPath = QStringLiteral("/r2_%1").arg(suffix);
}
const QByteArray pathBytes = mountPath.toUtf8();
RCoreLocked core = Core()->core();
if (!r_fs_mount(core->fs, "r2", pathBytes.constData(), 0)) {
previewStatus->setText(tr("Failed to mount the r2 filesystem."));
return;
}
}
refreshMountpoints();
setBrowserPath(mountPath);
bool hasSourceDirectory = false;
for (int row = 0; row < treeModel->rowCount(); ++row) {
hasSourceDirectory |= treeModel->index(row, 0).data().toString() == QStringLiteral("cl");
}
if (!hasSourceDirectory) {
previewStatus->setText(tr("This r2 filesystem does not provide a /cl source directory."));
return;
}
const QString sourcePath = QDir::cleanPath(mountPath + QStringLiteral("/cl"));
setBrowserPath(sourcePath);
if (treeModel->rowCount() == 0) {
previewStatus->setText(
tr("No source references are available. The binary needs source "
"line information and an r2 filesystem with /cl support."));
}
}
void FilesystemWidget::clearPreview()
{
previewPath.clear();
sourceFunctions->clear();
sourceLines->clear();
fileContents->clear();
previewStatus->setText(tr("Select a file to preview it."));
previewTabs->setTabEnabled(0, false);
previewTabs->setTabEnabled(1, false);
previewTabs->setCurrentWidget(fileContents);
}
void FilesystemWidget::onTreeSelectionChanged(const QModelIndex &index)
{
const QModelIndex nameIndex = index.sibling(index.row(), 0);
if (nameIndex.isValid()
&& nameIndex.sibling(nameIndex.row(), 2).data().toString() == QStringLiteral("file")) {
viewFileContents(nameIndex.data(FilesystemTreeModel::PathRole).toString());
} else {
clearPreview();
}
}
void FilesystemWidget::onMountButtonClicked()
{
QString fsType = fsTypeCombo->currentText();
@@ -351,7 +603,7 @@ void FilesystemWidget::onMountButtonClicked()
}
refreshMountpoints();
treeModel->refresh();
setBrowserPath(path);
}
void FilesystemWidget::onUmountButtonClicked()
@@ -383,12 +635,13 @@ void FilesystemWidget::onUmountButtonClicked()
}
refreshMountpoints();
treeModel->refresh();
setBrowserPath(QStringLiteral("/"));
}
void FilesystemWidget::onCreateDirButtonClicked()
{
QModelIndex current = filesystemTree->currentIndex();
current = current.sibling(current.row(), 0);
QString currentPath = "/";
if (current.isValid()) {
QString path = current.data(Qt::UserRole).toString();
@@ -416,6 +669,7 @@ void FilesystemWidget::onCreateDirButtonClicked()
void FilesystemWidget::onCreateFileButtonClicked()
{
QModelIndex current = filesystemTree->currentIndex();
current = current.sibling(current.row(), 0);
QString currentPath = "/";
if (current.isValid()) {
QString path = current.data(Qt::UserRole).toString();
@@ -447,11 +701,10 @@ void FilesystemWidget::onTreeItemDoubleClicked(const QModelIndex &index)
QString type = index.sibling(index.row(), 2).data(Qt::DisplayRole).toString();
if (type == "file") {
QString path = index.data(Qt::UserRole).toString();
if (path.isEmpty()) {
path = index.data(Qt::DisplayRole).toString();
}
const QString path = index.sibling(index.row(), 0).data(Qt::UserRole).toString();
if (type == "directory") {
setBrowserPath(path);
} else if (type == "file") {
viewFileContents(path);
}
}
@@ -466,7 +719,8 @@ void FilesystemWidget::onTreeExpanded(const QModelIndex &index)
return;
// Check if it has a dummy child
if (item->rowCount() == 1 && item->child(0)->text() == "Loading...") {
if (!item->data(FilesystemTreeModel::LoadedRole).toBool()) {
item->setData(true, FilesystemTreeModel::LoadedRole);
// Remove dummy and load real children
item->removeRow(0);
QString path = item->data(Qt::UserRole).toString();
@@ -483,23 +737,94 @@ void FilesystemWidget::onTreeContextMenu(const QPoint &pos)
return;
QString type = index.sibling(index.row(), 2).data(Qt::DisplayRole).toString();
filesystemTree->setCurrentIndex(index.sibling(index.row(), 0));
QMenu menu(this);
if (type == "file") {
menu.addAction(viewAction);
menu.addAction(deleteAction);
menu.addAction(mallocAction);
const QString path = index.sibling(index.row(), 0).data(Qt::UserRole).toString();
if (!isSourceFile(path)) {
menu.addAction(deleteAction);
menu.addAction(mallocAction);
}
}
menu.exec(filesystemTree->viewport()->mapToGlobal(pos));
}
void FilesystemWidget::viewFileContents(const QString &path)
{
QString sanitizedPath = IaitoCore::sanitizeStringForCommand(path);
QString cmd = QString("mc %1").arg(sanitizedPath);
QString output = Core()->cmdRaw(cmd.toUtf8().constData());
if (!output.isEmpty()) {
QMessageBox::information(this, tr("File Contents"), output);
QWidget *previousTab = previewPath == path ? previewTabs->currentWidget() : nullptr;
clearPreview();
if (path.isEmpty()) {
return;
}
const bool sourceFile = isSourceFile(path);
const QByteArray pathBytes = path.toUtf8();
QByteArray contents;
bool truncated = false;
{
RCoreLocked core = Core()->core();
RFSFile *file = r_fs_open(core->fs, pathBytes.constData(), false);
if (!file) {
previewStatus->setText(tr("Cannot open %1").arg(path));
return;
}
// Virtual r2 files are already populated by open. Other plugins need a read.
const int previewLimit = 1024 * 1024;
const int length = int(qMin<quint64>(file->size, previewLimit));
int bytesRead = length;
if (!file->data && length > 0) {
bytesRead = r_fs_read(core->fs, file, 0, length);
}
if (file->data && bytesRead >= 0) {
const int available = int(qMin<quint64>(file->size, bytesRead));
contents = QByteArray(reinterpret_cast<const char *>(file->data), available);
truncated = file->size > quint64(available);
}
r_fs_close(core->fs, file);
r_fs_file_free(file);
if (bytesRead < 0) {
previewStatus->setText(tr("Cannot read %1").arg(path));
return;
}
}
previewPath = path;
const QString text = QString::fromUtf8(contents);
fileContents->setPlainText(text);
previewStatus->setText(truncated ? tr("%1 (preview truncated to 1 MiB)").arg(path) : path);
if (!sourceFile) {
return;
}
QSet<RVA> functions;
const auto references = SourceLineReference::parse(text);
{
RCoreLocked core = Core()->core();
for (const auto &reference : references) {
auto *line = new QTreeWidgetItem(
sourceLines,
{RAddressString(reference.address),
QString::number(reference.line),
reference.code});
line->setData(0, Qt::UserRole, QVariant::fromValue(reference.address));
RAnalFunction *function = r_anal_get_fcn_in(core->anal, reference.address, 0);
if (function && !functions.contains(function->addr)) {
functions.insert(function->addr);
auto *item = new QTreeWidgetItem(
sourceFunctions,
{QString::fromUtf8(function->name), RAddressString(function->addr)});
item->setData(0, Qt::UserRole, QVariant::fromValue(RVA(function->addr)));
}
}
}
previewTabs->setTabEnabled(0, true);
previewTabs->setTabEnabled(1, true);
previewTabs->setCurrentWidget(
previousTab ? previousTab : (functions.isEmpty() ? sourceLines : sourceFunctions));
sourceFunctions->resizeColumnToContents(0);
if (functions.isEmpty() && !references.isEmpty()) {
previewStatus->setText(
tr("%1 — no analyzed functions; click a source address to view code.").arg(path));
}
}
+25 -4
View File
@@ -14,10 +14,12 @@
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QSplitter>
#include <QStandardItem>
#include <QStandardItemModel>
#include <QTabWidget>
#include <QTreeView>
#include <QTreeWidget>
#include <QVBoxLayout>
@@ -42,7 +44,6 @@ struct FileInfo
QString name;
QString type; // 'f' for file, 'd' for directory
quint64 size;
QString timestamp; // if available
};
class FilesystemTreeModel : public QStandardItemModel
@@ -55,10 +56,13 @@ public:
void setRootPath(const QString &path);
void refresh();
void populateDirectory(QStandardItem *parentItem, const QString &path);
QString getRootPath() const { return rootPath; }
enum Roles { PathRole = Qt::UserRole, LoadedRole };
private:
QString rootPath;
QJsonDocument parseMdCommand(const QString &path);
QList<FileInfo> readDirectory(const QString &path);
};
class FilesystemWidget : public IaitoDockWidget
@@ -69,6 +73,9 @@ public:
explicit FilesystemWidget(MainWindow *main);
~FilesystemWidget();
public slots:
void showSourceFiles();
private slots:
void refreshMountpoints();
void onMountButtonClicked();
@@ -78,6 +85,8 @@ private slots:
void onTreeItemDoubleClicked(const QModelIndex &index);
void onTreeContextMenu(const QPoint &pos);
void onTreeExpanded(const QModelIndex &index);
void refresh();
void onTreeSelectionChanged(const QModelIndex &index);
private:
void setupUI();
@@ -91,8 +100,9 @@ private:
void viewFileContents(const QString &path);
void deleteFile(const QString &path);
void loadIntoMalloc(const QString &path);
MainWindow *mainWindow;
void setBrowserPath(const QString &path);
bool isSourceFile(const QString &path) const;
void clearPreview();
// Mountpoints section
QGroupBox *mountpointsGroup;
@@ -110,6 +120,17 @@ private:
FilesystemTreeModel *treeModel;
QPushButton *createDirButton;
QPushButton *createFileButton;
QLineEdit *browserPathEdit;
QGroupBox *previewGroup;
QLabel *previewStatus;
QTabWidget *previewTabs;
QTreeWidget *sourceFunctions;
QTreeWidget *sourceLines;
QPlainTextEdit *fileContents;
QString previewPath;
QList<MountpointInfo> currentMountpoints;
RefreshDeferrer *refreshDeferrer;
// Context menu actions
QAction *viewAction;
+52 -16
View File
@@ -1,4 +1,5 @@
#include "FlagsWidget.h"
#include "NameListView.h"
#include "common/Helpers.h"
#include "common/ShortcutManager.h"
#include "core/MainWindow.h"
@@ -161,33 +162,39 @@ FlagsWidget::FlagsWidget(MainWindow *main)
flags_model = new FlagsModel(&flags, this);
flags_proxy_model = new FlagsSortFilterProxyModel(flags_model, this);
connect(
ui->filterLineEdit,
&QLineEdit::textChanged,
ui->quickFilterView,
&QuickFilterView::filterTextChanged,
flags_proxy_model,
&QSortFilterProxyModel::setFilterWildcard);
&AddressableFilterProxyModel::setFilterWildcard);
ui->flagsTreeView->setMainWindow(mainWindow);
ui->flagsTreeView->setModel(static_cast<QAbstractItemModel *>(flags_proxy_model));
auto names = new NameListView(
ui->flagsTreeView,
ui->flagsTreeView->getItemContextMenu(),
flags_proxy_model,
FlagsModel::NAME,
objectName());
ui->quickFilterView->addActionButton(names->toggleAction());
ui->flagsTreeView->sortByColumn(FlagsModel::OFFSET, Qt::AscendingOrder);
connect(ui->flagsTreeView, &QTreeView::clicked, this, &FlagsWidget::selectFlagRange);
connect(ui->flagsTreeView, &QTreeView::activated, this, &FlagsWidget::selectFlagRange);
// Ctrl-F to move the focus to the Filter search box
QShortcut *searchShortcut = ShortcutMgr()->registerShortcut("list.showFilter", this);
connect(searchShortcut, &QShortcut::activated, ui->filterLineEdit, [this]() {
ui->filterLineEdit->setFocus();
});
connect(searchShortcut, &QShortcut::activated, ui->quickFilterView, &QuickFilterView::showFilter);
searchShortcut->setContext(Qt::WidgetWithChildrenShortcut);
// Esc to clear the filter entry
QShortcut *clearShortcut = ShortcutMgr()->registerShortcut("list.clearFilter", this);
connect(clearShortcut, &QShortcut::activated, [this] {
if (ui->filterLineEdit->text().isEmpty()) {
ui->flagsTreeView->setFocus();
} else {
ui->filterLineEdit->setText("");
}
});
connect(clearShortcut, &QShortcut::activated, [this] { ui->quickFilterView->clearFilter(); });
connect(
ui->quickFilterView,
&QuickFilterView::filterClosed,
ui->flagsTreeView,
static_cast<void (QWidget::*)()>(&QWidget::setFocus));
clearShortcut->setContext(Qt::WidgetWithChildrenShortcut);
connect(ui->filterLineEdit, &QLineEdit::textChanged, this, [this] {
connect(ui->quickFilterView, &QuickFilterView::filterTextChanged, this, [this] {
tree->showItemsNumber(flags_proxy_model->rowCount());
});
@@ -204,6 +211,21 @@ FlagsWidget::FlagsWidget(MainWindow *main)
menu->addSeparator();
menu->addAction(ui->actionRename);
menu->addAction(ui->actionDelete);
connect(menu, &QMenu::aboutToShow, this, [this] {
const bool hasFlag = ui->flagsTreeView->itemAddress(ui->flagsTreeView->currentIndex())
!= RVA_INVALID;
ui->actionRename->setEnabled(hasFlag);
ui->actionDelete->setEnabled(hasFlag);
});
connect(
ui->flagsTreeView->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
[this](const QModelIndex &index) {
const bool hasFlag = ui->flagsTreeView->itemAddress(index) != RVA_INVALID;
ui->actionRename->setEnabled(hasFlag);
ui->actionDelete->setEnabled(hasFlag);
});
addAction(ui->actionRename);
addAction(ui->actionDelete);
}
@@ -214,6 +236,20 @@ FlagsWidget::~FlagsWidget()
delete flags_model;
}
void FlagsWidget::selectFlagRange(const QModelIndex &index)
{
const QVariant description = index.data(FlagsModel::FlagDescriptionRole);
if (!description.isValid()) {
return;
}
const auto flag = description.value<FlagDescription>();
Core()->clearAddressRangeSelection();
if (flag.size > 0 && flag.size < 1024 && flag.size <= RVA_MAX - flag.offset) {
Core()->setAddressRangeSelection(flag.offset, flag.offset + flag.size - 1);
}
}
void FlagsWidget::on_flagspaceCombo_currentTextChanged(const QString &arg1)
{
Q_UNUSED(arg1);
@@ -224,7 +260,7 @@ void FlagsWidget::on_flagspaceCombo_currentTextChanged(const QString &arg1)
void FlagsWidget::on_actionRename_triggered()
{
const QModelIndex index = ui->flagsTreeView->selectionModel()->currentIndex();
if (!index.isValid()) {
if (!index.isValid() || !index.data(FlagsModel::FlagDescriptionRole).isValid()) {
return;
}
FlagDescription flag = index.data(FlagsModel::FlagDescriptionRole).value<FlagDescription>();
@@ -245,7 +281,7 @@ void FlagsWidget::on_actionRename_triggered()
void FlagsWidget::on_actionDelete_triggered()
{
const QModelIndex index = ui->flagsTreeView->selectionModel()->currentIndex();
if (!index.isValid()) {
if (!index.isValid() || !index.data(FlagsModel::FlagDescriptionRole).isValid()) {
return;
}
FlagDescription flag = index.data(FlagsModel::FlagDescriptionRole).value<FlagDescription>();
+1
View File
@@ -70,6 +70,7 @@ public:
~FlagsWidget();
private slots:
void selectFlagRange(const QModelIndex &index);
void on_flagspaceCombo_currentTextChanged(const QString &arg1);
void on_actionRename_triggered();
+9 -10
View File
@@ -31,7 +31,7 @@
<number>0</number>
</property>
<item>
<widget class="AddressableItemList&lt;&gt;" name="flagsTreeView">
<widget class="AddressableItemView" name="flagsTreeView">
<property name="styleSheet">
<string notr="true">IaitoTreeView::item
{
@@ -71,14 +71,7 @@
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="filterLineEdit">
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Quick Filter</string>
</property>
</widget>
<widget class="QuickFilterView" name="quickFilterView" native="true"/>
</item>
<item>
<widget class="QLabel" name="flagspaceLabel">
@@ -119,7 +112,13 @@
</widget>
<customwidgets>
<customwidget>
<class>AddressableItemList&lt;&gt;</class>
<class>QuickFilterView</class>
<extends>QWidget</extends>
<header>widgets/QuickFilterView.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>AddressableItemView</class>
<extends>QTreeView</extends>
<header>widgets/AddressableItemList.h</header>
<container>1</container>
+20 -26
View File
@@ -530,7 +530,7 @@ FunctionsWidget::FunctionsWidget(MainWindow *main)
functionModel = new FunctionModel(
&functions, &importAddresses, &mainAdress, false, default_font, highlight_font, this);
functionProxyModel = new FunctionSortFilterProxyModel(functionModel, this);
setModels(functionProxyModel);
setModels(functionProxyModel, FunctionModel::NameColumn);
ui->treeView->sortByColumn(FunctionModel::NameColumn, Qt::AscendingOrder);
connect(Config(), &Configuration::fontsUpdated, this, [this]() {
@@ -593,6 +593,21 @@ FunctionsWidget::FunctionsWidget(MainWindow *main)
itemConextMenu->setActionIcon(
actionSetPinMenu, AddressableItemContextMenu::MenuIcon::Pin, QColor(103, 80, 164));
itemConextMenu->setWholeFunction(true);
connect(itemConextMenu, &QMenu::aboutToShow, this, [this] {
const bool hasFunction = ui->treeView->itemAddress(ui->treeView->currentIndex())
!= RVA_INVALID;
actionRename.setEnabled(hasFunction);
actionUndefine.setEnabled(hasFunction);
actionSetColorMenu->setEnabled(hasFunction);
actionSetPinMenu->setEnabled(hasFunction);
});
connect(
ui->treeView->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
[this](const QModelIndex &index) {
actionRename.setEnabled(ui->treeView->itemAddress(index) != RVA_INVALID);
});
addActions(itemConextMenu->actions());
@@ -685,7 +700,7 @@ void FunctionsWidget::onActionFunctionsRenameTriggered()
{
// Get selected item in functions tree view
const QModelIndex index = ui->treeView->selectionModel()->currentIndex();
if (!index.isValid()) {
if (!index.isValid() || !index.data(FunctionModel::FunctionDescriptionRole).isValid()) {
return;
}
FunctionDescription function
@@ -712,12 +727,7 @@ void FunctionsWidget::onActionFunctionsRenameTriggered()
void FunctionsWidget::onActionFunctionsUndefineTriggered()
{
const auto selection = ui->treeView->selectionModel()->selection().indexes();
std::vector<RVA> offsets;
offsets.reserve(selection.size());
for (const auto &index : selection) {
offsets.push_back(functionProxyModel->address(index));
}
const auto offsets = ui->treeView->selectedAddresses();
for (RVA offset : offsets) {
Core()->delFunction(offset);
}
@@ -725,15 +735,7 @@ void FunctionsWidget::onActionFunctionsUndefineTriggered()
void FunctionsWidget::onActionFunctionColorPicked(const QString &r2Color)
{
const auto selection = ui->treeView->selectionModel()->selection().indexes();
std::vector<RVA> offsets;
offsets.reserve(selection.size());
for (const auto &index : selection) {
RVA off = functionProxyModel->address(index);
if (std::find(offsets.begin(), offsets.end(), off) == offsets.end()) {
offsets.push_back(off);
}
}
const auto offsets = ui->treeView->selectedAddresses();
bool affectsView = false;
RVA seek = Core()->getOffset();
for (RVA offset : offsets) {
@@ -755,15 +757,7 @@ void FunctionsWidget::onActionFunctionColorPicked(const QString &r2Color)
void FunctionsWidget::onActionFunctionPinPicked(const QString &emoji)
{
const auto selection = ui->treeView->selectionModel()->selection().indexes();
std::vector<RVA> offsets;
offsets.reserve(selection.size());
for (const auto &index : selection) {
RVA off = functionProxyModel->address(index);
if (std::find(offsets.begin(), offsets.end(), off) == offsets.end()) {
offsets.push_back(off);
}
}
const auto offsets = ui->treeView->selectedAddresses();
bool affectsView = false;
RVA seek = Core()->getOffset();
for (RVA offset : offsets) {
+273
View File
@@ -0,0 +1,273 @@
#include "GlobalVariablesWidget.h"
#include "common/Helpers.h"
#include "ui_GlobalVariablesWidget.h"
#include "ui_ListDockWidget.h"
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QJsonArray>
#include <QJsonObject>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
GlobalVariablesModel::GlobalVariablesModel(QObject *parent)
: AddressableItemModel<QAbstractListModel>(parent)
{}
int GlobalVariablesModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : variables.size();
}
int GlobalVariablesModel::columnCount(const QModelIndex &) const
{
return ColumnCount;
}
QVariant GlobalVariablesModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= variables.size()) {
return {};
}
const auto &variable = variables.at(index.row());
if (role == DescriptionRole) {
return QVariant::fromValue(variable);
}
if (role != Qt::DisplayRole && role != Qt::EditRole) {
return {};
}
switch (index.column()) {
case AddressColumn:
return role == Qt::EditRole ? QVariant::fromValue(variable.address)
: QVariant(RAddressString(variable.address));
case NameColumn:
return variable.name;
case TypeColumn:
return variable.type;
case SizeColumn:
return role == Qt::EditRole ? QVariant::fromValue(variable.size)
: QVariant(RSizeString(variable.size));
case CommentColumn:
return Core()->getCommentAt(variable.address);
default:
return {};
}
}
QVariant GlobalVariablesModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {
return {};
}
switch (section) {
case AddressColumn:
return tr("Address");
case NameColumn:
return tr("Name");
case TypeColumn:
return tr("Type");
case SizeColumn:
return tr("Size");
case CommentColumn:
return tr("Comment");
default:
return {};
}
}
RVA GlobalVariablesModel::address(const QModelIndex &index) const
{
return index.isValid() && index.row() >= 0 && index.row() < variables.size()
? variables.at(index.row()).address
: RVA_INVALID;
}
QString GlobalVariablesModel::name(const QModelIndex &index) const
{
return index.isValid() && index.row() >= 0 && index.row() < variables.size()
? variables.at(index.row()).name
: QString();
}
void GlobalVariablesModel::setVariables(QList<GlobalVariableDescription> variables)
{
beginResetModel();
this->variables = std::move(variables);
endResetModel();
}
GlobalVariablesWidget::GlobalVariablesWidget(MainWindow *main)
: ListDockWidget(main)
, controls(new Ui::GlobalVariablesWidget)
, model(new GlobalVariablesModel(this))
, proxy(new AddressableFilterProxyModel(model, this))
{
setObjectName(QStringLiteral("GlobalVariablesWidget"));
setWindowTitle(tr("Global Variables"));
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxy->setFilterKeyColumn(-1);
proxy->setSortRole(Qt::EditRole);
setModels(proxy, GlobalVariablesModel::NameColumn, GlobalVariablesModel::TypeColumn);
ui->treeView->sortByColumn(GlobalVariablesModel::AddressColumn, Qt::AscendingOrder);
auto toolbar = new QWidget(this);
controls->setupUi(toolbar);
ui->verticalLayout->insertWidget(0, toolbar);
auto menu = ui->treeView->getItemContextMenu();
menu->addSeparator();
auto addAction = menu->addAction(tr("Register…"));
addAction->setToolTip(tr("Register a global variable"));
addAction->setObjectName(QStringLiteral("actionAddGlobalVariable"));
addAction->setProperty("addressIndependent", true);
editAction = menu->addAction(tr("Edit…"));
editAction->setToolTip(tr("Edit the global variable name and type"));
editAction->setObjectName(QStringLiteral("actionEditGlobalVariable"));
deleteAction = menu->addAction(tr("Delete"));
deleteAction->setToolTip(tr("Delete the global variable"));
deleteAction->setObjectName(QStringLiteral("actionDeleteGlobalVariable"));
auto refreshAction = menu->addAction(tr("Refresh"));
refreshAction->setObjectName(QStringLiteral("actionRefreshGlobalVariables"));
refreshAction->setProperty("addressIndependent", true);
controls->addButton->setDefaultAction(addAction);
controls->editButton->setDefaultAction(editAction);
controls->deleteButton->setDefaultAction(deleteAction);
controls->refreshButton->setDefaultAction(refreshAction);
editAction->setEnabled(false);
deleteAction->setEnabled(false);
connect(addAction, &QAction::triggered, this, [this]() { editVariable(true); });
connect(editAction, &QAction::triggered, this, [this]() { editVariable(false); });
connect(deleteAction, &QAction::triggered, this, &GlobalVariablesWidget::deleteVariable);
connect(refreshAction, &QAction::triggered, this, &GlobalVariablesWidget::refreshVariables);
const auto updateActions = [this]() {
const bool valid = currentVariable().address != RVA_INVALID;
editAction->setEnabled(valid);
deleteAction->setEnabled(valid);
};
connect(ui->treeView->selectionModel(), &QItemSelectionModel::currentChanged, this, updateActions);
connect(model, &QAbstractItemModel::modelReset, this, updateActions);
refreshDeferrer = createRefreshDeferrer([this]() { refreshVariables(); });
connect(Core(), &IaitoCore::flagsChanged, this, &GlobalVariablesWidget::refreshVariables);
connect(Core(), &IaitoCore::refreshAll, this, &GlobalVariablesWidget::refreshVariables);
connect(Core(), &IaitoCore::codeRebased, this, &GlobalVariablesWidget::refreshVariables);
connect(Core(), &IaitoCore::commentsChanged, this, [this]() {
qhelpers::emitColumnChanged(model, GlobalVariablesModel::CommentColumn);
});
connect(
this, &IaitoDockWidget::becameVisibleToUser, this, &GlobalVariablesWidget::refreshVariables);
refreshVariables();
}
GlobalVariablesWidget::~GlobalVariablesWidget()
{
ui->treeView->setModel(static_cast<QAbstractItemModel *>(nullptr));
}
void GlobalVariablesWidget::refreshVariables()
{
if (!refreshDeferrer->attemptRefresh(nullptr)) {
return;
}
model->setVariables(Core()->getAllGlobalVariables());
for (int column = 0; column < GlobalVariablesModel::ColumnCount; ++column) {
ui->treeView->resizeColumnToContents(column);
}
}
GlobalVariableDescription GlobalVariablesWidget::currentVariable() const
{
return ui->treeView->currentIndex()
.data(GlobalVariablesModel::DescriptionRole)
.value<GlobalVariableDescription>();
}
void GlobalVariablesWidget::editVariable(bool create)
{
const auto variable = currentVariable();
if (!create && variable.address == RVA_INVALID) {
return;
}
QDialog dialog(this);
dialog.setObjectName(QStringLiteral("globalVariableDialog"));
dialog.setWindowTitle(create ? tr("Register Global Variable") : tr("Edit Global Variable"));
auto layout = new QFormLayout(&dialog);
auto address
= new QLineEdit(RAddressString(create ? Core()->getOffset() : variable.address), &dialog);
address->setObjectName(QStringLiteral("globalVariableAddress"));
address->setReadOnly(!create);
address->setToolTip(tr("An address or radare2 expression, such as sym.counter or $$ + 8."));
auto name = new QLineEdit(create ? QString() : variable.name, &dialog);
name->setObjectName(QStringLiteral("globalVariableName"));
auto type = new QComboBox(&dialog);
type->setObjectName(QStringLiteral("globalVariableType"));
type->setEditable(true);
QStringList types;
for (const auto &description : Core()->getAllTypes()) {
types.append(description.type);
}
// Primitive types in current r2 are returned in the types member of tj.
const auto primitiveTypes = Core()->cmdj("tj").object()[QStringLiteral("types")].toArray();
for (const auto &value : primitiveTypes) {
const QString typeName = value.toObject()[QStringLiteral("type")].toString();
types.append(typeName);
}
types.removeDuplicates();
types.sort();
for (const auto &typeName : types) {
type->addItem(typeName, typeName);
}
type->setCurrentText(create ? QStringLiteral("int") : variable.type);
auto error = new QLabel(&dialog);
error->setObjectName(QStringLiteral("globalVariableError"));
error->setWordWrap(true);
error->hide();
auto buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
layout->addRow(tr("Address:"), address);
layout->addRow(tr("Name:"), name);
layout->addRow(tr("Type:"), type);
layout->addRow(error);
layout->addRow(buttons);
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
connect(buttons, &QDialogButtonBox::accepted, &dialog, [&]() {
RVA at;
QString message;
if (!Core()->tryMath(address->text(), at) || at == RVA_INVALID) {
message = tr("Enter a valid address or radare2 expression.");
} else {
if (Core()->setGlobalVariable(
at,
name->text().trimmed(),
type->currentText().trimmed(),
create ? QString() : variable.name,
&message)) {
dialog.accept();
return;
}
}
error->setText(message);
error->show();
});
name->setFocus();
name->selectAll();
dialog.exec();
}
void GlobalVariablesWidget::deleteVariable()
{
const auto variable = currentVariable();
if (variable.address == RVA_INVALID) {
return;
}
if (!Core()->deleteGlobalVariable(variable.address, variable.name)) {
QMessageBox::warning(
this,
tr("Delete Global Variable"),
tr("Could not delete the global variable. Refresh the list and check that no other "
"global variable shares its address."));
}
}
+56
View File
@@ -0,0 +1,56 @@
#ifndef GLOBALVARIABLESWIDGET_H
#define GLOBALVARIABLESWIDGET_H
#include "ListDockWidget.h"
#include <QAbstractListModel>
namespace Ui {
class GlobalVariablesWidget;
}
class GlobalVariablesModel : public AddressableItemModel<QAbstractListModel>
{
Q_OBJECT
public:
enum Column { AddressColumn, NameColumn, TypeColumn, SizeColumn, CommentColumn, ColumnCount };
enum Role { DescriptionRole = Qt::UserRole };
explicit GlobalVariablesModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = {}) const override;
int columnCount(const QModelIndex &parent = {}) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(
int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
RVA address(const QModelIndex &index) const override;
QString name(const QModelIndex &index) const override;
void setVariables(QList<GlobalVariableDescription> variables);
private:
QList<GlobalVariableDescription> variables;
};
class GlobalVariablesWidget : public ListDockWidget
{
Q_OBJECT
public:
explicit GlobalVariablesWidget(MainWindow *main);
~GlobalVariablesWidget() override;
private:
std::unique_ptr<Ui::GlobalVariablesWidget> controls;
GlobalVariablesModel *model;
AddressableFilterProxyModel *proxy;
RefreshDeferrer *refreshDeferrer;
QAction *editAction;
QAction *deleteAction;
void refreshVariables();
void editVariable(bool create);
void deleteVariable();
GlobalVariableDescription currentVariable() const;
};
#endif // GLOBALVARIABLESWIDGET_H
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GlobalVariablesWidget</class>
<widget class="QWidget" name="GlobalVariablesWidget">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin"><number>4</number></property>
<property name="topMargin"><number>4</number></property>
<property name="rightMargin"><number>4</number></property>
<property name="bottomMargin"><number>4</number></property>
<item><widget class="QToolButton" name="addButton"/></item>
<item><widget class="QToolButton" name="editButton"/></item>
<item><widget class="QToolButton" name="deleteButton"/></item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation"><enum>Qt::Horizontal</enum></property>
<property name="sizeHint" stdset="0"><size><width>0</width><height>0</height></size></property>
</spacer>
</item>
<item><widget class="QToolButton" name="refreshButton"/></item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+5
View File
@@ -144,6 +144,7 @@ void GraphView::contextMenuEvent(QContextMenuEvent *event)
void GraphView::computeGraphPlacement()
{
graphLayoutSystem->CalculateLayout(blocks, entry, width, height);
graphLayoutChanged();
setCacheDirty();
clampViewOffset();
viewport()->update();
@@ -371,6 +372,10 @@ void GraphView::paint(QPainter &p, QPoint offset, QRect viewport, qreal scale, b
p.setWindow(window);
QRectF windowF(window.x(), window.y(), window.width(), window.height());
p.save();
drawBackground(p, interactive);
p.restore();
for (auto &blockIt : blocks) {
GraphBlock &block = blockIt.second;
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff. Show more