Compare commits

..
Author SHA1 Message Date
pancake a4badc5fd4 Allow publishing releases from existing version tags 2026-11-06 02:49:08 +01:00
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
111 changed files with 7875 additions and 3297 deletions

No files matched your search

+116 -56
View File
@@ -1,12 +1,18 @@
name: iaito CI
env:
R2V: 6.2.4
R2GHIDRA_VERSION: 6.2.4
R2V: 6.2.6
R2GHIDRA_VERSION: 6.2.6
QT_MAC_INSTALL_VERSION: 6.11.*
QT_WIN_INSTALL_VERSION: 6.10.*
on:
workflow_dispatch:
inputs:
release_tag:
description: Existing version tag to build and publish
required: true
type: string
push:
branches:
- master
@@ -15,8 +21,51 @@ on:
- master
jobs:
source:
runs-on: ubuntu-24.04
outputs:
sha: ${{ steps.source.outputs.sha }}
r2: ${{ steps.source.outputs.R2V }}
r2ghidra: ${{ steps.source.outputs.R2GHIDRA_VERSION }}
qt_mac: ${{ steps.source.outputs.QT_MAC_INSTALL_VERSION }}
qt_win: ${{ steps.source.outputs.QT_WIN_INSTALL_VERSION }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.release_tag && format('refs/tags/{0}', inputs.release_tag) || github.sha }}
persist-credentials: false
- name: Resolve build source and dependencies
id: source
env:
RELEASE_TAG: ${{ inputs.release_tag }}
shell: bash
run: |
if [ -n "$RELEASE_TAG" ]; then
[[ "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
test "$(./configure -qV)" = "$RELEASE_TAG"
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
python3 - <<'PYTHON'
import os
import re
from pathlib import Path
workflow = Path('.github/workflows/ci.yml').read_text()
keys = ('R2V', 'R2GHIDRA_VERSION', 'QT_MAC_INSTALL_VERSION', 'QT_WIN_INSTALL_VERSION')
with open(os.environ['GITHUB_OUTPUT'], 'a') as output:
for key in keys:
value = os.environ[key]
if os.environ['RELEASE_TAG']:
match = re.search(r'^ ' + key + r': ([0-9.*]+)$', workflow, re.M)
if not match:
raise SystemExit(f'Missing dependency version: {key}')
value = match.group(1)
print(f'{key}={value}', file=output)
PYTHON
acr-linux-r2git:
runs-on: ubuntu-22.04
if: ${{ github.event_name != 'workflow_dispatch' }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
@@ -35,34 +84,31 @@ 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:
if: ${{ github.event_name != 'workflow_dispatch' }}
runs-on: ubuntu-24.04
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
needs: source
env:
R2V: ${{ needs.source.outputs.r2 }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.source.outputs.sha }}
submodules: false # 'recursive' 'true' or 'false'
persist-credentials: false
- uses: actions/setup-python@v7
@@ -82,6 +128,9 @@ jobs:
run: ./configure --prefix=/usr --with-qmake6
- name: build iaito
run: make -j4
- name: command-line checks
if: ${{ hashFiles('tests/commandline/test_commandline.py') != '' }}
run: python3 tests/commandline/test_commandline.py build/iaito
- name: packaging
run: make -C dist/debian
- uses: actions/upload-artifact@v7
@@ -89,7 +138,8 @@ jobs:
name: iaito-amd64.deb
path: dist/debian/*/*.deb
acr-linux-nodebugger:
runs-on: ubuntu-22.04
if: ${{ github.event_name != 'workflow_dispatch' }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
@@ -112,7 +162,15 @@ 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:
needs: source
env:
R2V: ${{ needs.source.outputs.r2 }}
R2GHIDRA_VERSION: ${{ needs.source.outputs.r2ghidra }}
QT_MAC_INSTALL_VERSION: ${{ needs.source.outputs.qt_mac }}
QT_WIN_INSTALL_VERSION: ${{ needs.source.outputs.qt_win }}
strategy:
fail-fast: false
matrix:
@@ -127,6 +185,7 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.source.outputs.sha }}
submodules: false # 'recursive' 'true' or 'false'
persist-credentials: false
- uses: actions/setup-python@v7
@@ -196,55 +255,40 @@ 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
if: ${{ github.event_name != 'workflow_dispatch' }}
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
ninja -C build -j4
w64-meson:
needs: source
env:
R2V: ${{ needs.source.outputs.r2 }}
QT_WIN_INSTALL_VERSION: ${{ needs.source.outputs.qt_win }}
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.source.outputs.sha }}
submodules: false
persist-credentials: false
- name: Preparing msvc toolchain
@@ -288,26 +332,28 @@ jobs:
path: iaito.zip
check_release:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/master') }}
outputs:
is_release: ${{ steps.release.outputs.is }}
tag_name: ${{ steps.release.outputs.tag }}
needs:
- source
- acr-linux
- acr-macos
- w64-meson
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ needs.source.outputs.sha }}
fetch-depth: 0 # Download all git history and tags
- name: Check if is a release
run: git describe --exact-match --tags ${{ github.sha }} | awk 'BEGIN{tag="-";r="no"}/^[0-9]+\.[0-9]+/{tag=$0;r="yes"};END{print "is="r;print "tag="tag}' >> ${GITHUB_OUTPUT}
run: git describe --exact-match --tags HEAD | awk 'BEGIN{tag="-";r="no"}/^[0-9]+\.[0-9]+/{tag=$0;r="yes"};END{print "is="r;print "tag="tag}' >> ${GITHUB_OUTPUT}
id: release
macos-sign:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.check_release.outputs.is_release == 'yes' }}
if: ${{ needs.check_release.outputs.is_release == 'yes' }}
needs:
- check_release
runs-on: macos-latest
@@ -324,6 +370,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: refs/tags/${{ needs.check_release.outputs.tag_name }}
- name: Download artifacts
uses: actions/download-artifact@v8
@@ -412,21 +460,28 @@ jobs:
run: security delete-keychain release.keychain
release:
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.check_release.outputs.is_release == 'yes' }}
permissions:
contents: write
if: ${{ needs.check_release.outputs.is_release == 'yes' }}
needs:
- macos-sign
- check_release
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: refs/tags/${{ needs.check_release.outputs.tag_name }}
fetch-depth: 0 # Download all git history and tags
- name: Extract version
run: echo "string=`./configure -qV`" >> $GITHUB_OUTPUT
id: version
- name: Prepare release notes
run: ./scripts/release-notes.sh | tee ./RELEASE_NOTES.md
env:
RELEASE_TAG: ${{ needs.check_release.outputs.tag_name }}
run: |
previous=$(git describe --tags --abbrev=0 "${RELEASE_TAG}^")
./scripts/release-notes.sh "$previous" "$RELEASE_TAG" | tee ./RELEASE_NOTES.md
- name: Download artifacts
uses: actions/download-artifact@v8
with:
@@ -442,10 +497,8 @@ jobs:
tag_name: ${{ needs.check_release.outputs.tag_name }}
release_name: ${{ steps.version.outputs.string }}
body_path: ./RELEASE_NOTES.md
draft: false
draft: true
prerelease: false
- name: Prepare release notes
run: ./scripts/release-notes.sh | tee ./RELEASE_NOTES.md
- name: Upload Linux asset
uses: actions/upload-release-asset@v1
env:
@@ -491,3 +544,10 @@ jobs:
asset_path: dist/artifacts/iaito-w64.exe/iaito.exe
asset_name: iaito-${{ steps.version.outputs.string }}.exe
asset_content_type: application/zip
- name: Publish complete release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_ID: ${{ steps.create_release.outputs.id }}
run: |
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \
-F draft=false -f make_latest=legacy --jq .html_url
+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:
+2 -21
View File
@@ -1,4 +1,4 @@
include config.mk
-include config.mk
ifeq ($(shell uname),Darwin)
BIN=build/iaito.app/Contents/MacOS/iaito
@@ -48,25 +48,6 @@ plugin core_plugin: iaito
rm -f $(R2_USER_PLUGINS)/IaitoPlugin.$(R2_LIBEXT)
cp -f build/CorePlugin.$(R2_LIBEXT) $(R2_USER_PLUGINS)/IaitoPlugin.$(R2_LIBEXT)
# iaito plugin: r2frida as a second IO descriptor, not a native debug session.
ifeq ($(shell uname),Darwin)
IAITO_FRIDA_PLUGIN_DIR=$(HOME)/Library/Application Support/radareorg/iaito/plugins/native
else
IAITO_FRIDA_PLUGIN_DIR=$(HOME)/.local/share/radareorg/iaito/plugins/native
endif
frida-plugin:
mkdir -p build/frida-plugin
cd build/frida-plugin && $(QMAKE) ../../src/plugins/r2frida/IaitoFridaPlugin.pro $(QMAKE_FLAGS)
$(MAKE) -C build/frida-plugin
mkdir -p "$(IAITO_FRIDA_PLUGIN_DIR)"
cp -f build/plugins/native/libIaitoFridaPlugin.* "$(IAITO_FRIDA_PLUGIN_DIR)/"
ifeq ($(shell uname),Darwin)
# The linker’s ad-hoc signature is not valid for dlopen. macOS then
# SIGKILLs iaito with “Code Signature Invalid” while loading the plugin.
codesign --force --sign - "$(IAITO_FRIDA_PLUGIN_DIR)/libIaitoFridaPlugin.dylib"
endif
asan:
export CXXFLAGS=-fsanitize=address ; \
export CFLAGS=-fsanitize=address ; \
@@ -88,7 +69,7 @@ endif
mrproper: clean
git clean -xdf
.PHONY: install run user-install dist macos clean mrproper install-translations frida-plugin
.PHONY: install run user-install dist macos clean mrproper install-translations
.PHONY: dockindent dockindent-image indent indent-host
.PHONY: install-deeplink uninstall-deeplink
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.4' ; VERSION_MAJOR=6; VERSION_MINOR=2; VERSION_PATCH=4; VERSION_NUMBER=60204; 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.4 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.4"
echo "6.2.6"
exit 0
fi
echo "iaito-6.2.4 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.4"
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.4
VERSION 6.2.6
CONTACT pancake ; [email protected]
LANG_CXX!
+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.
+11 -1
View File
@@ -4,7 +4,7 @@ TARGET = iaito
IAITO_VERSION_MAJOR = 6
IAITO_VERSION_MINOR = 2
IAITO_VERSION_PATCH = 4
IAITO_VERSION_PATCH = 6
CONFIG += c++20
QMAKE_CXXFLAGS += -std=c++20
@@ -206,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 \
@@ -232,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 \
@@ -257,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 \
@@ -362,6 +365,7 @@ SOURCES += \
dialogs/ScriptManagerWidget.cpp \
widgets/ZoomWidget.cpp \
common/DataAnalysis.cpp \
common/TypeLayout.cpp \
widgets/DataAnalysisView.cpp \
widgets/DataAnalysisWidget.cpp
@@ -418,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 \
@@ -431,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 \
@@ -444,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 \
@@ -469,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 \
@@ -583,12 +591,14 @@ HEADERS += \
dialogs/ScriptManagerWidget.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 \
+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();
}
+10
View File
@@ -1164,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);
+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();
};
+124 -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",
@@ -320,6 +381,18 @@ const QVector<ShortcutDescriptor> &catalog()
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,
@@ -347,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(
@@ -357,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,
@@ -387,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,
@@ -504,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);
};
+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;
+255 -4
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;
}
@@ -3348,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);
@@ -3687,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();
@@ -4777,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());
}
@@ -4875,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)) {
@@ -4882,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"));
@@ -4903,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())));
+10
View File
@@ -555,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();
@@ -593,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();
+9
View File
@@ -138,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;
@@ -427,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)
+115 -16
View File
@@ -59,6 +59,7 @@
#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"
@@ -1074,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);
@@ -1554,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),
@@ -1591,6 +1620,11 @@ void MainWindow::initDocks()
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),
};
@@ -1631,15 +1665,13 @@ 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;
@@ -2453,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);
@@ -2755,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()) {
@@ -2790,6 +2851,7 @@ void MainWindow::updateHistoryMenu(QMenu *menu, bool redo)
QFont font;
font.setBold(true);
action->setFont(font);
action->setEnabled(false);
}
}
}
@@ -2798,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]() {
@@ -3149,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;
@@ -3172,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();
@@ -3547,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()
@@ -4151,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;
@@ -4184,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;
}
@@ -4204,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;
}
}
+4
View File
@@ -35,6 +35,7 @@ class RelocsWidget;
class CommentsWidget;
class StringsWidget;
class FlagsWidget;
class GlobalVariablesWidget;
class Dashboard;
class QLineEdit;
class QRubberBand;
@@ -342,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;
@@ -418,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();
@@ -448,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
+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">
+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>
+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;
+17
View File
@@ -42,6 +42,23 @@
<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>
-273
View File
@@ -1,273 +0,0 @@
#include "FridaBackend.h"
#include "common/R2Task.h"
#include "core/Iaito.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
FridaBackend::FridaBackend(QObject *parent)
: QObject(parent)
{}
bool FridaBackend::pluginAvailable()
{
if (availabilityChecked_) {
return available_;
}
availabilityChecked_ = true;
const QString listed = Core()->cmd(QStringLiteral("oL"));
available_ = listed.contains(QStringLiteral("frida"), Qt::CaseInsensitive);
return available_;
}
int FridaBackend::currentFd() const
{
bool ok = false;
const int fd = Core()->cmd(QStringLiteral("oqq")).trimmed().toInt(&ok);
return ok ? fd : -1;
}
QSet<int> FridaBackend::openFds() const
{
QSet<int> fds;
const QJsonArray files = Core()->cmdj(QStringLiteral("oj")).array();
for (const QJsonValue value : files) {
fds.insert(value.toObject().value(QStringLiteral("fd")).toInt());
}
return fds;
}
int FridaBackend::findNewFridaFd(const QSet<int> &before) const
{
const QJsonArray files = Core()->cmdj(QStringLiteral("oj")).array();
for (const QJsonValue value : files) {
const QJsonObject file = value.toObject();
const int fd = file.value(QStringLiteral("fd")).toInt();
const QString uri = file.value(QStringLiteral("uri")).toString();
if (!before.contains(fd) && uri.startsWith(QStringLiteral("frida://"))) {
return fd;
}
}
return -1;
}
void FridaBackend::closeFd(int fd)
{
if (fd < 0) {
return;
}
Core()->cmd(QStringLiteral("o-") + QString::number(fd));
if (fridaFd_ == fd) {
fridaFd_ = -1;
}
}
int FridaBackend::restoreFd() const
{
const int current = currentFd();
if (current > 0 && current != fridaFd_) {
return current;
}
if (hostFd_ > 0 && hostFd_ != fridaFd_) {
return hostFd_;
}
return -1;
}
void FridaBackend::runBatch(const QString &batch, const std::function<void(QString)> &done)
{
if (busy_) {
if (done) {
done(QString());
}
return;
}
const QString savedColor = Core()->getConfig(QStringLiteral("scr.color"));
QString script = QStringLiteral("e scr.color=0\n");
script += batch;
if (!script.endsWith(QLatin1Char('\n'))) {
script += QLatin1Char('\n');
}
script += QStringLiteral("e scr.color=");
script += savedColor;
script += QLatin1Char('\n');
busy_ = true;
task_ = QSharedPointer<R2Task>(new R2Task(script));
connect(task_.data(), &R2Task::finished, this, [this, done, savedColor]() {
const QString output = task_ ? task_->getResult() : QString();
task_.clear();
busy_ = false;
Core()->setConfig(QStringLiteral("scr.color"), savedColor);
if (done) {
done(output);
}
});
task_->startTask();
}
QString FridaBackend::fridaCommand(const QString &command)
{
if (fridaFd_ < 0 || command.isEmpty()) {
return QString();
}
const int back = restoreFd();
QString batch = QStringLiteral("o=") + QString::number(fridaFd_) + QStringLiteral("\n:")
+ command + QLatin1Char('\n');
if (back > 0) {
batch += QStringLiteral("o=") + QString::number(back) + QLatin1Char('\n');
}
return Core()->cmd(batch);
}
static QString stripAnsi(QString text)
{
static const QRegularExpression ansi(QStringLiteral("\x1b\\[[0-9;]*m"));
text.replace(ansi, QString());
text.replace(QChar(0x2502), QLatin1Char('|'));
return text;
}
static bool isSeparatorLine(const QString &line)
{
if (line.isEmpty()) {
return true;
}
for (const QChar ch : line) {
if (ch.isSpace() || ch == QLatin1Char('-') || ch == QLatin1Char('|')
|| ch == QLatin1Char('+') || ch == QChar(0x2500) || ch == QChar(0x250c)
|| ch == QChar(0x2510)
|| ch == QChar(0x2514) || ch == QChar(0x2518) || ch == QChar(0x251c)
|| ch == QChar(0x2524) || ch == QChar(0x252c) || ch == QChar(0x2534)
|| ch == QChar(0x253c)) {
continue;
}
return false;
}
return true;
}
static QStringList cellsOf(const QString &line)
{
QString trimmed = line.trimmed();
if (trimmed.startsWith(QLatin1Char('|'))) {
QStringList cells;
const QStringList parts = trimmed.split(QLatin1Char('|'));
for (QString part : parts) {
part = part.trimmed();
if (!part.isEmpty()) {
cells.append(part);
}
}
return cells;
}
return trimmed.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
}
QList<FridaDeviceInfo> FridaBackend::parseDevices(const QString &output)
{
QList<FridaDeviceInfo> devices;
const QStringList lines = stripAnsi(output).split(QLatin1Char('\n'));
for (const QString &raw : lines) {
if (isSeparatorLine(raw.trimmed())) {
continue;
}
const QStringList cells = cellsOf(raw);
if (cells.size() < 3) {
continue;
}
const QString type = cells.at(1).toLower();
if (type != QStringLiteral("local") && type != QStringLiteral("usb")
&& type != QStringLiteral("remote")) {
continue;
}
FridaDeviceInfo device;
device.id = cells.at(0);
device.type = type;
device.name = cells.mid(2).join(QLatin1Char(' '));
devices.append(device);
}
return devices;
}
QList<FridaTargetInfo> FridaBackend::parseTargets(const QString &output, FridaListKind kind)
{
QList<FridaTargetInfo> targets;
const QStringList lines = stripAnsi(output).split(QLatin1Char('\n'));
for (const QString &raw : lines) {
if (isSeparatorLine(raw.trimmed())) {
continue;
}
const QStringList cells = cellsOf(raw);
if (cells.isEmpty()) {
continue;
}
const QString pid = cells.at(0);
const bool pidOk = pid == QStringLiteral("-")
|| QRegularExpression(QStringLiteral("^\\d+$")).match(pid).hasMatch();
if (!pidOk || pid.compare(QStringLiteral("PID"), Qt::CaseInsensitive) == 0) {
continue;
}
FridaTargetInfo target;
target.pid = pid;
if (kind == FridaListKind::Applications && cells.size() >= 3) {
target.identifier = cells.last();
target.name = cells.mid(1, cells.size() - 2).join(QLatin1Char(' '));
} else if (cells.size() >= 2) {
target.name = cells.mid(1).join(QLatin1Char(' '));
}
targets.append(target);
}
return targets;
}
QJsonDocument FridaBackend::parseJsonLoose(const QString &text)
{
const int arrayAt = text.indexOf(QLatin1Char('['));
const int objectAt = text.indexOf(QLatin1Char('{'));
int start = -1;
if (arrayAt >= 0 && (objectAt < 0 || arrayAt < objectAt)) {
start = arrayAt;
} else {
start = objectAt;
}
if (start < 0) {
return QJsonDocument();
}
const QChar open = text.at(start);
const QChar close = open == QLatin1Char('[') ? QLatin1Char(']') : QLatin1Char('}');
const int end = text.lastIndexOf(close);
if (end < start) {
return QJsonDocument();
}
return QJsonDocument::fromJson(text.mid(start, end - start + 1).toUtf8());
}
quint64 FridaBackend::parseAddress(const QJsonValue &value)
{
if (value.isString()) {
QString text = value.toString().trimmed();
bool ok = false;
int base = 10;
if (text.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) {
text = text.mid(2);
base = 16;
}
const quint64 parsed = text.toULongLong(&ok, base);
return ok ? parsed : 0;
}
if (value.isDouble()) {
return static_cast<quint64>(value.toDouble());
}
if (value.isObject()) {
const QJsonObject object = value.toObject();
for (const char *key : {"address", "value", "$handle", "base"}) {
if (object.contains(QLatin1String(key))) {
return parseAddress(object.value(QLatin1String(key)));
}
}
}
return 0;
}
-53
View File
@@ -1,53 +0,0 @@
#pragma once
#include "FridaTypes.h"
#include <QJsonDocument>
#include <QJsonValue>
#include <QObject>
#include <QSet>
#include <QSharedPointer>
#include <functional>
class R2Task;
// Talks to io_frida through the already-open radare2 core.
// The static file stays the current descriptor. Frida is opened with `of`
// (no maps) and selected only for the duration of a `:` command.
class FridaBackend : public QObject
{
Q_OBJECT
public:
explicit FridaBackend(QObject *parent = nullptr);
bool pluginAvailable();
bool busy() const { return busy_; }
int currentFd() const;
int hostFd() const { return hostFd_; }
int fridaFd() const { return fridaFd_; }
void setHostFd(int fd) { hostFd_ = fd; }
void setFridaFd(int fd) { fridaFd_ = fd; }
QSet<int> openFds() const;
int findNewFridaFd(const QSet<int> &before) const;
void closeFd(int fd);
void runBatch(const QString &batch, const std::function<void(QString)> &done);
QString fridaCommand(const QString &command);
static QList<FridaDeviceInfo> parseDevices(const QString &output);
static QList<FridaTargetInfo> parseTargets(const QString &output, FridaListKind kind);
static QJsonDocument parseJsonLoose(const QString &text);
static quint64 parseAddress(const QJsonValue &value);
private:
int restoreFd() const;
int hostFd_ = -1;
int fridaFd_ = -1;
bool busy_ = false;
bool availabilityChecked_ = false;
bool available_ = false;
QSharedPointer<R2Task> task_;
};
-344
View File
@@ -1,344 +0,0 @@
#include "FridaConnectDialog.h"
#include "FridaSession.h"
#include "FridaUri.h"
#include <QButtonGroup>
#include <QComboBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QStyle>
#include <QTreeWidget>
#include <QVBoxLayout>
namespace {
QPushButton *railButton(const QString &text, QStyle::StandardPixmap icon, QWidget *parent)
{
auto *button = new QPushButton(text, parent);
button->setCheckable(true);
button->setIcon(parent->style()->standardIcon(icon));
button->setStyleSheet(QStringLiteral(
"QPushButton { text-align: left; padding: 8px 14px; border: none; border-radius: 6px; }"
"QPushButton:checked { background-color: palette(highlight);"
" color: palette(highlighted-text); }"));
return button;
}
QPushButton *kindButton(const QString &text, QWidget *parent)
{
auto *button = new QPushButton(text, parent);
button->setCheckable(true);
button->setStyleSheet(QStringLiteral(
"QPushButton { padding: 6px 12px; border: 1px solid palette(mid); border-radius: 4px; }"
"QPushButton:checked { background-color: palette(highlight);"
" color: palette(highlighted-text); }"));
return button;
}
}
FridaConnectDialog::FridaConnectDialog(FridaSession *session, QWidget *parent)
: QDialog(parent)
, session(session)
{
setWindowTitle(tr("Connect to r2frida"));
setMinimumSize(640, 460);
auto *layout = new QHBoxLayout(this);
auto *rail = new QVBoxLayout();
transportGroup = new QButtonGroup(this);
localButton = railButton(tr("Local"), QStyle::SP_ComputerIcon, this);
usbButton = railButton(tr("USB"), QStyle::SP_DriveHDIcon, this);
remoteButton = railButton(tr("Remote"), QStyle::SP_DriveNetIcon, this);
localButton->setChecked(true);
transportGroup->addButton(localButton, int(FridaTransport::Local));
transportGroup->addButton(usbButton, int(FridaTransport::Usb));
transportGroup->addButton(remoteButton, int(FridaTransport::Remote));
rail->addWidget(localButton);
rail->addWidget(usbButton);
rail->addWidget(remoteButton);
rail->addStretch();
layout->addLayout(rail);
auto *mainColumn = new QVBoxLayout();
mainColumn->addWidget(new QLabel(tr("Select a device"), this));
auto *deviceRow = new QHBoxLayout();
devices = new QComboBox(this);
auto *refreshDevicesButton = new QPushButton(tr("Refresh"), this);
deviceRow->addWidget(devices, 1);
deviceRow->addWidget(refreshDevicesButton);
mainColumn->addLayout(deviceRow);
remoteHost = new QLineEdit(this);
remoteHost->setPlaceholderText(tr("10.0.0.3:9999"));
remoteHost->setVisible(false);
mainColumn->addWidget(remoteHost);
auto *kindRow = new QHBoxLayout();
kindGroup = new QButtonGroup(this);
appsButton = kindButton(tr("Applications"), this);
processButton = kindButton(tr("Processes"), this);
appsButton->setChecked(true);
kindGroup->addButton(appsButton, int(FridaListKind::Applications));
kindGroup->addButton(processButton, int(FridaListKind::Processes));
kindRow->addWidget(appsButton);
kindRow->addWidget(processButton);
kindRow->addStretch();
mainColumn->addLayout(kindRow);
search = new QLineEdit(this);
search->setPlaceholderText(tr("Search"));
mainColumn->addWidget(search);
targets = new QTreeWidget(this);
targets->setHeaderLabels({tr("Name"), tr("Identifier"), tr("PID")});
targets->setRootIsDecorated(false);
targets->setAlternatingRowColors(true);
targets->header()->setStretchLastSection(false);
targets->header()->setSectionResizeMode(0, QHeaderView::Stretch);
targets->header()->setSectionResizeMode(1, QHeaderView::Stretch);
mainColumn->addWidget(targets, 1);
status = new QLabel(this);
status->setWordWrap(true);
mainColumn->addWidget(status);
auto *buttons = new QHBoxLayout();
auto *attachButton = new QPushButton(tr("Attach"), this);
auto *spawnButton = new QPushButton(tr("Spawn"), this);
auto *launchButton = new QPushButton(tr("Launch"), this);
auto *closeButton = new QPushButton(tr("Cancel"), this);
attachButton->setDefault(true);
buttons->addWidget(attachButton);
buttons->addWidget(spawnButton);
buttons->addWidget(launchButton);
buttons->addStretch();
buttons->addWidget(closeButton);
mainColumn->addLayout(buttons);
layout->addLayout(mainColumn, 1);
const auto transportChanged = [this](bool checked) {
if (!checked) {
return;
}
remoteHost->setVisible(transport() == FridaTransport::Remote);
applyDevices(deviceCache);
refreshTargets();
};
connect(localButton, &QPushButton::toggled, this, transportChanged);
connect(usbButton, &QPushButton::toggled, this, transportChanged);
connect(remoteButton, &QPushButton::toggled, this, transportChanged);
connect(appsButton, &QPushButton::toggled, this, [this](bool checked) {
if (checked) {
refreshTargets();
}
});
connect(processButton, &QPushButton::toggled, this, [this](bool checked) {
if (checked) {
refreshTargets();
}
});
connect(
devices,
qOverload<int>(&QComboBox::currentIndexChanged),
this,
[this](int) { refreshTargets(); });
connect(refreshDevicesButton, &QPushButton::clicked, this, &FridaConnectDialog::refreshDevices);
connect(search, &QLineEdit::textChanged, this, &FridaConnectDialog::filterTargets);
connect(attachButton, &QPushButton::clicked, this, [this]() { choose(FridaAction::Attach); });
connect(spawnButton, &QPushButton::clicked, this, [this]() { choose(FridaAction::Spawn); });
connect(launchButton, &QPushButton::clicked, this, [this]() { choose(FridaAction::Launch); });
connect(closeButton, &QPushButton::clicked, this, &QDialog::reject);
connect(targets, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem *, int) {
const FridaTargetInfo target = selectedTarget();
const bool running = target.pid != QStringLiteral("-") && !target.pid.isEmpty();
choose(running ? FridaAction::Attach : FridaAction::Spawn);
});
if (!session->checkAvailable()) {
status->setText(
tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
attachButton->setEnabled(false);
spawnButton->setEnabled(false);
launchButton->setEnabled(false);
return;
}
refreshDevices();
}
FridaTransport FridaConnectDialog::transport() const
{
return static_cast<FridaTransport>(transportGroup->checkedId());
}
FridaListKind FridaConnectDialog::listKind() const
{
return static_cast<FridaListKind>(kindGroup->checkedId());
}
QString FridaConnectDialog::deviceId() const
{
if (transport() == FridaTransport::Remote) {
const QString host = remoteHost->text().trimmed();
if (!host.isEmpty()) {
return host;
}
}
return devices->currentData().toString();
}
QString FridaConnectDialog::deviceLabel() const
{
if (transport() == FridaTransport::Remote && !remoteHost->text().trimmed().isEmpty()) {
return remoteHost->text().trimmed();
}
const QString text = devices->currentText();
return text.isEmpty() ? deviceId() : text;
}
FridaTargetInfo FridaConnectDialog::selectedTarget() const
{
FridaTargetInfo target;
QTreeWidgetItem *item = targets->currentItem();
if (!item) {
return target;
}
target.name = item->text(0);
target.identifier = item->text(1);
target.pid = item->text(2);
return target;
}
void FridaConnectDialog::refreshDevices()
{
if (!session->checkAvailable()) {
return;
}
status->setText(tr("Listing devices..."));
const int generation = ++probeGeneration;
session->probe(FridaUri::listDevices(), [this, generation](const QString &output) {
if (generation != probeGeneration) {
return;
}
deviceCache = FridaBackend::parseDevices(output);
applyDevices(deviceCache);
if (deviceCache.isEmpty()) {
status->setText(
output.trimmed().isEmpty() ? tr("No devices found.") : output.trimmed());
}
refreshTargets();
});
}
void FridaConnectDialog::applyDevices(const QList<FridaDeviceInfo> &devices)
{
const QString previous = deviceId();
this->devices->blockSignals(true);
this->devices->clear();
const QString want = transport() == FridaTransport::Local
? QStringLiteral("local")
: (transport() == FridaTransport::Usb ? QStringLiteral("usb")
: QStringLiteral("remote"));
for (const FridaDeviceInfo &device : devices) {
if (device.type != want) {
continue;
}
const QString label
= device.name.isEmpty()
? device.id
: device.name + QStringLiteral(" (") + device.id + QLatin1Char(')');
this->devices->addItem(label, device.id);
}
const int index = this->devices->findData(previous);
if (index >= 0) {
this->devices->setCurrentIndex(index);
}
this->devices->blockSignals(false);
}
void FridaConnectDialog::refreshTargets()
{
if (!session->checkAvailable() || session->busy()) {
return;
}
status->setText(tr("Listing targets..."));
targets->clear();
const int generation = ++probeGeneration;
const QString uri = FridaUri::listTargets(transport(), listKind(), deviceId());
session->probe(uri, [this, generation](const QString &output) {
if (generation != probeGeneration) {
return;
}
const QList<FridaTargetInfo> parsed = FridaBackend::parseTargets(output, listKind());
applyTargets(parsed);
if (parsed.isEmpty()) {
status->setText(
output.trimmed().isEmpty() ? tr("No targets found.") : output.trimmed());
} else {
status->setText(tr("%1 targets").arg(parsed.size()));
}
filterTargets(search->text());
});
}
void FridaConnectDialog::applyTargets(const QList<FridaTargetInfo> &targets)
{
this->targets->clear();
for (const FridaTargetInfo &target : targets) {
auto *item = new QTreeWidgetItem(this->targets);
item->setText(0, target.name);
item->setText(1, target.identifier);
item->setText(2, target.pid);
}
}
void FridaConnectDialog::filterTargets(const QString &text)
{
const QString needle = text.trimmed();
for (int row = 0; row < targets->topLevelItemCount(); ++row) {
QTreeWidgetItem *item = targets->topLevelItem(row);
const bool match = needle.isEmpty() || item->text(0).contains(needle, Qt::CaseInsensitive)
|| item->text(1).contains(needle, Qt::CaseInsensitive)
|| item->text(2).contains(needle, Qt::CaseInsensitive);
item->setHidden(!match);
}
}
void FridaConnectDialog::choose(FridaAction action)
{
const FridaTargetInfo target = selectedTarget();
QString id;
if (action == FridaAction::Attach) {
bool pidOk = false;
const int pid = target.pid.toInt(&pidOk);
if (pidOk && pid > 0) {
id = target.pid;
}
}
if (id.isEmpty()) {
id = !target.identifier.isEmpty() ? target.identifier : target.name;
}
if (!FridaUri::targetIsSafe(id)) {
QMessageBox::warning(
this,
tr("Connect to target"),
tr("Select an application or process."));
return;
}
if (transport() == FridaTransport::Remote && deviceId().isEmpty()) {
QMessageBox::warning(this, tr("Connect to target"), tr("Enter a remote host:port."));
return;
}
const QString uri = FridaUri::target(transport(), action, deviceId(), id);
QString transportName = tr("local");
if (transport() == FridaTransport::Usb) {
transportName = tr("usb");
} else if (transport() == FridaTransport::Remote) {
transportName = tr("remote");
}
session->openTarget(uri, action == FridaAction::Spawn, deviceLabel(), transportName);
accept();
}
-51
View File
@@ -1,51 +0,0 @@
#pragma once
#include "FridaTypes.h"
#include <QDialog>
#include <QList>
class FridaSession;
class QButtonGroup;
class QComboBox;
class QLineEdit;
class QLabel;
class QTreeWidget;
class QPushButton;
class FridaConnectDialog : public QDialog
{
Q_OBJECT
public:
explicit FridaConnectDialog(FridaSession *session, QWidget *parent = nullptr);
private:
void refreshDevices();
void refreshTargets();
void applyDevices(const QList<FridaDeviceInfo> &devices);
void applyTargets(const QList<FridaTargetInfo> &targets);
void filterTargets(const QString &text);
void choose(FridaAction action);
FridaTransport transport() const;
FridaListKind listKind() const;
QString deviceId() const;
QString deviceLabel() const;
FridaTargetInfo selectedTarget() const;
FridaSession *session = nullptr;
QButtonGroup *transportGroup = nullptr;
QButtonGroup *kindGroup = nullptr;
QPushButton *localButton = nullptr;
QPushButton *usbButton = nullptr;
QPushButton *remoteButton = nullptr;
QPushButton *appsButton = nullptr;
QPushButton *processButton = nullptr;
QLineEdit *remoteHost = nullptr;
QComboBox *devices = nullptr;
QLineEdit *search = nullptr;
QTreeWidget *targets = nullptr;
QLabel *status = nullptr;
QList<FridaDeviceInfo> deviceCache;
int probeGeneration = 0;
};
-511
View File
@@ -1,511 +0,0 @@
#include "FridaPanels.h"
#include "FridaSession.h"
#include "FridaWidget.h"
#include "core/Iaito.h"
#include <QApplication>
#include <QClipboard>
#include <QComboBox>
#include <QFileInfo>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QJsonObject>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QRegularExpression>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QTreeView>
#include <QVBoxLayout>
namespace {
QString formatSize(quint64 size)
{
if (size >= (1024ull * 1024ull * 1024ull)) {
return QString::number(size / (1024.0 * 1024.0 * 1024.0), 'f', 1) + QStringLiteral(" GB");
}
if (size >= (1024ull * 1024ull)) {
return QString::number(size / (1024.0 * 1024.0), 'f', 1) + QStringLiteral(" MB");
}
if (size >= 1024ull) {
return QString::number(size / 1024.0, 'f', 1) + QStringLiteral(" KB");
}
return QString::number(size) + QStringLiteral(" B");
}
QStandardItem *textItem(const QString &text)
{
auto *item = new QStandardItem(text);
item->setEditable(false);
return item;
}
QStandardItem *addressItem(const QString &text, quint64 runtime)
{
auto *item = textItem(text);
item->setData(QVariant::fromValue(runtime), Qt::UserRole);
return item;
}
quint64 roleAddress(const QModelIndex &index)
{
return index.data(Qt::UserRole).toULongLong();
}
void host(QWidget *page, QWidget *child)
{
auto *layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(child);
}
void loadJava(FridaSession *session, QPlainTextEdit *view)
{
if (!session->isAttached()) {
view->setPlainText(QObject::tr("Not attached."));
return;
}
view->setPlainText(QObject::tr("Loading..."));
const QString source = QStringLiteral(
"if (!Java.available) { console.log('Java runtime is not available'); }\n"
"else Java.perform(function () {\n"
" var n = 0;\n"
" Java.enumerateLoadedClasses({\n"
" onMatch: function (name) { if (n++ < 2000) console.log(name); },\n"
" onComplete: function () { if (n >= 2000) console.log('... truncated'); }\n"
" });\n"
"});\n");
const QString path = session->writeScript(source);
if (path.isEmpty()) {
view->setPlainText(QObject::tr("Could not write the Java script."));
return;
}
session->command(QStringLiteral(". ") + path, [view](const QString &output) {
view->setPlainText(output);
});
}
void showCommand(FridaSession *session, const QString &command, QPlainTextEdit *view)
{
if (!session->isAttached()) {
view->setPlainText(QObject::tr("Not attached."));
return;
}
view->setPlainText(QObject::tr("Loading..."));
session->command(command, [view](const QString &output) {
QString text = output;
if (text.size() > 1000000) {
text.truncate(1000000);
text += QObject::tr("\n... truncated");
}
view->setPlainText(text);
});
}
class FridaOutputDock : public QWidget
{
public:
FridaOutputDock(
const QString &title,
const QString &objectName,
FridaSession *fridaSession,
const std::function<void(QPlainTextEdit *)> &load)
: QWidget(nullptr)
, session(fridaSession)
{
setObjectName(objectName);
setWindowTitle(title);
auto *root = new QWidget(this);
auto *layout = new QVBoxLayout(root);
auto *refresh = new QPushButton(tr("Refresh"), root);
view = new QPlainTextEdit(root);
view->setReadOnly(true);
layout->addWidget(refresh);
layout->addWidget(view);
host(this, root);
connect(refresh, &QPushButton::clicked, this, [this, load]() { load(view); });
connect(session, &FridaSession::changed, this, [this]() {
if (!session->isAttached()) {
view->clear();
}
});
}
private:
FridaSession *session = nullptr;
QPlainTextEdit *view = nullptr;
};
class FridaModulesDock : public QWidget
{
public:
FridaModulesDock(FridaSession *fridaSession)
: QWidget(nullptr)
, session(fridaSession)
{
setObjectName(QStringLiteral("FridaModulesDock"));
setWindowTitle(tr("Modules"));
model = new QStandardItemModel(this);
model->setHorizontalHeaderLabels({tr("Name"), tr("Base"), tr("Size"), tr("Path")});
proxy = new QSortFilterProxyModel(this);
proxy->setSourceModel(model);
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxy->setFilterKeyColumn(-1);
auto *root = new QWidget(this);
auto *layout = new QVBoxLayout(root);
auto *row = new QHBoxLayout();
auto *filter = new QLineEdit(root);
filter->setPlaceholderText(tr("Filter"));
auto *mode = new QComboBox(root);
mode->addItem(tr("Runtime"), int(FridaAddressMode::Runtime));
mode->addItem(tr("Static"), int(FridaAddressMode::Static));
mode->addItem(tr("Module + offset"), int(FridaAddressMode::ModuleOffset));
row->addWidget(filter, 1);
row->addWidget(mode);
view = new QTreeView(root);
view->setModel(proxy);
view->setRootIsDecorated(false);
view->setAlternatingRowColors(true);
view->setContextMenuPolicy(Qt::CustomContextMenu);
view->header()->setStretchLastSection(true);
view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
layout->addLayout(row);
layout->addWidget(view);
host(this, root);
connect(
filter, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterFixedString);
connect(mode, qOverload<int>(&QComboBox::currentIndexChanged), this, [this, mode](int index) {
session->setAddressMode(static_cast<FridaAddressMode>(mode->itemData(index).toInt()));
});
connect(session, &FridaSession::changed, this, [this]() { refill(); });
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
fridaSeekRuntime(session, roleAddress(index.sibling(index.row(), 0)));
});
connect(view, &QTreeView::customContextMenuRequested, this, [this](const QPoint &pos) {
const QModelIndex index = view->indexAt(pos);
if (!index.isValid()) {
return;
}
const QModelIndex nameIndex = index.sibling(index.row(), 0);
const quint64 runtime = roleAddress(nameIndex);
const QString name = nameIndex.data().toString();
QMenu menu(this);
menu.addAction(tr("Seek static address"), this, [this, runtime]() {
fridaSeekRuntime(session, runtime);
});
menu.addAction(tr("Copy runtime address"), this, [runtime]() {
QApplication::clipboard()->setText(RAddressString(runtime));
});
menu.addAction(tr("Live hexdump"), this, [this, runtime]() {
fridaShowLiveHex(this, session, runtime);
});
menu.addAction(tr("Show exports"), this, [this, name]() {
fridaShowExports(this, session, name);
});
menu.addAction(tr("Import symbols into analysis"), this, [this, name]() {
session->importExports(name);
});
menu.exec(view->viewport()->mapToGlobal(pos));
});
refill();
}
private:
void refill()
{
model->removeRows(0, model->rowCount());
for (const FridaModuleInfo &module : session->modules()) {
auto *name = textItem(module.name);
name->setData(QVariant::fromValue(module.base), Qt::UserRole);
name->setToolTip(session->describe(module.base));
model->appendRow(
{name,
addressItem(session->formatRuntime(module.base), module.base),
textItem(formatSize(module.size)),
textItem(module.path)});
}
}
FridaSession *session = nullptr;
QStandardItemModel *model = nullptr;
QSortFilterProxyModel *proxy = nullptr;
QTreeView *view = nullptr;
};
class FridaMapsDock : public QWidget
{
public:
FridaMapsDock(FridaSession *fridaSession)
: QWidget(nullptr)
, session(fridaSession)
{
setObjectName(QStringLiteral("FridaMemoryMapDock"));
setWindowTitle(tr("Memory Map"));
model = new QStandardItemModel(this);
model->setHorizontalHeaderLabels({tr("Start"), tr("End"), tr("Perm"), tr("Module")});
proxy = new QSortFilterProxyModel(this);
proxy->setSourceModel(model);
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxy->setFilterKeyColumn(-1);
auto *root = new QWidget(this);
auto *layout = new QVBoxLayout(root);
auto *filter = new QLineEdit(root);
filter->setPlaceholderText(tr("Filter"));
view = new QTreeView(root);
view->setModel(proxy);
view->setRootIsDecorated(false);
view->setAlternatingRowColors(true);
view->setContextMenuPolicy(Qt::CustomContextMenu);
view->header()->setStretchLastSection(true);
layout->addWidget(filter);
layout->addWidget(view);
host(this, root);
connect(
filter, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterFixedString);
connect(session, &FridaSession::changed, this, [this]() { refill(); });
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
fridaSeekRuntime(session, roleAddress(index.sibling(index.row(), 0)));
});
connect(view, &QTreeView::customContextMenuRequested, this, [this](const QPoint &pos) {
const QModelIndex index = view->indexAt(pos);
if (!index.isValid()) {
return;
}
const quint64 runtime = roleAddress(index.sibling(index.row(), 0));
QMenu menu(this);
menu.addAction(tr("Seek static address"), this, [this, runtime]() {
fridaSeekRuntime(session, runtime);
});
menu.addAction(tr("Live hexdump"), this, [this, runtime]() {
fridaShowLiveHex(this, session, runtime);
});
menu.addAction(tr("Copy address"), this, [runtime]() {
QApplication::clipboard()->setText(RAddressString(runtime));
});
menu.exec(view->viewport()->mapToGlobal(pos));
});
refill();
}
private:
void refill()
{
model->removeRows(0, model->rowCount());
for (const FridaMapInfo &map : session->maps()) {
const QString module = QFileInfo(map.path).fileName();
auto *start = addressItem(session->formatRuntime(map.start), map.start);
start->setToolTip(session->describe(map.start));
auto *name = textItem(module.isEmpty() ? map.path : module);
name->setToolTip(map.path);
model->appendRow(
{start,
addressItem(session->formatRuntime(map.end), map.end),
textItem(map.permission),
name});
}
}
FridaSession *session = nullptr;
QStandardItemModel *model = nullptr;
QSortFilterProxyModel *proxy = nullptr;
QTreeView *view = nullptr;
};
class FridaHooksDock : public QWidget
{
public:
FridaHooksDock(FridaSession *fridaSession)
: QWidget(nullptr)
, session(fridaSession)
{
setObjectName(QStringLiteral("FridaHooksDock"));
setWindowTitle(tr("Hooks"));
model = new QStandardItemModel(this);
model->setHorizontalHeaderLabels({tr("Address"), tr("Summary")});
view = new QTreeView(this);
view->setModel(model);
view->setRootIsDecorated(false);
view->setAlternatingRowColors(true);
view->header()->setStretchLastSection(true);
host(this, view);
connect(session, &FridaSession::changed, this, [this]() { refill(); });
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
fridaSeekRuntime(session, roleAddress(index.sibling(index.row(), 0)));
});
refill();
}
private:
void refill()
{
model->removeRows(0, model->rowCount());
for (const FridaHookInfo &hook : session->hooks()) {
model->appendRow(
{addressItem(session->formatRuntime(hook.address), hook.address),
textItem(hook.summary)});
}
}
FridaSession *session = nullptr;
QStandardItemModel *model = nullptr;
QTreeView *view = nullptr;
};
class FridaTracesDock : public QWidget
{
public:
FridaTracesDock(FridaSession *fridaSession)
: QWidget(nullptr)
, session(fridaSession)
{
setObjectName(QStringLiteral("FridaTracesDock"));
setWindowTitle(tr("Traces"));
model = new QStandardItemModel(this);
model->setHorizontalHeaderLabels({tr("Function"), tr("Hits")});
view = new QTreeView(this);
view->setModel(model);
view->setRootIsDecorated(false);
view->setAlternatingRowColors(true);
view->header()->setStretchLastSection(true);
view->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
auto *root = new QWidget(this);
auto *layout = new QVBoxLayout(root);
auto *refresh = new QPushButton(tr("Refresh"), root);
layout->addWidget(refresh);
layout->addWidget(view);
host(this, root);
connect(refresh, &QPushButton::clicked, this, [this]() { reload(); });
connect(session, &FridaSession::changed, this, [this]() {
if (!session->isAttached()) {
model->removeRows(0, model->rowCount());
}
});
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
const QString line = index.sibling(index.row(), 0).data().toString();
static const QRegularExpression addressRe(QStringLiteral("0x[0-9a-fA-F]+"));
const QRegularExpressionMatch match = addressRe.match(line);
if (!match.hasMatch()) {
return;
}
bool ok = false;
const quint64 addr = match.captured(0).toULongLong(&ok, 16);
if (ok) {
fridaSeekRuntime(session, addr);
}
});
}
private:
void reload()
{
if (!session->isAttached()) {
model->removeRows(0, model->rowCount());
return;
}
session->command(QStringLiteral("dt"), [this](const QString &output) {
model->removeRows(0, model->rowCount());
for (const QString &raw : output.split(QLatin1Char('\n'))) {
const QString line = raw.trimmed();
if (line.isEmpty()) {
continue;
}
QString function = line;
QString hits;
const int space = line.lastIndexOf(QLatin1Char(' '));
if (space > 0) {
bool ok = false;
line.mid(space + 1).toULongLong(&ok);
if (ok) {
function = line.left(space).trimmed();
hits = line.mid(space + 1);
}
}
model->appendRow({textItem(function), textItem(hits)});
}
});
}
FridaSession *session = nullptr;
QStandardItemModel *model = nullptr;
QTreeView *view = nullptr;
};
class FridaRuntimePage : public QWidget
{
public:
explicit FridaRuntimePage(FridaSession *fridaSession)
: QWidget(nullptr)
, session(fridaSession)
{
setWindowTitle(tr("ObjC/Java Runtime"));
auto *layout = new QVBoxLayout(this);
auto *row = new QHBoxLayout();
kind = new QComboBox(this);
kind->addItem(tr("Objective-C"));
kind->addItem(tr("Java"));
auto *refresh = new QPushButton(tr("Refresh"), this);
view = new QPlainTextEdit(this);
view->setReadOnly(true);
row->addWidget(kind);
row->addWidget(refresh);
row->addStretch();
layout->addLayout(row);
layout->addWidget(view);
connect(refresh, &QPushButton::clicked, this, [this]() { reload(); });
connect(session, &FridaSession::changed, this, [this]() {
if (!session->isAttached()) {
view->clear();
}
});
}
private:
void reload()
{
if (kind->currentIndex() == 0) {
showCommand(session, QStringLiteral("ic"), view);
} else {
loadJava(session, view);
}
}
FridaSession *session = nullptr;
QComboBox *kind = nullptr;
QPlainTextEdit *view = nullptr;
};
} // namespace
QList<QWidget *> createFridaPanels(FridaSession *session)
{
const auto command = [session](const QString &name) {
return [session, name](QPlainTextEdit *view) { showCommand(session, name, view); };
};
QList<QWidget *> panels;
panels << new FridaModulesDock(session);
panels << new FridaOutputDock(
QObject::tr("Threads"),
QStringLiteral("FridaThreadsPage"),
session,
command(QStringLiteral("dpt")));
panels << new FridaMapsDock(session);
panels << new FridaOutputDock(
QObject::tr("Breakpoints"),
QStringLiteral("FridaBreakpointsPage"),
session,
command(QStringLiteral("db")));
panels << new FridaRuntimePage(session);
panels << new FridaHooksDock(session);
panels << new FridaTracesDock(session);
return panels;
}
-8
View File
@@ -1,8 +0,0 @@
#pragma once
#include <QList>
class FridaSession;
class QWidget;
QList<QWidget *> createFridaPanels(FridaSession *session);
-501
View File
@@ -1,501 +0,0 @@
#include "FridaSession.h"
#include "FridaUri.h"
#include "core/Iaito.h"
#include <QDir>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
#include <QTemporaryFile>
FridaSession::FridaSession(QObject *parent)
: QObject(parent)
{}
bool FridaSession::checkAvailable()
{
available_ = backend_.pluginAvailable();
return available_;
}
void FridaSession::setAddressMode(FridaAddressMode mode)
{
if (addressMode_ == mode) {
return;
}
addressMode_ = mode;
emit changed();
}
void FridaSession::setState(FridaSessionState state)
{
state_ = state;
emit changed();
}
void FridaSession::clearTarget()
{
suspended_ = false;
process_ = FridaProcessInfo();
modules_.clear();
maps_.clear();
hooks_.clear();
staticBase_ = 0;
runtimeBase_ = 0;
matchedSize_ = 0;
slide_ = 0;
uri_.clear();
deviceLabel_.clear();
transportLabel_.clear();
}
bool FridaSession::containsStatic(quint64 addr) const
{
return matchedSize_ > 0 && addr >= staticBase_ && addr - staticBase_ < matchedSize_;
}
bool FridaSession::containsRuntime(quint64 addr) const
{
return matchedSize_ > 0 && addr >= runtimeBase_ && addr - runtimeBase_ < matchedSize_;
}
quint64 FridaSession::toRuntime(quint64 staticVa) const
{
if (!containsStatic(staticVa)) {
return RVA_INVALID;
}
return runtimeBase_ + (staticVa - staticBase_);
}
quint64 FridaSession::toStatic(quint64 runtimeVa) const
{
if (!containsRuntime(runtimeVa)) {
return RVA_INVALID;
}
return staticBase_ + (runtimeVa - runtimeBase_);
}
QString FridaSession::formatRuntime(quint64 runtimeVa) const
{
if (addressMode_ == FridaAddressMode::ModuleOffset) {
for (const FridaModuleInfo &module : modules_) {
if (module.size > 0 && runtimeVa >= module.base
&& runtimeVa - module.base < module.size) {
return module.name + QStringLiteral(" + ")
+ RAddressString(runtimeVa - module.base);
}
}
}
if (addressMode_ == FridaAddressMode::Static) {
const quint64 staticVa = toStatic(runtimeVa);
if (staticVa != RVA_INVALID) {
return RAddressString(staticVa);
}
}
return RAddressString(runtimeVa);
}
QString FridaSession::describe(quint64 addr) const
{
quint64 staticVa = addr;
quint64 runtimeVa = addr;
if (containsStatic(addr)) {
runtimeVa = toRuntime(addr);
} else if (containsRuntime(addr)) {
staticVa = toStatic(addr);
} else {
return tr("No static mapping for %1").arg(RAddressString(addr));
}
QString moduleText = RAddressString(runtimeVa);
for (const FridaModuleInfo &module : modules_) {
if (module.size > 0 && runtimeVa >= module.base && runtimeVa - module.base < module.size) {
moduleText = module.name + QStringLiteral(" + ")
+ RAddressString(runtimeVa - module.base);
break;
}
}
return tr("static %1\nruntime %2\n%3\nslide %4")
.arg(
RAddressString(staticVa),
RAddressString(runtimeVa),
moduleText,
RAddressString(slide_));
}
void FridaSession::probe(const QString &uri, const std::function<void(QString)> &done)
{
if (!checkAvailable()) {
if (done) {
done(tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
}
return;
}
if (backend_.busy()) {
if (done) {
done(tr("r2frida is busy."));
}
return;
}
const QSet<int> before = backend_.openFds();
// r2frida prints the table with r_cons_gprintf, which follows the
// thread-local console. A background task drains a child console, so the
// table never comes back. Run here, on the console cmd() captures.
// `of` still performs the plugin open, and stays quiet when it returns
// no descriptor. `o` would log "Cannot open file" for that.
const QString savedColor = Core()->getConfig(QStringLiteral("scr.color"));
const QString batch = QStringLiteral("e scr.color=0\nof ") + FridaUri::quote(uri)
+ QStringLiteral("\ne scr.color=") + savedColor + QLatin1Char('\n');
RCons *previous = r_cons_singleton();
RCons *cons = r_core_get_cons(Core()->core_);
if (cons) {
r_cons_global(cons);
}
const QString output = Core()->cmd(batch);
if (previous && previous != cons) {
r_cons_global(previous);
}
const int leaked = backend_.findNewFridaFd(before);
backend_.closeFd(leaked);
QStringList kept;
for (const QString &line : output.split(QLatin1Char('\n'))) {
if (line.contains(QStringLiteral("Cannot open"))) {
continue;
}
kept.append(line);
}
if (done) {
done(kept.join(QLatin1Char('\n')));
}
}
void FridaSession::openTarget(
const QString &uri, bool suspended, const QString &deviceLabel, const QString &transportLabel)
{
if (!checkAvailable()) {
lastError_ = tr(
"r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito.");
setState(FridaSessionState::Error);
emit errorMessage(lastError_);
return;
}
if (backend_.busy()) {
emit errorMessage(tr("r2frida is busy."));
return;
}
if (backend_.fridaFd() >= 0) {
detach();
}
const int generation = ++generation_;
const QSet<int> before = backend_.openFds();
backend_.setHostFd(backend_.currentFd());
deviceLabel_ = deviceLabel;
transportLabel_ = transportLabel;
uri_ = uri;
suspended_ = suspended;
setState(FridaSessionState::Connecting);
emit consoleMessage(tr("opening %1").arg(uri));
const QString batch = QStringLiteral("of ") + FridaUri::quote(uri);
backend_.runBatch(batch, [this, before, generation, suspended](const QString &output) {
const int fd = backend_.findNewFridaFd(before);
if (generation != generation_) {
backend_.closeFd(fd);
return;
}
if (fd < 0) {
lastError_ = output.trimmed().isEmpty()
? tr("Could not open the r2frida target.")
: output.trimmed();
clearTarget();
setState(FridaSessionState::Error);
emit errorMessage(lastError_);
emit consoleMessage(lastError_);
return;
}
backend_.setFridaFd(fd);
const int host = backend_.hostFd();
if (host > 0 && backend_.currentFd() != host) {
Core()->cmd(QStringLiteral("o=") + QString::number(host));
}
suspended_ = suspended;
readSnapshot();
setState(FridaSessionState::Attached);
emit consoleMessage(
tr("attached pid %1 (%2)").arg(process_.pid).arg(process_.name));
});
}
void FridaSession::detach()
{
++generation_;
const int fd = backend_.fridaFd();
const int host = backend_.hostFd();
if (host > 0 && backend_.currentFd() == fd) {
Core()->cmd(QStringLiteral("o=") + QString::number(host));
}
backend_.closeFd(fd);
clearTarget();
lastError_.clear();
setState(FridaSessionState::Disconnected);
emit consoleMessage(tr("detached"));
}
void FridaSession::resume()
{
if (!isAttached()) {
return;
}
command(QStringLiteral("dc"), [this](const QString &output) {
suspended_ = false;
emit consoleMessage(output.trimmed().isEmpty() ? tr("resumed") : output.trimmed());
emit changed();
});
}
void FridaSession::command(const QString &fridaCmd, const std::function<void(QString)> &done)
{
if (!isAttached()) {
if (done) {
done(tr("Not attached."));
}
return;
}
if (backend_.busy()) {
if (done) {
done(tr("r2frida is busy."));
}
return;
}
const int generation = generation_;
const int fd = backend_.fridaFd();
const int back = backend_.currentFd() == fd ? backend_.hostFd() : backend_.currentFd();
QString batch = QStringLiteral("o=") + QString::number(fd) + QStringLiteral("\n:") + fridaCmd
+ QLatin1Char('\n');
if (back > 0 && back != fd) {
batch += QStringLiteral("o=") + QString::number(back) + QLatin1Char('\n');
}
backend_.runBatch(batch, [this, generation, done](const QString &output) {
if (generation != generation_) {
return;
}
if (done) {
done(output);
}
});
}
QByteArray FridaSession::readBytes(quint64 runtime, int length)
{
if (!isAttached() || length <= 0 || backend_.busy()) {
return {};
}
const int fd = backend_.fridaFd();
const int current = backend_.currentFd();
const int back = current == fd ? backend_.hostFd() : current;
const QString savedColor = Core()->getConfig(QStringLiteral("scr.color"));
QString batch = QStringLiteral("e scr.color=0\no=") + QString::number(fd);
batch += QStringLiteral("\np8 ") + QString::number(length);
batch += QStringLiteral(" @ ") + RAddressString(runtime) + QLatin1Char('\n');
if (back > 0 && back != fd) {
batch += QStringLiteral("o=") + QString::number(back) + QLatin1Char('\n');
}
batch += QStringLiteral("e scr.color=") + savedColor + QLatin1Char('\n');
RCons *previous = r_cons_singleton();
RCons *cons = r_core_get_cons(Core()->core_);
if (cons) {
r_cons_global(cons);
}
const QString output = Core()->cmd(batch);
if (previous && previous != cons) {
r_cons_global(previous);
}
if (output.contains(QStringLiteral("ERROR")) || output.contains(QStringLiteral("Cannot"))) {
return {};
}
QString hex;
hex.reserve(output.size());
for (const QChar ch : output) {
if (ch.isDigit() || (ch >= QLatin1Char('a') && ch <= QLatin1Char('f'))
|| (ch >= QLatin1Char('A') && ch <= QLatin1Char('F'))) {
hex.append(ch);
}
}
return QByteArray::fromHex(hex.toLatin1());
}
void FridaSession::addHook(quint64 address, const QString &summary)
{
FridaHookInfo hook;
hook.address = address;
hook.summary = summary;
hooks_.append(hook);
emit changed();
}
void FridaSession::note(const QString &text)
{
emit consoleMessage(text);
}
QString FridaSession::writeScript(const QString &source)
{
QString dir = QDir::tempPath();
if (dir.contains(QLatin1Char(' '))) {
dir = QStringLiteral("/tmp");
}
auto *file = new QTemporaryFile(dir + QStringLiteral("/iaito-frida-XXXXXX.js"), this);
file->setAutoRemove(true);
if (!file->open()) {
delete file;
return QString();
}
file->write(source.toUtf8());
file->flush();
return file->fileName();
}
void FridaSession::importExports(const QString &moduleName)
{
if (!isAttached() || moduleName.isEmpty()) {
return;
}
const QString commandText = QStringLiteral("iEj ") + moduleName;
command(commandText, [this, moduleName](const QString &output) {
const QJsonDocument doc = FridaBackend::parseJsonLoose(output);
const QJsonArray exports = doc.array();
QString script = QStringLiteral("fs frida\n");
int imported = 0;
const QString prefix = QStringLiteral("frida.") + moduleName + QLatin1Char('.');
for (const QJsonValue value : exports) {
const QJsonObject exp = value.toObject();
const quint64 runtime
= FridaBackend::parseAddress(exp.value(QStringLiteral("address")));
const quint64 staticVa = toStatic(runtime);
if (staticVa == RVA_INVALID) {
continue;
}
QString name = exp.value(QStringLiteral("name")).toString();
name.replace(QRegularExpression(QStringLiteral("[^A-Za-z0-9._]")), QStringLiteral("_"));
if (name.isEmpty()) {
continue;
}
script += QStringLiteral("f ");
script += prefix;
script += name;
script += QStringLiteral(" = ");
script += RAddressString(staticVa);
script += QLatin1Char('\n');
++imported;
if (imported >= 20000) {
break;
}
}
script += QStringLiteral("fs *\n");
if (imported == 0) {
emit errorMessage(tr("No exports from %1 map into the static file.").arg(moduleName));
return;
}
Core()->cmd(script);
Core()->triggerRefreshAll();
emit consoleMessage(tr("imported %1 symbols from %2").arg(imported).arg(moduleName));
});
}
void FridaSession::readSnapshot()
{
const QString blob = backend_.fridaCommand(
QStringLiteral("ij\n?e ---IAITO-FRIDA---\n:ilj\n?e ---IAITO-FRIDA---\n:dmj"));
QStringList parts = blob.split(QStringLiteral("---IAITO-FRIDA---"));
if (parts.size() < 3) {
parts = QStringList{blob, QString(), QString()};
}
applyInfo(FridaBackend::parseJsonLoose(parts.value(0)).object());
applyModules(FridaBackend::parseJsonLoose(parts.value(1)).array());
applyMaps(FridaBackend::parseJsonLoose(parts.value(2)).array());
}
void FridaSession::applyInfo(const QJsonObject &info)
{
process_ = FridaProcessInfo();
process_.pid = info.value(QStringLiteral("pid")).toInt(-1);
process_.arch = info.value(QStringLiteral("arch")).toString();
process_.bits = info.value(QStringLiteral("bits")).toInt();
process_.os = info.value(QStringLiteral("os")).toString();
process_.moduleName = info.value(QStringLiteral("modulename")).toString();
process_.moduleBase = FridaBackend::parseAddress(info.value(QStringLiteral("modulebase")));
const QString app = info.value(QStringLiteral("appname")).toString();
const QString packageName = info.value(QStringLiteral("packageName")).toString();
const QString bundle = info.value(QStringLiteral("bundle")).toString();
process_.identifier = !bundle.isEmpty() ? bundle : packageName;
if (!app.isEmpty()) {
process_.name = app;
} else if (!packageName.isEmpty()) {
process_.name = packageName;
} else if (!bundle.isEmpty()) {
process_.name = bundle;
} else {
process_.name = process_.moduleName;
}
}
void FridaSession::applyModules(const QJsonArray &modules)
{
modules_.clear();
const QString opened = QFileInfo(Core()->getFilePath()).fileName();
int match = -1;
for (const QJsonValue value : modules) {
const QJsonObject object = value.toObject();
FridaModuleInfo module;
module.name = object.value(QStringLiteral("name")).toString();
module.path = object.value(QStringLiteral("path")).toString();
module.base = FridaBackend::parseAddress(object.value(QStringLiteral("base")));
module.size = static_cast<quint64>(object.value(QStringLiteral("size")).toDouble());
const QString fileName = QFileInfo(module.path).fileName();
const bool nameMatch = !opened.isEmpty() && module.name == opened;
const bool pathMatch = !opened.isEmpty() && fileName == opened;
modules_.append(module);
if (nameMatch || pathMatch) {
if (match < 0 || nameMatch) {
match = modules_.size() - 1;
}
}
}
staticBase_ = 0;
runtimeBase_ = 0;
matchedSize_ = 0;
slide_ = 0;
if (match < 0) {
return;
}
const QJsonObject bin
= Core()->cmdj(QStringLiteral("ij")).object().value(QStringLiteral("bin")).toObject();
staticBase_ = bin.value(QStringLiteral("baddr")).toVariant().toULongLong();
FridaModuleInfo &module = modules_[match];
module.matched = true;
runtimeBase_ = module.base;
matchedSize_ = module.size;
slide_ = runtimeBase_ - staticBase_;
}
void FridaSession::applyMaps(const QJsonArray &maps)
{
maps_.clear();
for (const QJsonValue value : maps) {
const QJsonObject object = value.toObject();
FridaMapInfo map;
map.start = FridaBackend::parseAddress(object.value(QStringLiteral("base")));
map.end = map.start + static_cast<quint64>(object.value(QStringLiteral("size")).toDouble());
map.permission = object.value(QStringLiteral("protection")).toString();
const QJsonValue file = object.value(QStringLiteral("file"));
if (file.isObject()) {
map.path = file.toObject().value(QStringLiteral("path")).toString();
} else if (file.isString()) {
map.path = file.toString();
}
maps_.append(map);
}
}
-95
View File
@@ -1,95 +0,0 @@
#pragma once
#include "FridaBackend.h"
#include "FridaTypes.h"
#include <QByteArray>
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <functional>
class FridaSession : public QObject
{
Q_OBJECT
public:
explicit FridaSession(QObject *parent = nullptr);
bool checkAvailable();
bool available() const { return available_; }
FridaSessionState state() const { return state_; }
bool isAttached() const { return state_ == FridaSessionState::Attached && fridaFd() >= 0; }
bool isSuspended() const { return suspended_; }
bool busy() const { return backend_.busy(); }
QString lastError() const { return lastError_; }
QString deviceLabel() const { return deviceLabel_; }
QString transportLabel() const { return transportLabel_; }
QString uri() const { return uri_; }
const FridaProcessInfo &process() const { return process_; }
const QList<FridaModuleInfo> &modules() const { return modules_; }
const QList<FridaMapInfo> &maps() const { return maps_; }
const QList<FridaHookInfo> &hooks() const { return hooks_; }
quint64 staticBase() const { return staticBase_; }
quint64 slide() const { return slide_; }
bool hasStaticMapping() const { return matchedSize_ > 0; }
FridaAddressMode addressMode() const { return addressMode_; }
void setAddressMode(FridaAddressMode mode);
quint64 toRuntime(quint64 staticVa) const;
quint64 toStatic(quint64 runtimeVa) const;
bool containsStatic(quint64 addr) const;
bool containsRuntime(quint64 addr) const;
QString formatRuntime(quint64 runtimeVa) const;
QString describe(quint64 addr) const;
void probe(const QString &uri, const std::function<void(QString)> &done);
void openTarget(
const QString &uri,
bool suspended,
const QString &deviceLabel,
const QString &transportLabel);
void detach();
void resume();
void command(const QString &fridaCmd, const std::function<void(QString)> &done);
QByteArray readBytes(quint64 runtime, int length);
void importExports(const QString &moduleName);
QString writeScript(const QString &source);
void note(const QString &text);
void addHook(quint64 address, const QString &summary);
signals:
void changed();
void errorMessage(const QString &message);
void consoleMessage(const QString &message);
private:
int fridaFd() const { return backend_.fridaFd(); }
void setState(FridaSessionState state);
void clearTarget();
void readSnapshot();
void applyInfo(const QJsonObject &info);
void applyModules(const QJsonArray &modules);
void applyMaps(const QJsonArray &maps);
FridaBackend backend_;
FridaSessionState state_ = FridaSessionState::Disconnected;
bool available_ = false;
bool suspended_ = false;
int generation_ = 0;
QString lastError_;
QString deviceLabel_;
QString transportLabel_;
QString uri_;
FridaProcessInfo process_;
QList<FridaModuleInfo> modules_;
QList<FridaMapInfo> maps_;
QList<FridaHookInfo> hooks_;
quint64 staticBase_ = 0;
quint64 runtimeBase_ = 0;
quint64 matchedSize_ = 0;
quint64 slide_ = 0;
FridaAddressMode addressMode_ = FridaAddressMode::Runtime;
};
-63
View File
@@ -1,63 +0,0 @@
#pragma once
#include <QString>
#include <QtGlobal>
enum class FridaTransport { Local, Usb, Remote };
enum class FridaAction { Attach, Spawn, Launch };
enum class FridaListKind { Devices, Processes, Applications };
enum class FridaAddressMode { Static, Runtime, ModuleOffset };
enum class FridaSessionState { Disconnected, Connecting, Attached, Error };
struct FridaDeviceInfo
{
QString id;
QString type;
QString name;
};
struct FridaTargetInfo
{
QString pid;
QString name;
QString identifier;
};
struct FridaModuleInfo
{
QString name;
QString path;
quint64 base = 0;
quint64 size = 0;
bool matched = false;
};
struct FridaMapInfo
{
quint64 start = 0;
quint64 end = 0;
QString permission;
QString path;
};
struct FridaProcessInfo
{
int pid = -1;
QString name;
QString identifier;
QString arch;
int bits = 0;
QString os;
QString moduleName;
quint64 moduleBase = 0;
};
struct FridaHookInfo
{
quint64 address = 0;
QString summary;
};
-89
View File
@@ -1,89 +0,0 @@
#include "FridaUri.h"
static QString actionName(FridaAction action)
{
switch (action) {
case FridaAction::Spawn:
return QStringLiteral("spawn");
case FridaAction::Launch:
return QStringLiteral("launch");
case FridaAction::Attach:
break;
}
return QStringLiteral("attach");
}
static QString linkName(FridaTransport transport)
{
switch (transport) {
case FridaTransport::Usb:
return QStringLiteral("usb");
case FridaTransport::Remote:
return QStringLiteral("remote");
case FridaTransport::Local:
break;
}
return QStringLiteral("local");
}
QString FridaUri::quote(const QString &text)
{
QString escaped = text;
escaped.replace(QLatin1Char('\''), QStringLiteral("'\\''"));
return QStringLiteral("'") + escaped + QStringLiteral("'");
}
bool FridaUri::targetIsSafe(const QString &targetId)
{
if (targetId.isEmpty()) {
return false;
}
const QString forbidden = QStringLiteral("/'\"`;|@\n\r");
for (const QChar ch : targetId) {
if (forbidden.contains(ch)) {
return false;
}
}
return true;
}
QString FridaUri::listDevices()
{
// Three components with an empty device id: r2frida dumps every device
// (local, usb, and remote), then fails the open on purpose.
return QStringLiteral("frida://list/usb/");
}
QString FridaUri::listTargets(
FridaTransport transport, FridaListKind kind, const QString &deviceId)
{
const QString action = kind == FridaListKind::Applications ? QStringLiteral("apps")
: QStringLiteral("list");
QString id = deviceId;
if (transport == FridaTransport::Usb && (id.isEmpty() || id == QStringLiteral("usb"))) {
id.clear();
}
if (transport == FridaTransport::Local) {
id.clear();
}
return QStringLiteral("frida://") + action + QStringLiteral("/") + linkName(transport)
+ QStringLiteral("/") + id + QStringLiteral("/");
}
QString FridaUri::target(
FridaTransport transport,
FridaAction action,
const QString &deviceId,
const QString &targetId)
{
const QString verb = actionName(action);
if (transport == FridaTransport::Local) {
return QStringLiteral("frida://") + verb + QStringLiteral("/") + targetId;
}
QString id = deviceId;
if (transport == FridaTransport::Usb && (id.isEmpty() || id == QStringLiteral("usb"))) {
id.clear();
}
return QStringLiteral("frida://") + verb + QStringLiteral("/") + linkName(transport)
+ QStringLiteral("/") + id + QStringLiteral("/") + targetId;
}
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include "FridaTypes.h"
class FridaUri
{
public:
static QString listDevices();
static QString listTargets(
FridaTransport transport, FridaListKind kind, const QString &deviceId);
static QString target(
FridaTransport transport,
FridaAction action,
const QString &deviceId,
const QString &targetId);
static QString quote(const QString &text);
static bool targetIsSafe(const QString &targetId);
};
-345
View File
@@ -1,345 +0,0 @@
#include "FridaWidget.h"
#include "FridaConnectDialog.h"
#include "FridaPanels.h"
#include "FridaSession.h"
#include "core/Iaito.h"
#include "core/MainWindow.h"
#include "widgets/HexdumpWidget.h"
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QTabWidget>
#include <QTreeWidget>
#include <QVBoxLayout>
quint64 fridaRuntimeAddress(FridaSession *session, quint64 addr)
{
if (!session) {
return addr;
}
const quint64 runtime = session->toRuntime(addr);
return runtime == RVA_INVALID ? addr : runtime;
}
void fridaSeekRuntime(FridaSession *session, quint64 runtimeVa)
{
if (!session) {
return;
}
const quint64 staticVa = session->toStatic(runtimeVa);
if (staticVa == RVA_INVALID) {
Core()->message(QObject::tr("No static mapping for %1").arg(RAddressString(runtimeVa)));
session->note(session->describe(runtimeVa));
return;
}
Core()->seekAndShow(staticVa);
}
void fridaTrace(FridaSession *session, quint64 addr)
{
if (!session || !session->isAttached()) {
return;
}
const quint64 runtime = fridaRuntimeAddress(session, addr);
const QString command = QStringLiteral("dt ") + RAddressString(runtime);
session->command(command, [session](const QString &output) {
session->note(output.trimmed().isEmpty() ? QObject::tr("trace installed") : output);
});
}
void fridaBreakpoint(FridaSession *session, quint64 addr)
{
if (!session || !session->isAttached()) {
return;
}
const quint64 runtime = fridaRuntimeAddress(session, addr);
const QString command = QStringLiteral("db ") + RAddressString(runtime);
session->command(command, [session](const QString &output) {
session->note(output.trimmed().isEmpty() ? QObject::tr("breakpoint set") : output);
});
}
void fridaShowHookDialog(QWidget *parent, FridaSession *session, quint64 addr)
{
if (!session || !session->isAttached()) {
return;
}
const quint64 runtime = fridaRuntimeAddress(session, addr);
auto *dialog = new QDialog(parent);
dialog->setWindowTitle(QObject::tr("Hook %1").arg(RAddressString(runtime)));
dialog->setAttribute(Qt::WA_DeleteOnClose);
auto *layout = new QVBoxLayout(dialog);
layout->addWidget(new QLabel(session->describe(addr), dialog));
auto *logArgs = new QCheckBox(QObject::tr("Log arguments"), dialog);
auto *logRet = new QCheckBox(QObject::tr("Log return value"), dialog);
auto *backtrace = new QCheckBox(QObject::tr("Backtrace"), dialog);
logArgs->setChecked(true);
logRet->setChecked(true);
layout->addWidget(logArgs);
layout->addWidget(logRet);
layout->addWidget(backtrace);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dialog);
buttons->button(QDialogButtonBox::Ok)->setText(QObject::tr("Install Hook"));
layout->addWidget(buttons);
QObject::connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::reject);
QObject::connect(buttons, &QDialogButtonBox::accepted, dialog, [=]() {
QString body = QStringLiteral("Interceptor.attach(ptr('%1'), {\n")
.arg(RAddressString(runtime));
body += QStringLiteral(" onEnter(args) {\n");
if (logArgs->isChecked()) {
body += QStringLiteral(
" console.log('hook', this.context.pc, args[0], args[1], args[2], args[3]);\n");
}
if (backtrace->isChecked()) {
body += QStringLiteral(
" console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)"
".map(DebugSymbol.fromAddress).join('\\n'));\n");
}
body += QStringLiteral(" },\n onLeave(retval) {\n");
if (logRet->isChecked()) {
body += QStringLiteral(" console.log('return', retval);\n");
}
body += QStringLiteral(" }\n});\n");
const QString path = session->writeScript(body);
if (path.isEmpty()) {
return;
}
session->addHook(runtime, QObject::tr("Interceptor.attach"));
session->command(QStringLiteral(". ") + path, [session](const QString &output) {
session->note(output.trimmed().isEmpty() ? QObject::tr("hook installed") : output);
});
dialog->accept();
});
dialog->open();
}
void fridaShowLiveHex(QWidget *parent, FridaSession *session, quint64 runtimeAddr)
{
if (!session || !session->isAttached() || !parent) {
return;
}
auto *main = qobject_cast<MainWindow *>(parent->window());
if (!main) {
return;
}
HexdumpWidget *hex = nullptr;
const auto dumps = main->findChildren<HexdumpWidget *>();
for (HexdumpWidget *dump : dumps) {
if (!dump->isHidden()) {
hex = dump;
break;
}
}
if (!hex && !dumps.isEmpty()) {
hex = dumps.first();
}
if (!hex) {
return;
}
auto *toggle = hex->findChild<QCheckBox *>(QStringLiteral("fridaLiveToggle"));
if (toggle && !toggle->isChecked()) {
toggle->setChecked(true);
} else if (auto *view = hex->findChild<HexWidget *>(QStringLiteral("hexTextView"))) {
view->refresh();
}
const quint64 staticVa = session->toStatic(runtimeAddr);
hex->show();
hex->raise();
if (IaitoSeekable *seekable = hex->getSeekable()) {
seekable->seek(staticVa == RVA_INVALID ? runtimeAddr : staticVa);
}
}
void fridaShowExports(QWidget *parent, FridaSession *session, const QString &moduleName)
{
const QString command = QStringLiteral("iEj ") + moduleName;
session->command(command, [parent, session, moduleName](const QString &output) {
const QJsonDocument doc = FridaBackend::parseJsonLoose(output);
auto *dialog = new QDialog(parent);
dialog->setWindowTitle(QObject::tr("Exports %1").arg(moduleName));
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->resize(640, 480);
auto *view = new QTreeWidget(dialog);
view->setHeaderLabels(
{QObject::tr("Address"), QObject::tr("Type"), QObject::tr("Name")});
view->setRootIsDecorated(false);
for (const QJsonValue value : doc.array()) {
const QJsonObject exp = value.toObject();
const quint64 runtime
= FridaBackend::parseAddress(exp.value(QStringLiteral("address")));
auto *item = new QTreeWidgetItem(view);
item->setText(0, session->formatRuntime(runtime));
item->setText(1, exp.value(QStringLiteral("type")).toString());
item->setText(2, exp.value(QStringLiteral("name")).toString());
item->setData(0, Qt::UserRole, QVariant::fromValue(runtime));
}
auto *layout = new QVBoxLayout(dialog);
layout->addWidget(view);
QObject::connect(
view, &QTreeWidget::itemActivated, session, [session](QTreeWidgetItem *item, int) {
fridaSeekRuntime(session, item->data(0, Qt::UserRole).toULongLong());
});
dialog->open();
});
}
FridaWidget::FridaWidget(FridaSession *fridaSession, MainWindow *main)
: IaitoDockWidget(main)
, session(fridaSession)
{
setObjectName(QStringLiteral("FridaWidget"));
setWindowTitle(tr("r2frida"));
auto *root = new QWidget(this);
auto *layout = new QVBoxLayout(root);
auto *header = new QHBoxLayout();
statusDot = new QWidget(root);
statusDot->setFixedSize(10, 10);
stateLabel = new QLabel(root);
connectButton = new QPushButton(tr("Connect"), root);
resumeButton = new QPushButton(tr("Resume"), root);
detachButton = new QPushButton(tr("Detach"), root);
detachButton->setStyleSheet(QStringLiteral(
"QPushButton { background-color: #E5534B; color: white; border: none;"
" border-radius: 4px; padding: 4px 12px; }"
"QPushButton:disabled { background-color: palette(mid); color: palette(button-text); }"));
header->addWidget(statusDot, 0, Qt::AlignVCenter);
header->addWidget(stateLabel, 1);
header->addWidget(resumeButton);
header->addWidget(connectButton);
header->addWidget(detachButton);
layout->addLayout(header);
deviceCombo = new QComboBox(root);
targetCombo = new QComboBox(root);
pidLabel = new QLabel(root);
archLabel = new QLabel(root);
sessionLabel = new QLabel(root);
sessionLabel->setWordWrap(true);
sessionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto *targetRow = new QHBoxLayout();
targetRow->addWidget(targetCombo, 1);
targetRow->addWidget(pidLabel);
auto *targetWrap = new QWidget(root);
targetWrap->setLayout(targetRow);
auto *form = new QFormLayout();
form->setContentsMargins(0, 0, 0, 0);
form->addRow(tr("Device"), deviceCombo);
form->addRow(tr("Target"), targetWrap);
form->addRow(tr("Architecture"), archLabel);
form->addRow(tr("Session"), sessionLabel);
layout->addLayout(form);
auto *tabs = new QTabWidget(root);
for (QWidget *page : createFridaPanels(session)) {
tabs->addTab(page, page->windowTitle());
}
layout->addWidget(tabs, 1);
setWidget(root);
connect(session, &FridaSession::changed, this, &FridaWidget::refresh);
connect(session, &FridaSession::errorMessage, this, [this](const QString &message) {
QMessageBox::warning(this, tr("r2frida"), message);
});
connect(connectButton, &QPushButton::clicked, this, &FridaWidget::showConnectDialog);
connect(resumeButton, &QPushButton::clicked, session, &FridaSession::resume);
connect(detachButton, &QPushButton::clicked, session, &FridaSession::detach);
const auto openConnect = [this](int index) {
if (index == 1) {
showConnectDialog();
deviceCombo->setCurrentIndex(0);
targetCombo->setCurrentIndex(0);
}
};
connect(deviceCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, openConnect);
connect(targetCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, openConnect);
refresh();
hide();
}
void FridaWidget::showConnectDialog()
{
auto *dialog = new FridaConnectDialog(session, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->open();
}
void FridaWidget::fillChoice(QComboBox *combo, const QString &current)
{
const bool blocked = combo->blockSignals(true);
combo->clear();
combo->addItem(current.isEmpty() ? tr("Not connected") : current);
combo->addItem(tr("Connect..."));
combo->setCurrentIndex(0);
combo->blockSignals(blocked);
}
void FridaWidget::setLive(bool live)
{
const QString color = live ? QStringLiteral("#3DDC84") : QStringLiteral("#E5534B");
statusDot->setStyleSheet(
QStringLiteral("background-color: %1; border-radius: 5px;").arg(color));
}
void FridaWidget::refresh()
{
const bool attached = session->isAttached();
const bool spawned = attached && session->isSuspended();
QString stateText = tr("Disconnected");
if (session->state() == FridaSessionState::Connecting) {
stateText = tr("Connecting");
} else if (spawned) {
stateText = tr("Spawned");
} else if (attached) {
stateText = tr("Connected");
} else if (session->state() == FridaSessionState::Error) {
stateText = tr("Error");
}
stateLabel->setText(stateText);
setLive(attached);
const FridaProcessInfo info = session->process();
QString device = session->deviceLabel();
if (!session->transportLabel().isEmpty()) {
device = device.isEmpty()
? session->transportLabel()
: session->transportLabel() + QStringLiteral(": ") + device;
}
fillChoice(deviceCombo, attached ? device : QString());
fillChoice(targetCombo, attached ? info.name : QString());
const QString pid = info.pid >= 0 ? QString::number(info.pid) : QStringLiteral("—");
pidLabel->setText(tr("PID: %1").arg(pid));
archLabel->setText(info.arch.isEmpty() ? QStringLiteral("—") : info.arch);
QString sessionText = info.identifier;
if (sessionText.isEmpty()) {
sessionText = session->uri().isEmpty() ? QStringLiteral("—") : session->uri();
}
sessionLabel->setText(sessionText);
sessionLabel->setToolTip(session->uri());
resumeButton->setEnabled(spawned);
detachButton->setEnabled(attached || session->state() == FridaSessionState::Connecting);
if (attached && !wasAttached) {
show();
raise();
}
wasAttached = attached;
}
-45
View File
@@ -1,45 +0,0 @@
#pragma once
#include "widgets/IaitoDockWidget.h"
class FridaSession;
class QComboBox;
class QLabel;
class QPushButton;
class QWidget;
class FridaWidget : public IaitoDockWidget
{
Q_OBJECT
public:
explicit FridaWidget(FridaSession *session, MainWindow *main);
void showConnectDialog();
private:
void refresh();
void fillChoice(QComboBox *combo, const QString &current);
void setLive(bool live);
FridaSession *session = nullptr;
QWidget *statusDot = nullptr;
QLabel *stateLabel = nullptr;
QLabel *pidLabel = nullptr;
QLabel *archLabel = nullptr;
QLabel *sessionLabel = nullptr;
QComboBox *deviceCombo = nullptr;
QComboBox *targetCombo = nullptr;
QPushButton *connectButton = nullptr;
QPushButton *resumeButton = nullptr;
QPushButton *detachButton = nullptr;
bool wasAttached = false;
};
quint64 fridaRuntimeAddress(FridaSession *session, quint64 addr);
void fridaSeekRuntime(FridaSession *session, quint64 runtimeVa);
void fridaTrace(FridaSession *session, quint64 addr);
void fridaBreakpoint(FridaSession *session, quint64 addr);
void fridaShowHookDialog(QWidget *parent, FridaSession *session, quint64 addr);
void fridaShowLiveHex(QWidget *parent, FridaSession *session, quint64 runtimeAddr);
void fridaShowExports(QWidget *parent, FridaSession *session, const QString &moduleName);
-298
View File
@@ -1,298 +0,0 @@
#include "IaitoFridaPlugin.h"
#include "FridaSession.h"
#include "FridaWidget.h"
#include "core/Iaito.h"
#include "core/MainWindow.h"
#include "widgets/ConsoleWidget.h"
#include "widgets/HexdumpWidget.h"
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QLabel>
#include <QMenu>
#include <QPointer>
#include <QSignalBlocker>
#include <QStatusBar>
#include <QTimer>
#include <QVBoxLayout>
namespace {
ConsoleWidget *sessionConsole(MainWindow *main)
{
return main ? main->findChild<ConsoleWidget *>() : nullptr;
}
} // namespace
void IaitoFridaPlugin::setupPlugin() {}
void IaitoFridaPlugin::terminate()
{
HexdumpWidget::setCustomizeHook(nullptr);
if (mainWindow) {
if (ConsoleWidget *console = sessionConsole(mainWindow)) {
console->setCommandHandler(nullptr);
}
for (HexdumpWidget *dump : mainWindow->findChildren<HexdumpWidget *>()) {
if (auto *view = dump->findChild<HexWidget *>(QStringLiteral("hexTextView"))) {
view->setDataReader({});
}
}
}
if (session && session->isAttached()) {
session->detach();
}
}
void IaitoFridaPlugin::setupInterface(MainWindow *main)
{
mainWindow = main;
session = new FridaSession(main);
widget = new FridaWidget(session, main);
main->addPluginDockWidget(widget);
widget->hide();
statusButton = new QToolButton(main);
statusButton->setAutoRaise(false);
statusButton->setToolTip(tr("r2frida session"));
statusDetail = new QLabel(main);
main->statusBar()->addPermanentWidget(statusButton);
main->statusBar()->addPermanentWidget(statusDetail);
connect(statusButton, &QToolButton::clicked, this, [this]() {
widget->show();
widget->raise();
});
connect(session, &FridaSession::changed, this, &IaitoFridaPlugin::updateStatus);
if (ConsoleWidget *console = sessionConsole(main)) {
console->setCommandHandler(
[this](const QString &command) { return handleConsoleCommand(command); });
connect(session, &FridaSession::consoleMessage, console, &ConsoleWidget::addOutput);
}
HexdumpWidget::setCustomizeHook(
[this](HexdumpWidget *hexdump) { installLiveHexdump(hexdump); });
QTimer::singleShot(0, this, [this, main]() { buildMenu(main); });
buildContextMenus(main);
updateStatus();
}
bool IaitoFridaPlugin::handleConsoleCommand(const QString &command)
{
const QString line = command.trimmed();
if (!line.startsWith(QLatin1Char(':'))) {
return false;
}
ConsoleWidget *console = sessionConsole(mainWindow);
if (!console) {
return false;
}
const QString frida = line.mid(1);
if (frida.contains(QLatin1Char(';')) || frida.contains(QLatin1Char('|'))
|| frida.contains(QLatin1Char('`'))) {
console->addOutput(tr("That command is not sent to r2frida."));
return true;
}
if (!session->isAttached()) {
console->addOutput(tr("r2frida is not attached."));
return true;
}
QPointer<ConsoleWidget> target = console;
session->command(frida, [target](const QString &output) {
if (target) {
target->addOutput(output);
}
});
return true;
}
QByteArray IaitoFridaPlugin::readLiveBytes(uint64_t addr, int len) const
{
if (!session || !session->isAttached() || len <= 0) {
return QByteArray(qMax(len, 0), '\0');
}
quint64 runtime = RVA_INVALID;
if (session->containsStatic(addr)) {
runtime = session->toRuntime(addr);
} else if (session->containsRuntime(addr)) {
runtime = addr;
}
if (runtime == RVA_INVALID) {
return QByteArray(len, '\0');
}
QByteArray bytes = session->readBytes(runtime, len);
if (bytes.size() < len) {
bytes.append(QByteArray(len - bytes.size(), '\0'));
} else if (bytes.size() > len) {
bytes.truncate(len);
}
return bytes;
}
void IaitoFridaPlugin::installLiveHexdump(HexdumpWidget *hexdump)
{
if (!hexdump || hexdump->findChild<QCheckBox *>(QStringLiteral("fridaLiveToggle"))) {
return;
}
auto *live = new QCheckBox(tr("Live process"), hexdump);
live->setObjectName(QStringLiteral("fridaLiveToggle"));
live->setToolTip(tr("Show bytes from the attached r2frida target"));
if (QLayout *layout = hexdump->widget() ? hexdump->widget()->layout() : nullptr) {
if (auto *box = qobject_cast<QVBoxLayout *>(layout)) {
box->insertWidget(0, live);
}
}
auto *view = hexdump->findChild<HexWidget *>(QStringLiteral("hexTextView"));
connect(live, &QCheckBox::toggled, this, [this, live, view](bool on) {
if (!view) {
return;
}
if (!on) {
view->setDataReader({});
return;
}
if (!session || !session->isAttached()) {
const QSignalBlocker blocker(live);
live->setChecked(false);
if (ConsoleWidget *console = sessionConsole(mainWindow)) {
console->addOutput(tr("r2frida is not attached."));
}
return;
}
view->setDataReader([this](uint64_t addr, int len) { return readLiveBytes(addr, len); });
});
connect(session, &FridaSession::changed, this, [this, live, view]() {
if (!session->isAttached()) {
if (live->isChecked()) {
live->setChecked(false);
}
return;
}
if (live->isChecked() && view) {
view->refresh();
}
});
}
void IaitoFridaPlugin::buildMenu(MainWindow *main)
{
QMenu *debugMenu = main->getMenuByType(MainWindow::MenuType::Debug);
if (!debugMenu) {
return;
}
debugMenu->addSeparator();
auto *menu = debugMenu->addMenu(tr("r2frida"));
connectAction = menu->addAction(tr("Connect..."), widget, &FridaWidget::showConnectDialog);
resumeAction = menu->addAction(tr("Resume"), session, &FridaSession::resume);
detachAction = menu->addAction(tr("Detach"), session, &FridaSession::detach);
menu->addSeparator();
menu->addAction(tr("Show panel"), this, [this]() {
widget->show();
widget->raise();
});
if (!session->checkAvailable()) {
connectAction->setEnabled(false);
connectAction->setToolTip(
tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
}
updateStatus();
}
void IaitoFridaPlugin::addFridaMenu(QMenu *pluginMenu)
{
if (!pluginMenu) {
return;
}
auto *menu = pluginMenu->addMenu(tr("Frida"));
auto *trace = menu->addAction(tr("Trace"));
auto *hook = menu->addAction(tr("Hook"));
auto *breakpoint = menu->addAction(tr("Set breakpoint"));
auto *live = menu->addAction(tr("Read live bytes"));
auto *copy = menu->addAction(tr("Copy runtime address"));
auto *mapping = menu->addAction(tr("Show address mapping"));
const auto enable = [this, menu]() {
const bool on = session->isAttached();
for (QAction *action : menu->actions()) {
action->setEnabled(on);
action->setData(menu->menuAction()->data());
}
};
connect(menu, &QMenu::aboutToShow, this, enable);
connect(trace, &QAction::triggered, this, [this, trace]() {
fridaTrace(session, trace->data().toULongLong());
});
connect(hook, &QAction::triggered, this, [this, hook]() {
fridaShowHookDialog(mainWindow, session, hook->data().toULongLong());
});
connect(breakpoint, &QAction::triggered, this, [this, breakpoint]() {
fridaBreakpoint(session, breakpoint->data().toULongLong());
});
connect(live, &QAction::triggered, this, [this, live]() {
const quint64 addr = live->data().toULongLong();
fridaShowLiveHex(mainWindow, session, fridaRuntimeAddress(session, addr));
});
connect(copy, &QAction::triggered, this, [this, copy]() {
const quint64 runtime = fridaRuntimeAddress(session, copy->data().toULongLong());
QApplication::clipboard()->setText(RAddressString(runtime));
});
connect(mapping, &QAction::triggered, this, [this, mapping]() {
Core()->message(session->describe(mapping->data().toULongLong()));
});
}
void IaitoFridaPlugin::buildContextMenus(MainWindow *main)
{
addFridaMenu(main->getContextMenuExtensions(MainWindow::ContextMenuType::Disassembly));
addFridaMenu(main->getContextMenuExtensions(MainWindow::ContextMenuType::Addressable));
}
void IaitoFridaPlugin::updateStatus()
{
if (!statusButton) {
return;
}
const bool live = session->isAttached();
const QString color = live ? QStringLiteral("#2E7D32") : QStringLiteral("#C62828");
statusButton->setText(tr("FRIDA"));
statusButton->setStyleSheet(QStringLiteral(
"QToolButton { color: white; background-color: %1; border: none;"
" border-radius: 3px; padding: 0 6px; font-weight: 600; }")
.arg(color));
if (!session->checkAvailable()) {
statusButton->setToolTip(
tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
statusDetail->clear();
} else if (live) {
const FridaProcessInfo info = session->process();
QString arch = info.arch;
if (!arch.isEmpty()) {
arch = arch.toUpper();
}
statusDetail->setText(
tr("%1 %2 [%3] %4 %5")
.arg(
session->deviceLabel(),
info.name,
QString::number(info.pid),
arch,
session->transportLabel().toUpper()));
statusButton->setToolTip(tr("%1 | %2 | slide %3")
.arg(
session->transportLabel(),
session->deviceLabel(),
RAddressString(session->slide())));
} else if (session->state() == FridaSessionState::Connecting) {
statusButton->setToolTip(tr("r2frida is connecting"));
statusDetail->setText(tr("connecting"));
} else {
statusButton->setToolTip(tr("r2frida is disconnected"));
statusDetail->clear();
}
if (resumeAction) {
resumeAction->setEnabled(session->isSuspended());
}
if (detachAction) {
detachAction->setEnabled(session->isAttached());
}
}
-53
View File
@@ -1,53 +0,0 @@
#pragma once
#include "IaitoPlugin.h"
#include <QByteArray>
#include <QObject>
class FridaSession;
class FridaWidget;
class HexdumpWidget;
class MainWindow;
class QAction;
class QMenu;
class QLabel;
class QToolButton;
class IaitoFridaPlugin : public QObject, public IaitoPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.radare.iaito.plugins.IaitoPlugin")
Q_INTERFACES(IaitoPlugin)
public:
void setupPlugin() override;
void setupInterface(MainWindow *main) override;
void terminate() override;
QString getName() const override { return QStringLiteral("r2frida"); }
QString getAuthor() const override { return QStringLiteral("radareorg"); }
QString getDescription() const override
{
return QStringLiteral("Dynamic instrumentation through r2frida.");
}
QString getVersion() const override { return QStringLiteral("1.0"); }
private:
void buildMenu(MainWindow *main);
void buildContextMenus(MainWindow *main);
void updateStatus();
void addFridaMenu(QMenu *pluginMenu);
void installLiveHexdump(HexdumpWidget *hexdump);
bool handleConsoleCommand(const QString &command);
QByteArray readLiveBytes(uint64_t addr, int len) const;
FridaSession *session = nullptr;
FridaWidget *widget = nullptr;
MainWindow *mainWindow = nullptr;
QToolButton *statusButton = nullptr;
QLabel *statusDetail = nullptr;
QAction *connectAction = nullptr;
QAction *detachAction = nullptr;
QAction *resumeAction = nullptr;
};
-44
View File
@@ -1,44 +0,0 @@
TEMPLATE = lib
CONFIG += plugin
CONFIG -= import_plugins
QT += widgets svg network
TARGET = IaitoFridaPlugin
DESTDIR = $$PWD/../../../build/plugins/native
QMAKE_CXXFLAGS += $$system(pkg-config --cflags r_core)
QMAKE_CXXFLAGS += -std=c++20
INCLUDEPATH += \
$$PWD \
$$PWD/.. \
$$PWD/../.. \
$$PWD/../../common \
$$PWD/../../core \
$$PWD/../../widgets \
$$PWD/../../dialogs \
$$PWD/../../menus \
$$PWD/../../plugins \
$$system(r2 -H R2_INCDIR)
macx: QMAKE_LFLAGS += -undefined dynamic_lookup
unix:!macx: QMAKE_LFLAGS += -Wl,--unresolved-symbols=ignore-all
HEADERS += \
FridaTypes.h \
FridaUri.h \
FridaBackend.h \
FridaSession.h \
FridaConnectDialog.h \
FridaWidget.h \
FridaPanels.h \
IaitoFridaPlugin.h \
../IaitoPlugin.h
SOURCES += \
FridaUri.cpp \
FridaBackend.cpp \
FridaSession.cpp \
FridaConnectDialog.cpp \
FridaWidget.cpp \
FridaPanels.cpp \
IaitoFridaPlugin.cpp
+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
+2
View File
@@ -233,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>
+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(
+1 -7
View File
@@ -44,6 +44,7 @@ static const char *refreshAllCommandPrefixes[] = {
"C",
"aa",
"af",
"avg",
"w",
};
@@ -261,13 +262,6 @@ void ConsoleWidget::executeCommand(const QString &command)
QString cmd_line = "[" + RAddressString(Core()->getOffset()) + "]> " + command;
addOutput(cmd_line);
if (commandHandler && commandHandler(command)) {
historyAdd(command);
ui->r2InputLineEdit->setEnabled(true);
ui->r2InputLineEdit->setFocus();
return;
}
RVA oldOffset = Core()->getOffset();
bool refreshAll = consoleCommandRefreshesAll(command);
bool isPiped = command.contains(">");
-8
View File
@@ -10,7 +10,6 @@
#include <QSocketNotifier>
#include <QStringListModel>
#include <functional>
#include <memory>
class QCompleter;
@@ -33,12 +32,6 @@ public:
void setMaxHistoryEntries(int max) { maxHistoryEntries = max; }
// Return true to handle the command instead of sending it to radare2.
void setCommandHandler(const std::function<bool(const QString &command)> &handler)
{
commandHandler = handler;
}
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
QWidget *widgetToFocusOnRaise() override;
@@ -93,7 +86,6 @@ private:
void redirectOutput();
QSharedPointer<CommandTask> commandTask;
std::function<bool(const QString &)> commandHandler;
std::unique_ptr<Ui::ConsoleWidget> ui;
QAction *actionWrapLines;
+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
+528 -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);
@@ -196,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;
@@ -254,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;
}
@@ -303,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;
}
@@ -324,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();
}
}
@@ -443,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();
@@ -454,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);
@@ -478,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;
@@ -926,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) {
@@ -971,6 +1262,7 @@ void DecompilerWidget::updateCursorPosition()
QTextCursor cursor = ui->textEdit->textCursor();
cursor.setPosition(pos);
ui->textEdit->setTextCursor(cursor);
updateCursorContext();
updateSelection();
connectCursorPositionChanged(true);
}
@@ -998,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();
}
@@ -1011,6 +1303,7 @@ QString DecompilerWidget::getWindowTitle() const
void DecompilerWidget::fontsUpdatedSlot()
{
setupFonts();
refreshDecompilerHover();
}
void DecompilerWidget::colorsUpdatedSlot()
@@ -1022,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())) {
@@ -1055,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();
@@ -1081,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;
@@ -1088,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);
}
@@ -1152,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;
}
@@ -1283,7 +1704,7 @@ static RCodeMeta *copyCodeMeta(RCodeMeta *source)
return copy;
}
struct DecompilerAddressLine
struct DecompilerColumnLine
{
size_t start = 0;
size_t end = 0;
@@ -1292,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);
@@ -1309,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
@@ -1325,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) {
@@ -1334,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;
}
@@ -1350,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;
}
@@ -1363,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());
@@ -1413,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;
@@ -1435,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()));
@@ -1456,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) {
@@ -1465,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)
@@ -1474,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);
+26 -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()
@@ -291,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>
+48 -16
View File
@@ -10,6 +10,7 @@
#include "common/TempConfig.h"
#include "core/MainWindow.h"
#include "dialogs/ShortcutKeysDialog.h"
#include "menus/ContextMenuIcons.h"
#include "menus/DisassemblyContextMenu.h"
#include "widgets/ExceptionBar.h"
@@ -307,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) \
@@ -328,6 +348,12 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
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);
})
@@ -345,6 +371,7 @@ 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()) {
@@ -1117,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));
}
@@ -1383,12 +1404,19 @@ void DisassemblyWidget::cursorPositionChanged()
void DisassemblyWidget::updateContextMenuState()
{
exceptionBar->populateMenu(exceptionMenu, readCurrentDisassemblyOffset());
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
@@ -1479,11 +1507,17 @@ void DisassemblyWidget::copyBytes()
void DisassemblyWidget::jumpToOffsetUnderCursor(const QTextCursor &cursor)
{
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) {
const RVA offset = readDisassemblyOffset(cursor);
if (target != RVA_INVALID && target != offset) {
seekable->seek(target);
} else {
seekable->seekToReference(readDisassemblyOffset(cursor));
seekable->seekToReference(offset);
}
}
@@ -1753,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);
}
@@ -1908,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();
}
}
@@ -1986,7 +2018,7 @@ void DisassemblyTextEdit::paintEvent(QPaintEvent *event)
void DisassemblyWidget::seekPrev()
{
Core()->seekPrev();
seekable->seekPrev();
}
/*********************
+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;
+16
View File
@@ -176,6 +176,8 @@ FlagsWidget::FlagsWidget(MainWindow *main)
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);
@@ -234,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);
+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();
+2 -2
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
{
@@ -118,7 +118,7 @@
<container>1</container>
</customwidget>
<customwidget>
<class>AddressableItemList&lt;&gt;</class>
<class>AddressableItemView</class>
<extends>QTreeView</extends>
<header>widgets/AddressableItemList.h</header>
<container>1</container>
+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>
+1 -1
View File
@@ -37,7 +37,7 @@ GraphWidget::GraphWidget(MainWindow *main)
connect(this, &QDockWidget::visibilityChanged, this, [this, main](bool visibility) {
main->toggleOverview(visibility, this);
if (visibility) {
graphView->onSeekChanged(Core()->getOffset());
graphView->onSeekChanged(seekable->getOffset());
}
});
+28 -13
View File
@@ -102,7 +102,8 @@ struct GlyphRow
void HexWidget::drawBackgroundRanges(QPainter &painter, bool ascii)
{
if (flagBackgroundRanges.isEmpty() && basicBlockBackgroundRanges.isEmpty()) {
if (flagBackgroundRanges.isEmpty() && basicBlockBackgroundRanges.isEmpty()
&& typeRanges.isEmpty()) {
return;
}
@@ -119,6 +120,7 @@ void HexWidget::drawBackgroundRanges(QPainter &painter, bool ascii)
};
drawRanges(flagBackgroundRanges);
drawRanges(basicBlockBackgroundRanges);
drawRanges(typeRanges);
painter.restore();
}
@@ -406,6 +408,17 @@ HexWidget::HexWidget(QWidget *parent)
actionCopyAddress = new QAction(tr("Copy address"), this);
actionCopyAddress->setShortcutContext(Qt::ShortcutContext::WidgetWithChildrenShortcut);
ShortcutMgr()->bindAction("hex.copyAddress", actionCopyAddress);
auto flagAction = new QAction(tr("Add / edit flag..."), this);
flagAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
ShortcutMgr()->bindAction("hex.flag", flagAction);
addAction(flagAction);
connect(flagAction, &QAction::triggered, this, [this]() {
FlagDialog dialog(getLocationAddress(), selection.isEmpty() ? 1 : selection.size(), this);
if (dialog.exec() == QDialog::Accepted) {
refresh();
}
});
connect(actionCopyAddress, &QAction::triggered, this, &HexWidget::copyAddress);
addAction(actionCopyAddress);
// Action to copy selected bytes as a C string
@@ -903,6 +916,12 @@ void HexWidget::clearSelection()
emit selectionChanged(getSelection());
}
void HexWidget::setTypeRanges(const QVector<ColoredRange> &ranges)
{
typeRanges = ranges;
viewport()->update();
}
HexWidget::Selection HexWidget::getSelection()
{
return Selection{selection.isEmpty(), selection.start(), selection.end()};
@@ -924,17 +943,6 @@ void HexWidget::refresh()
viewport()->update();
}
void HexWidget::setDataReader(const std::function<QByteArray(uint64_t, int)> &reader)
{
if (auto *memory = dynamic_cast<MemoryData *>(data.get())) {
memory->setReader(reader);
}
if (auto *memory = dynamic_cast<MemoryData *>(oldData.get())) {
memory->setReader(reader);
}
refresh();
}
void HexWidget::setItemEndianess(bool bigEndian)
{
itemBigEndian = bigEndian;
@@ -1558,7 +1566,7 @@ void HexWidget::addFlagAction(QMenu *parent, const HexContextInfo &ctx)
}
const QColor flagColor(245, 166, 35);
auto [addFlag, editFlag] = FlagDialog::addFlagMenuActions(
auto [addFlag, editFlag, deleteFlag] = FlagDialog::addFlagMenuActions(
parent,
this,
ctx.effectiveAddress,
@@ -1568,6 +1576,13 @@ void HexWidget::addFlagAction(QMenu *parent, const HexContextInfo &ctx)
[this]() { refresh(); });
setActionIcon(addFlag, MenuIcon::Flag, flagColor);
setActionIcon(editFlag, MenuIcon::Flag, flagColor);
setActionIcon(deleteFlag, MenuIcon::Flag, QColor(117, 117, 117));
auto flagAction = addFlag->isEnabled() ? addFlag : editFlag;
ShortcutMgr()->bindAction("hex.flag", flagAction);
flagAction->setShortcutContext(Qt::WidgetShortcut);
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
flagAction->setShortcutVisibleInContextMenu(true);
#endif
parent->addSeparator();
}
+20 -20
View File
@@ -6,7 +6,6 @@
#include "common/SparseHexLayout.h"
#include "dialogs/HexdumpRangeDialog.h"
#include <functional>
#include <memory>
#include <QColor>
#include <QIcon>
@@ -140,31 +139,27 @@ public:
{
// FIXME: reuse data if possible
const uint64_t blockSize = 0x1000ULL;
uint64_t alignedAddr = address & ~(blockSize - 1);
int offset = address - alignedAddr;
int len = (offset + length + (blockSize - 1)) & ~(blockSize - 1);
const uint64_t alignedAddr = address & ~(blockSize - 1);
m_blocks.clear();
if (length <= 0) {
m_firstBlockAddr = 1;
m_lastValidAddr = 0;
return;
}
const uint64_t offset = address - alignedAddr;
uint64_t len = (offset + uint64_t(length) + (blockSize - 1)) & ~(blockSize - 1);
m_firstBlockAddr = alignedAddr;
m_lastValidAddr = length ? alignedAddr + len - 1 : 0;
m_lastValidAddr = alignedAddr + len - 1;
if (m_lastValidAddr < m_firstBlockAddr) {
m_lastValidAddr = -1;
len = m_lastValidAddr - m_firstBlockAddr + 1;
}
m_blocks.clear();
uint64_t addr = alignedAddr;
for (ut64 i = 0; i < len / blockSize; ++i, addr += blockSize) {
QByteArray block = reader ? reader(addr, int(blockSize))
: Core()->ioRead(addr, blockSize);
if (block.size() < int(blockSize)) {
block.append(QByteArray(int(blockSize) - block.size(), '\0'));
} else if (block.size() > int(blockSize)) {
block.truncate(int(blockSize));
}
m_blocks.append(block);
m_blocks.append(Core()->ioRead(addr, blockSize));
}
}
void setReader(const std::function<QByteArray(uint64_t, int)> &next) { reader = next; }
bool copy(void *out, uint64_t addr, size_t len) override
{
if (addr < m_firstBlockAddr || addr > m_lastValidAddr
@@ -197,7 +192,6 @@ private:
QVector<QByteArray> m_blocks;
uint64_t m_firstBlockAddr = 0;
uint64_t m_lastValidAddr = 0;
std::function<QByteArray(uint64_t, int)> reader;
};
class HexSelection
@@ -327,6 +321,14 @@ public:
void selectRange(RVA start, RVA end);
void clearSelection();
struct ColoredRange
{
RVA start;
RVA end;
QColor color;
};
void setTypeRanges(const QVector<ColoredRange> &ranges);
struct Selection
{
bool empty;
@@ -335,9 +337,6 @@ public:
// TODO: add currentAddress
};
Selection getSelection();
// When set, displayed bytes come from this reader instead of the open file.
void setDataReader(const std::function<QByteArray(uint64_t, int)> &reader);
public slots:
void seek(uint64_t address);
void refresh();
@@ -653,6 +652,7 @@ private:
QString statusBarText;
QVector<FlagBackgroundRange> flagBackgroundRanges;
QVector<BasicBlockBackgroundRange> basicBlockBackgroundRanges;
QVector<ColoredRange> typeRanges;
bool sparse = false;
bool repaintCollapseButtons = false;
std::vector<SparseHexLayout::Row> sparseRows;
+514 -7
View File
@@ -9,20 +9,23 @@
#include "core/MainWindow.h"
#include <cstring>
#include <functional>
#include <QByteArray>
#include <QClipboard>
#include <QCoreApplication>
#include <QElapsedTimer>
#include <QEvent>
#include <QHeaderView>
#include <QInputDialog>
#include <QJsonArray>
#include <QJsonObject>
#include <QKeyEvent>
#include <QMenu>
#include <QPlainTextEdit>
#include <QRegularExpression>
#include <QScrollBar>
#include <QSet>
#include <QShortcut>
#include <QSignalBlocker>
#include <QStatusBar>
#include <QStringList>
#include <QTextDocumentFragment>
@@ -30,13 +33,29 @@
#include <QtEndian>
namespace {
std::function<void(HexdumpWidget *)> hexdumpCustomize;
enum StructureColumn { FieldColumn, TypeColumn, ValueColumn, AddressColumn, SizeColumn };
enum StructureRole { AddressRole = Qt::UserRole, SizeRole, PopulatedRole, PathRole };
QString arrayType(const QString &element, quint64 count)
{
QString type = element;
const int bracket = type.indexOf(QLatin1Char('['));
type.insert(bracket < 0 ? type.size() : bracket, QStringLiteral("[%1]").arg(count));
return type;
}
void HexdumpWidget::setCustomizeHook(const std::function<void(HexdumpWidget *)> &hook)
QColor fieldColor(const QString &path)
{
hexdumpCustomize = hook;
// Stable across reloads, processes and themes; alpha preserves readable text.
quint32 hash = 2166136261U;
for (const QChar ch : path) {
hash = (hash ^ ch.unicode()) * 16777619U;
}
QColor color = QColor::fromHsv(hash % 360, 160, 220);
color.setAlpha(65);
return color;
}
} // namespace
HexdumpWidget::HexdumpWidget(MainWindow *main)
: MemoryDockWidget(MemoryWidgetType::Hexdump, main)
@@ -227,6 +246,7 @@ HexdumpWidget::HexdumpWidget(MainWindow *main)
ui->hexSideTab_2->installEventFilter(this);
initParsing();
initStructures();
// Populate Format tab struct selector and inline format
{
// Load type names from radare2
@@ -270,9 +290,6 @@ HexdumpWidget::HexdumpWidget(MainWindow *main)
make_nav("hex.navUpSelect", Qt::Key_Up, Qt::ShiftModifier);
make_nav("hex.navRightSelect", Qt::Key_Right, Qt::ShiftModifier);
}
if (hexdumpCustomize) {
hexdumpCustomize(this);
}
}
// Event filter to catch resize events on the side tabs and refresh values
@@ -363,6 +380,9 @@ void HexdumpWidget::refresh(RVA addr)
}
// this->current_address = ui->hexTextView->cursor.address;
sent_seek = false;
if (addr == RVA_INVALID) {
reloadStructures();
}
}
void HexdumpWidget::initParsing()
@@ -449,6 +469,9 @@ void HexdumpWidget::setupFonts()
ui->hexDisasTextEdit->setFont(font);
ui->hexTextView->setMonospaceFont(font);
ui->formatOutput->setFont(font);
ui->structureTree->setFont(font);
ui->structurePinsTree->setFont(font);
ui->structureCommandOutput->setFont(font);
}
void HexdumpWidget::refreshSelectionInfo()
@@ -469,6 +492,490 @@ void HexdumpWidget::fontsUpdated()
setupFonts();
}
void HexdumpWidget::initStructures()
{
ui->structureTypeComboBox->setInsertPolicy(QComboBox::NoInsert);
ui->structureAddressEdit->setText(RAddressString(seekable->getOffset()));
ui->structureCommandOutput->hide();
ui->structureTree->header()->setStretchLastSection(false);
for (int column = FieldColumn; column <= SizeColumn; ++column) {
ui->structureTree->header()->setSectionResizeMode(column, QHeaderView::ResizeToContents);
}
qhelpers::setVerticalScrollMode(ui->structureTree);
ui->structureTree->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
ui->structureSplitter->setStretchFactor(0, 3);
ui->structureSplitter->setStretchFactor(1, 1);
connect(ui->hexSideTab_2, &QTabWidget::currentChanged, this, [this]() {
if (ui->hexSideTab_2->currentWidget() == ui->tabStructures && !structurePanelSized) {
const int width = ui->splitter->width();
ui->splitter->setSizes({width / 2, width / 2});
structurePanelSized = true;
}
});
connect(ui->structureInspectButton, &QPushButton::clicked, this, &HexdumpWidget::inspectStructure);
connect(
ui->structureAddressEdit, &QLineEdit::returnPressed, this, &HexdumpWidget::inspectStructure);
connect(
ui->structureTypeComboBox->lineEdit(),
&QLineEdit::returnPressed,
this,
&HexdumpWidget::inspectStructure);
connect(ui->structureRefreshButton, &QPushButton::clicked, this, &HexdumpWidget::reloadStructures);
connect(ui->structurePinButton, &QPushButton::clicked, this, &HexdumpWidget::pinStructure);
connect(ui->structureCommandButton, &QPushButton::clicked, this, &HexdumpWidget::pinCommand);
connect(ui->structureUnpinButton, &QPushButton::clicked, this, &HexdumpWidget::removeStructurePin);
connect(ui->structureCursorButton, &QPushButton::clicked, this, [this]() {
const auto selection = ui->hexTextView->getSelection();
const RVA address = selection.empty ? current_address : selection.startAddress;
ui->structureAddressEdit->setText(RAddressString(address));
inspectStructure();
});
connect(ui->structureTree, &QTreeWidget::itemExpanded, this, &HexdumpWidget::populateStructure);
connect(ui->structureTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *item) {
selectStructureField(item);
});
connect(
ui->structureTree, &QTreeWidget::itemActivated, this, &HexdumpWidget::selectStructureField);
connect(ui->structureTree, &QTreeWidget::itemCollapsed, this, [this]() {
updateStructureHighlights();
});
connect(
ui->structurePinsTree,
&QTreeWidget::itemActivated,
this,
&HexdumpWidget::activateStructurePin);
connect(
ui->structurePinsTree, &QTreeWidget::currentItemChanged, this, [this](QTreeWidgetItem *item) {
ui->structureUnpinButton->setEnabled(item != nullptr);
ui->structureRunButton->setEnabled(item && item->data(1, Qt::UserRole).toBool());
ui->structureCommandOutput->clear();
ui->structureCommandOutput->hide();
});
connect(ui->structureRunButton, &QPushButton::clicked, this, [this]() {
auto *item = ui->structurePinsTree->currentItem();
if (!item || !item->data(1, Qt::UserRole).toBool()) {
return;
}
const RVA address = item->data(0, Qt::UserRole).toULongLong();
ui->structureCommandOutput->show();
ui->structureCommandOutput->setPlainText(
Core()->cmdRawAt(Core()->getCommandAnnotation(address), address).trimmed());
});
connect(ui->structureTree, &QTreeWidget::customContextMenuRequested, this, [this](QPoint pos) {
auto *item = ui->structureTree->itemAt(pos);
if (!item || !item->data(AddressColumn, AddressRole).isValid()) {
return;
}
QMenu menu(this);
menu.addAction(tr("Select field bytes"), this, [this, item]() {
selectStructureField(item);
});
menu.addAction(tr("Copy value"), this, [item]() {
QGuiApplication::clipboard()->setText(item->text(ValueColumn));
});
menu.addAction(tr("Copy address"), this, [item]() {
QGuiApplication::clipboard()->setText(item->text(AddressColumn));
});
menu.exec(ui->structureTree->viewport()->mapToGlobal(pos));
});
connect(ui->structurePinsTree, &QTreeWidget::customContextMenuRequested, this, [this](QPoint pos) {
auto *item = ui->structurePinsTree->itemAt(pos);
if (!item) {
return;
}
ui->structurePinsTree->setCurrentItem(item);
QMenu menu(this);
menu.addAction(tr("Go to pinned address"), this, [this, item]() {
activateStructurePin(item);
});
menu.addAction(tr("Unpin"), this, &HexdumpWidget::removeStructurePin);
menu.exec(ui->structurePinsTree->viewport()->mapToGlobal(pos));
});
connect(
ui->hexTextView,
&HexWidget::contextMenuAboutToShow,
this,
[this](QMenu *menu, RVA start, RVA) {
menu->addAction(tr("Inspect structure here"), this, [this, start]() {
showSidePanel(true);
ui->hexSideTab_2->setCurrentWidget(ui->tabStructures);
reloadStructures();
RVA base = start;
const auto links = typeLayout.links();
for (auto it = links.cbegin(); it != links.cend(); ++it) {
const RVA size = typeLayout.size(it.value());
if (start >= it.key() && start - it.key() < size) {
base = it.key();
ui->structureTypeComboBox->setCurrentText(it.value());
}
}
ui->structureAddressEdit->setText(RAddressString(base));
inspectStructure();
});
});
reloadStructures();
}
void HexdumpWidget::reloadStructures()
{
if (updatingStructures) {
return;
}
typeLayout.load(Core()->cmdRaw("tk"), Core()->getConfigi("asm.bits") / 8);
const QString selectedType = ui->structureTypeComboBox->currentText();
{
QSignalBlocker blocker(ui->structureTypeComboBox);
ui->structureTypeComboBox->clear();
ui->structureTypeComboBox->addItem(QString());
ui->structureTypeComboBox->addItems(typeLayout.names());
ui->structureTypeComboBox->setCurrentText(selectedType);
}
auto *selected = ui->structurePinsTree->currentItem();
const QVariant selectedAddress = selected ? selected->data(0, Qt::UserRole) : QVariant();
const bool selectedCommand = selected && selected->data(1, Qt::UserRole).toBool();
ui->structurePinsTree->clear();
const auto addPin = [this,
&selectedAddress,
selectedCommand](RVA address, const QString &text, bool command) {
auto *item = new QTreeWidgetItem(ui->structurePinsTree, {RAddressString(address), text});
item->setData(0, Qt::UserRole, QVariant::fromValue(address));
item->setData(1, Qt::UserRole, command);
item->setToolTip(
1,
command ? tr("Command annotation (Cr): %1").arg(text)
: tr("Type link (tl): %1").arg(text));
if (selectedAddress.isValid() && selectedAddress.toULongLong() == address
&& selectedCommand == command) {
ui->structurePinsTree->setCurrentItem(item);
}
};
const auto links = typeLayout.links();
for (auto it = links.cbegin(); it != links.cend(); ++it) {
addPin(it.key(), it.value(), false);
}
// Read addresses as text rather than JSON doubles, including addresses above 2^53.
static const QRegularExpression commandPin(
QStringLiteral("^Cr\\s+\\d+\\s+(0x[0-9a-fA-F]+)\\s+#"));
for (const QString &line : Core()->cmdRaw("Cr*").split(QLatin1Char('\n'))) {
const auto match = commandPin.match(line);
if (match.hasMatch()) {
bool ok = false;
const RVA address = match.captured(1).toULongLong(&ok, 16);
if (ok) {
addPin(address, Core()->getCommandAnnotation(address), true);
}
}
}
ui->structurePinsTree->resizeColumnToContents(0);
if (!structureType.isEmpty()) {
inspectStructure();
}
}
void HexdumpWidget::inspectStructure()
{
const QString type = ui->structureTypeComboBox->currentText().trimmed();
const QString expression = ui->structureAddressEdit->text().trimmed();
RVA address = current_address;
const bool validAddress = expression.isEmpty() || Core()->tryMath(expression, address);
QSet<QString> expanded;
QString selectedPath;
const bool sameStructure = type == structureType && validAddress && address == structureAddress
&& ui->structureTree->topLevelItemCount() > 0;
if (sameStructure) {
const auto remember = [&expanded](const auto &self, QTreeWidgetItem *item) -> void {
if (item->isExpanded()) {
expanded.insert(item->data(FieldColumn, PathRole).toString());
}
for (int i = 0; i < item->childCount(); ++i) {
self(self, item->child(i));
}
};
for (int i = 0; i < ui->structureTree->topLevelItemCount(); ++i) {
remember(remember, ui->structureTree->topLevelItem(i));
}
if (auto *selected = ui->structureTree->currentItem()) {
selectedPath = selected->data(FieldColumn, PathRole).toString();
}
}
QSignalBlocker blocker(ui->structureTree);
ui->structureTree->clear();
ui->hexTextView->setTypeRanges({});
structureType = type;
structureAddress = validAddress ? address : RVA_INVALID;
if (!typeLayout.size(structureType)) {
ui->structureStatusLabel->setText(tr("Choose a known type with a valid, finite layout."));
return;
}
if (!validAddress || !Core()->isAddressMapped(structureAddress)) {
ui->structureStatusLabel->setText(tr("Enter a valid address expression in mapped memory."));
return;
}
auto *root = addStructureField(
ui->structureTree->invisibleRootItem(), structureType, structureType, structureAddress);
populateStructure(root);
root->setExpanded(
!sameStructure || expanded.contains(root->data(FieldColumn, PathRole).toString()));
const auto restore =
[this, &expanded, &selectedPath](const auto &self, QTreeWidgetItem *item) -> void {
const QString path = item->data(FieldColumn, PathRole).toString();
if (expanded.contains(path)) {
populateStructure(item);
item->setExpanded(true);
}
if (!selectedPath.isEmpty() && path == selectedPath) {
ui->structureTree->setCurrentItem(item);
}
for (int i = 0; i < item->childCount(); ++i) {
self(self, item->child(i));
}
};
restore(restore, root);
updateStructureHighlights();
ui->structureStatusLabel->setText(
tr("%1 at %2. Expand fields; select a row to highlight its bytes.")
.arg(structureType, RAddressString(structureAddress)));
}
QTreeWidgetItem *HexdumpWidget::addStructureField(
QTreeWidgetItem *parent, const QString &name, const QString &type, RVA address)
{
auto *item = new QTreeWidgetItem(parent);
const RVA size = typeLayout.size(type);
item->setText(FieldColumn, name);
item->setText(TypeColumn, type);
item->setText(AddressColumn, RAddressString(address));
item->setText(SizeColumn, QString::number(size));
const QString path = parent->data(FieldColumn, PathRole).toString() + QLatin1Char('.') + name;
item->setData(FieldColumn, PathRole, path);
item->setBackground(FieldColumn, fieldColor(path));
if (!size || size - 1 > RVA_MAX - address) {
item->setText(ValueColumn, tr("Invalid layout / address overflow"));
return item;
}
item->setData(AddressColumn, AddressRole, QVariant::fromValue(address));
item->setData(SizeColumn, SizeRole, QVariant::fromValue(size));
const QString kind = typeLayout.kind(type);
bool expandable = kind == QLatin1String("struct") || kind == QLatin1String("union")
|| kind == QLatin1String("array") || kind == QLatin1String("pointer");
if (kind != QLatin1String("struct") && kind != QLatin1String("union")
&& kind != QLatin1String("array")) {
const int length = int(qMin<RVA>(size, 16));
if (!Core()->isAddressMapped(address) || !Core()->isAddressMapped(address + length - 1)) {
item->setText(ValueColumn, tr("Unmapped"));
expandable = false;
} else {
const QByteArray bytes = Core()->ioRead(address, length);
item->setText(
ValueColumn,
bytes.size() == length
? typeLayout.value(type, bytes, Core()->getConfigb("cfg.bigendian"))
: tr("Unreadable"));
if (size > RVA(length)) {
item->setText(ValueColumn, item->text(ValueColumn) + QStringLiteral(" …"));
}
}
}
int depth = 0;
for (auto *ancestor = parent; ancestor; ancestor = ancestor->parent()) {
++depth;
if (ancestor->data(AddressColumn, AddressRole).isValid()
&& ancestor->data(AddressColumn, AddressRole).toULongLong() == address
&& typeLayout.resolve(ancestor->text(TypeColumn)) == typeLayout.resolve(type)) {
expandable = false;
item->setText(ValueColumn, tr("Recursive reference"));
break;
}
}
if (depth >= 32) {
expandable = false;
item->setText(ValueColumn, tr("Depth limit reached"));
}
if (expandable) {
new QTreeWidgetItem(item); // Expander; children are read only when opened.
}
for (int column = FieldColumn; column <= SizeColumn; ++column) {
item->setToolTip(
column,
tr("%1\nType: %2\nAddress: %3\nSize: %4 bytes")
.arg(path, type, RAddressString(address), QString::number(size)));
}
return item;
}
void HexdumpWidget::populateStructure(QTreeWidgetItem *item)
{
if (!item || item->data(FieldColumn, PopulatedRole).toBool()
|| !item->data(AddressColumn, AddressRole).isValid() || item->childCount() == 0) {
return;
}
item->setData(FieldColumn, PopulatedRole, true);
qDeleteAll(item->takeChildren());
const QString type = typeLayout.resolve(item->text(TypeColumn));
const RVA address = item->data(AddressColumn, AddressRole).toULongLong();
QString element;
quint64 count;
if (TypeLayout::array(type, &element, &count)) {
const RVA stride = typeLayout.size(element);
// Group large arrays hierarchically, keeping every element reachable.
const quint64 groupSize = count > 256 ? count / 256 + (count % 256 != 0) : 1;
for (quint64 index = 0; index < count;) {
const quint64 groupCount = qMin(groupSize, count - index);
addStructureField(
item,
groupCount == 1 ? QStringLiteral("[%1]").arg(index)
: QStringLiteral("[%1…%2]").arg(index).arg(index + groupCount - 1),
groupCount == 1 ? element : arrayType(element, groupCount),
address + index * stride);
index += groupCount;
}
} else if (typeLayout.kind(type) == QLatin1String("pointer")) {
const int size = int(typeLayout.size(type));
const QByteArray bytes = Core()->ioRead(address, size);
const RVA target = TypeLayout::unsignedValue(bytes, Core()->getConfigb("cfg.bigendian"));
if (bytes.size() == size && target && Core()->isAddressMapped(target)) {
addStructureField(
item,
QStringLiteral("*%1").arg(item->text(FieldColumn)),
type.left(type.size() - 1).trimmed(),
target);
} else {
item->setText(ValueColumn, item->text(ValueColumn) + tr(" (no mapped pointee)"));
}
} else {
for (const auto &field : typeLayout.fields(type)) {
addStructureField(
item,
field.name,
field.count > 1 ? arrayType(field.type, field.count) : field.type,
address + field.offset);
}
}
updateStructureHighlights();
}
void HexdumpWidget::updateStructureHighlights()
{
QVector<HexWidget::ColoredRange> ranges;
const auto visit = [this, &ranges](const auto &self, QTreeWidgetItem *item) -> void {
bool hasChildren = false;
if (item->isExpanded() && item->data(FieldColumn, PopulatedRole).toBool()) {
for (int i = 0; i < item->childCount(); ++i) {
self(self, item->child(i));
hasChildren = true;
}
}
if ((!hasChildren || typeLayout.kind(item->text(TypeColumn)) == QLatin1String("pointer"))
&& item->data(AddressColumn, AddressRole).isValid()) {
const RVA address = item->data(AddressColumn, AddressRole).toULongLong();
const RVA size = item->data(SizeColumn, SizeRole).toULongLong();
ranges.append(
{address,
address + size - 1,
fieldColor(item->data(FieldColumn, PathRole).toString())});
}
};
for (int i = 0; i < ui->structureTree->topLevelItemCount(); ++i) {
visit(visit, ui->structureTree->topLevelItem(i));
}
ui->hexTextView->setTypeRanges(ranges);
}
void HexdumpWidget::selectStructureField(QTreeWidgetItem *item)
{
if (!item || !item->data(AddressColumn, AddressRole).isValid()) {
return;
}
const RVA address = item->data(AddressColumn, AddressRole).toULongLong();
const RVA size = item->data(SizeColumn, SizeRole).toULongLong();
// Keep the inspected root anchored while the hex cursor and seek move to a field.
updatingStructures = true;
sent_seek = true;
ui->hexTextView->seek(address);
ui->hexTextView->selectRange(address, address + size - 1);
seekable->seek(address);
sent_seek = false;
updatingStructures = false;
onHexSelectionChanged(ui->hexTextView->getSelection());
}
void HexdumpWidget::activateStructurePin(QTreeWidgetItem *item)
{
if (!item) {
return;
}
const RVA address = item->data(0, Qt::UserRole).toULongLong();
seekable->seek(address);
ui->structureAddressEdit->setText(RAddressString(address));
if (!item->data(1, Qt::UserRole).toBool()) {
ui->structureTypeComboBox->setCurrentText(item->text(1));
inspectStructure();
}
}
void HexdumpWidget::pinStructure()
{
inspectStructure();
const QString entered = ui->structureTypeComboBox->currentText().trimmed();
const QString type = typeLayout.names().contains(entered) ? entered
: typeLayout.resolve(entered);
static const QRegularExpression safeType(QStringLiteral("^[\\p{L}\\p{N}_ :*]+$"));
if (ui->structureTree->topLevelItemCount() == 0 || !typeLayout.names().contains(type)
|| !safeType.match(type).hasMatch()) {
ui->structureStatusLabel->setText(
tr("Pin a named r2 type. Define a typedef to pin an array."));
return;
}
Core()->cmdRaw(QStringLiteral("tl %1 = %2").arg(type, RAddressString(structureAddress)));
emit Core() -> refreshCodeViews();
reloadStructures();
}
void HexdumpWidget::pinCommand()
{
const QString expression = ui->structureAddressEdit->text().trimmed();
RVA address = current_address;
if ((!expression.isEmpty() && !Core()->tryMath(expression, address))
|| !Core()->isAddressMapped(address)) {
ui->structureStatusLabel->setText(tr("Enter a valid command address in mapped memory."));
return;
}
bool accepted = false;
const QString command = QInputDialog::getText(
this,
tr("Pin command"),
tr("r2 command at %1:").arg(RAddressString(address)),
QLineEdit::Normal,
Core()->getCommandAnnotation(address),
&accepted)
.trimmed();
if (!accepted || command.isEmpty()) {
return;
}
const auto selection = ui->hexTextView->getSelection();
const RVA size = !selection.empty && selection.startAddress == address
? selection.endAddress - address + 1
: qMax<RVA>(1, typeLayout.size(ui->structureTypeComboBox->currentText()));
if (size - 1 > RVA_MAX - address) {
return;
}
Core()->setCommandAnnotation(address, size, command);
reloadStructures();
}
void HexdumpWidget::removeStructurePin()
{
const auto *item = ui->structurePinsTree->currentItem();
if (!item) {
return;
}
const RVA address = item->data(0, Qt::UserRole).toULongLong();
Core()->cmdRawAt(
item->data(1, Qt::UserRole).toBool() ? "Cr-" : "tl- " + RAddressString(address), address);
emit Core() -> refreshCodeViews();
reloadStructures();
}
void HexdumpWidget::clearParseWindow()
{
ui->hexDisasTextEdit->setPlainText("");
+20 -2
View File
@@ -7,7 +7,6 @@
#include <QTextEdit>
#include <array>
#include <functional>
#include <memory>
#include "HexWidget.h"
@@ -15,6 +14,7 @@
#include "common/Highlighter.h"
#include "common/IaitoSeekable.h"
#include "common/SvgIconEngine.h"
#include "common/TypeLayout.h"
#include "core/Iaito.h"
#include "Dashboard.h"
@@ -25,6 +25,7 @@ class HexdumpWidget;
// Forward declarations
class QLineEdit;
class QTreeWidgetItem;
class RefreshDeferrer;
class QSyntaxHighlighter;
@@ -38,7 +39,6 @@ public:
Highlighter *highlighter;
static QString getWidgetType();
static void setCustomizeHook(const std::function<void(HexdumpWidget *)> &hook);
public slots:
void initParsing();
@@ -77,6 +77,24 @@ private:
void writeValueEdit(QLineEdit *editor, int byteCount, bool isSigned);
IOModesController ioModesController;
TypeLayout typeLayout;
RVA structureAddress = RVA_INVALID;
QString structureType;
bool updatingStructures = false;
bool structurePanelSized = false;
void initStructures();
void reloadStructures();
void inspectStructure();
void populateStructure(QTreeWidgetItem *item);
QTreeWidgetItem *addStructureField(
QTreeWidgetItem *parent, const QString &name, const QString &type, RVA address);
void updateStructureHighlights();
void selectStructureField(QTreeWidgetItem *item);
void activateStructurePin(QTreeWidgetItem *item);
void pinStructure();
void pinCommand();
void removeStructurePin();
private slots:
void onSeekChanged(RVA addr);
+102
View File
@@ -986,6 +986,108 @@ li.checked::marker { content: &quot;\2612&quot;; }
</layout>
</widget>
</widget>
<widget class="QWidget" name="tabStructures">
<attribute name="title">
<string>Structures</string>
</attribute>
<layout class="QVBoxLayout" name="structuresLayout">
<item>
<widget class="QComboBox" name="structureTypeComboBox">
<property name="toolTip"><string>Choose or enter an r2 type, pointer, or array</string></property>
<property name="editable"><bool>true</bool></property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="structureAddressLayout">
<item>
<widget class="QLineEdit" name="structureAddressEdit">
<property name="placeholderText"><string>Address or expression</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="structureCursorButton">
<property name="text"><string>At cursor</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="structureInspectButton">
<property name="text"><string>Inspect</string></property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="structurePinLayout">
<item>
<widget class="QPushButton" name="structurePinButton">
<property name="text"><string>Pin type</string></property>
<property name="toolTip"><string>Link this type to the address in r2 (tl)</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="structureCommandButton">
<property name="text"><string>Pin command…</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="structureRefreshButton">
<property name="text"><string>Refresh</string></property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSplitter" name="structureSplitter">
<property name="orientation"><enum>Qt::Vertical</enum></property>
<widget class="QTreeWidget" name="structureTree">
<property name="alternatingRowColors"><bool>true</bool></property>
<property name="uniformRowHeights"><bool>true</bool></property>
<property name="contextMenuPolicy"><enum>Qt::CustomContextMenu</enum></property>
<column><property name="text"><string>Field</string></property></column>
<column><property name="text"><string>Type</string></property></column>
<column><property name="text"><string>Value</string></property></column>
<column><property name="text"><string>Address</string></property></column>
<column><property name="text"><string>Size</string></property></column>
</widget>
<widget class="QTreeWidget" name="structurePinsTree">
<property name="toolTip"><string>Pinned r2 type links and command annotations. Double-click to navigate.</string></property>
<property name="rootIsDecorated"><bool>false</bool></property>
<property name="contextMenuPolicy"><enum>Qt::CustomContextMenu</enum></property>
<column><property name="text"><string>Pinned address</string></property></column>
<column><property name="text"><string>Type / command</string></property></column>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="structureCommandLayout">
<item>
<widget class="QPushButton" name="structureRunButton">
<property name="text"><string>Run command</string></property>
<property name="enabled"><bool>false</bool></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="structureUnpinButton">
<property name="text"><string>Unpin</string></property>
<property name="enabled"><bool>false</bool></property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPlainTextEdit" name="structureCommandOutput">
<property name="readOnly"><bool>true</bool></property>
<property name="maximumSize"><size><width>16777215</width><height>140</height></size></property>
</widget>
</item>
<item>
<widget class="QLabel" name="structureStatusLabel">
<property name="text"><string>Choose a type and an address, then inspect. Expand fields to explore.</string></property>
<property name="wordWrap"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QToolButton" name="openSideViewB">
<property name="sizePolicy">
+2 -2
View File
@@ -28,7 +28,7 @@
<number>0</number>
</property>
<item>
<widget class="AddressableItemList&lt;&gt;" name="treeView">
<widget class="AddressableItemView" name="treeView">
<property name="styleSheet">
<string notr="true">IaitoTreeView::item
{
@@ -58,7 +58,7 @@
</widget>
<customwidgets>
<customwidget>
<class>AddressableItemList&lt;&gt;</class>
<class>AddressableItemView</class>
<extends>QTreeView</extends>
<header>widgets/AddressableItemList.h</header>
<container>1</container>
+2 -2
View File
@@ -31,7 +31,7 @@
<number>0</number>
</property>
<item>
<widget class="AddressableItemList&lt;&gt;" name="searchTreeView">
<widget class="AddressableItemView" name="searchTreeView">
<property name="styleSheet">
<string notr="true">IaitoTreeView::item
{
@@ -118,7 +118,7 @@
</widget>
<customwidgets>
<customwidget>
<class>AddressableItemList&lt;&gt;</class>
<class>AddressableItemView</class>
<extends>QTreeView</extends>
<header>widgets/AddressableItemList.h</header>
<container>1</container>
+6 -6
View File
@@ -365,7 +365,7 @@ void AbstractAddrDock::updateDock()
textColor = ConfigColor("gui.dataoffset");
int y = 0;
int validMinSize = getValidMinSize();
RVA validMinSize = getValidMinSize();
int rectWidth = getRectWidth();
proxyModel->sort(SectionsModel::AddressColumn, Qt::AscendingOrder);
for (int i = 0; i < proxyModel->rowCount(); ++i) {
@@ -410,10 +410,10 @@ void AbstractAddrDock::addTextItem(QColor color, QPoint pos, QString string)
addrDockScene->addItem(text);
}
int AbstractAddrDock::getAdjustedSize(int size, int validMinSize)
int AbstractAddrDock::getAdjustedSize(RVA size, RVA validMinSize)
{
if (size == 0) {
return size;
if (size == 0 || validMinSize == 0) {
return 0;
}
if (size == validMinSize) {
return heightThreshold;
@@ -434,12 +434,12 @@ int AbstractAddrDock::getIndicatorWidth()
return getRectWidth() + 200;
}
int AbstractAddrDock::getValidMinSize()
RVA AbstractAddrDock::getValidMinSize()
{
proxyModel->sort(SectionsModel::SizeColumn, Qt::AscendingOrder);
for (int i = 0; i < proxyModel->rowCount(); i++) {
QModelIndex idx = proxyModel->index(i, 0);
int size = getSizeOfSection(
RVA size = getSizeOfSection(
idx.data(SectionsModel::SectionDescriptionRole).value<SectionDescription>());
if (size > 0) {
return size;
+2 -2
View File
@@ -143,10 +143,10 @@ protected:
SectionsProxyModel *proxyModel;
void addTextItem(QColor color, QPoint pos, QString string);
int getAdjustedSize(int size, int validMinSize);
int getAdjustedSize(RVA size, RVA validMinSize);
int getRectWidth();
int getIndicatorWidth();
int getValidMinSize();
RVA getValidMinSize();
virtual RVA getSizeOfSection(const SectionDescription &section) = 0;
virtual RVA getAddressOfSection(const SectionDescription &section) = 0;
+2 -2
View File
@@ -31,7 +31,7 @@
<number>0</number>
</property>
<item>
<widget class="AddressableItemList&lt;&gt;" name="stringsTreeView">
<widget class="AddressableItemView" name="stringsTreeView">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
@@ -84,7 +84,7 @@
</widget>
<customwidgets>
<customwidget>
<class>AddressableItemList&lt;&gt;</class>
<class>AddressableItemView</class>
<extends>QTreeView</extends>
<header>widgets/AddressableItemList.h</header>
<container>1</container>
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Check version queries and argument errors without starting the GUI event loop."""
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
binary = str(Path(sys.argv[1] if len(sys.argv) > 1 else "build/iaito").resolve())
with tempfile.TemporaryDirectory() as directory:
env = dict(os.environ, QT_QPA_PLATFORM="offscreen", XDG_CONFIG_HOME=directory,
XDG_CACHE_HOME=directory, XDG_DATA_HOME=directory, R2_NOPLUGINS="1")
def run(args, status, message=None):
result = subprocess.run([binary, *args], env=env, capture_output=True,
text=True, timeout=15)
assert result.returncode == status, (args, result)
assert "QThreadStorage:" not in result.stderr, (args, result.stderr)
if message:
assert message in result.stderr, (args, result.stderr)
else:
assert re.fullmatch(r"iaito \S+\n", result.stdout), (args, result.stdout)
for args in [("--version",), ("-v",), ("--no-plugins", "--version"),
("--version", "--help"), ("-A", "99", "--version")]:
run(args, 0)
run(("-A", "99"), 1, "Invalid Analysis Level")
run(("-A", "0"), 1, "Filename must be specified")
# A value that looks like a flag must not trigger version handling.
run(("-i", "--version", "-A", "99"), 1, "Invalid Analysis Level")
print("Command-line checks passed")
+11
View File
@@ -0,0 +1,11 @@
include ../../config.mk
.PHONY: all check
all:
$(MAKE) -C ../.. -j8 > /dev/null
mkdir -p build
cd build && $(QMAKE) ../decompilercolumns.pro
$(MAKE) -C build -j8 > /dev/null
check: all
QT_QPA_PLATFORM=offscreen ./build/test_decompilercolumns
@@ -0,0 +1,15 @@
QT += core gui widgets network svg testlib
greaterThan(QT_MAJOR_VERSION, 5): QT += svgwidgets
CONFIG += console testcase c++20 link_pkgconfig
CONFIG -= app_bundle
TEMPLATE = app
TARGET = test_decompilercolumns
DEFINES += IAITO_SOURCE_BUILD
PKGCONFIG += r_core
INCLUDEPATH += ../../src ../../src/core ../../src/common ../../src/widgets ../../build
SOURCES += test_decompilercolumns.cpp
IAITO_BUILD = $$clean_path($$PWD/../../build)
IAITO_OBJECTS = $$files($$IAITO_BUILD/*.o)
IAITO_OBJECTS -= $$IAITO_BUILD/Main.o $$IAITO_BUILD/main.o
LIBS += $$IAITO_OBJECTS
PRE_TARGETDEPS += $$IAITO_OBJECTS
@@ -0,0 +1,761 @@
#include "common/Configuration.h"
#include "common/DecompilerHighlighter.h"
#include "common/IaitoSeekable.h"
#include "common/R2pdcCmdDecompiler.h"
#include "common/ShortcutManager.h"
#include "common/TextEditDialog.h"
#include "core/MainWindow.h"
#include "dialogs/CommentsDialog.h"
#include "menus/DecompilerContextMenu.h"
#include "menus/DisassemblyContextMenu.h"
#include "widgets/DecompilerWidget.h"
#include <algorithm>
#include <QClipboard>
#include <QFileInfo>
#include <QPlainTextEdit>
#include <QSettings>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QTest>
#include <QTextBlock>
#include <QTextLayout>
#include <QTimer>
#include <QUrl>
class TestDecompilerWidget : public DecompilerWidget
{
public:
explicit TestDecompilerWidget(MainWindow *main)
: DecompilerWidget(main)
{}
DecompilerContextMenu *contextMenu() const { return mCtxMenu; }
bool showCode(RCodeMeta *code)
{
return QMetaObject::invokeMethod(
this, "decompilationFinished", Qt::DirectConnection, Q_ARG(RCodeMeta *, code));
}
};
class HeaderCommentDecompiler : public Decompiler
{
public:
int calls = 0;
HeaderCommentDecompiler()
: Decompiler("test-header-comment", "Header comment test")
{}
RCodeMeta *decompileSync(RVA address) override
{
calls++;
const QString text = Core()->getCommentAt(address) + "\nfirst();\n";
RCodeMeta *code = r_codemeta_new(text.toUtf8().constData());
RCodeMetaItem *offset = r_codemeta_item_new();
offset->type = R_CODEMETA_TYPE_OFFSET;
offset->start = text.indexOf("first");
offset->end = text.size();
offset->offset.offset = address + 4;
r_codemeta_add_item(code, offset);
return code;
}
void decompileAt(RVA address) override { emit finished(decompileSync(address)); }
};
class DecompilerColumnTests : public QObject
{
Q_OBJECT
QTemporaryDir settings;
HeaderCommentDecompiler *headerDecompiler = nullptr;
static void columnCombinations()
{
QTest::addColumn<bool>("addresses");
QTest::addColumn<bool>("disassembly");
QTest::newRow("plain") << false << false;
QTest::newRow("addresses") << true << false;
QTest::newRow("disassembly") << false << true;
QTest::newRow("both") << true << true;
}
static QString lineContaining(QPlainTextEdit *edit, const QString &needle)
{
for (const QString &line : edit->toPlainText().split(QLatin1Char('\n'))) {
if (line.contains(needle)) {
return line;
}
}
return {};
}
static RCodeMeta *hoverCode(const QString &token, int type = -1)
{
const QString text = "// references\n " + token + " ();\n";
RCodeMeta *code = r_codemeta_new(text.toUtf8().constData());
RCodeMetaItem *offset = r_codemeta_item_new();
offset->type = R_CODEMETA_TYPE_OFFSET;
offset->end = text.size();
offset->offset.offset = 0;
r_codemeta_add_item(code, offset);
if (type >= 0) {
RCodeMetaItem *reference = r_codemeta_item_new();
reference->type = static_cast<RCodeMetaItemType>(type);
reference->start = text.indexOf(token);
reference->end = reference->start + token.size();
if (type == R_CODEMETA_TYPE_LOCAL_VARIABLE) {
reference->variable.name = strdup(token.toUtf8().constData());
} else {
reference->reference.offset = 0x20;
if (type == R_CODEMETA_TYPE_FUNCTION_NAME) {
reference->reference.name = strdup(token.toUtf8().constData());
}
}
r_codemeta_add_item(code, reference);
}
return code;
}
static QString hoveredText(QPlainTextEdit *edit)
{
for (const auto &selection : edit->extraSelections()) {
if (selection.format.boolProperty(QTextFormat::UserProperty)) {
return selection.cursor.selectedText();
}
}
return {};
}
private slots:
void initTestCase()
{
QCoreApplication::setOrganizationName("iaito-tests");
QCoreApplication::setApplicationName("decompilercolumns");
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, settings.path());
Core()->initialize(false);
qRegisterMetaType<RVA>("RVA");
Config()->loadInitial();
Config()->setOutputRedirectionEnabled(false);
Config()->setDecompilerAutoRefreshEnabled(false);
QVERIFY(Core()->registerDecompiler(new R2pdcCmdDecompiler));
headerDecompiler = new HeaderCommentDecompiler;
QVERIFY(Core()->registerDecompiler(headerDecompiler));
Config()->setSelectedDecompiler("pdc");
Core()->cmd("o malloc://64");
Core()->setConfig("asm.arch", "x86");
Core()->setConfig("asm.bits", 64);
Core()->setConfig("dir.cache", settings.path() + "/annotations");
Core()->cmd("wx 554889e5b82a0000005dc3 @ 0");
Core()->cmd("ompg +x");
Core()->cmd("af @ 0");
Core()->cmd("wx c3 @ 0x20");
Core()->cmd("af @ 0x20");
}
void init()
{
Config()->setDecompilerShowAddresses(false);
Config()->setDecompilerShowDisassembly(false);
Config()->setSelectedDecompiler("pdc");
Core()->seek(0);
Core()->cmdRawAt("ano-", 0);
Core()->cmdRawAt("ano-", 0x20);
Core()->cmdRawAt("CC-", 0);
Core()->cmdRawAt("CC-", 4);
Core()->cmdRawAt("CC-", 0x20);
}
void pdcColumnsAndNavigation_data() { columnCombinations(); }
void pdcColumnsAndNavigation()
{
QFETCH(bool, addresses);
QFETCH(bool, disassembly);
MainWindow main;
TestDecompilerWidget widget(&main);
widget.getSeekable()->setSynchronization(false);
auto *edit = widget.findChild<QPlainTextEdit *>("textEdit");
QVERIFY(edit);
QVERIFY(widget.showCode(Core()->getDecompilerById("pdc")->decompileSync(0)));
const QString plain = edit->toPlainText();
QVERIFY(plain.contains("eax = 0x2a"));
DecompilerContextMenu *menu = widget.contextMenu();
QVERIFY(QMetaObject::invokeMethod(menu, "aboutToShowSlot", Qt::DirectConnection));
auto *addressAction = menu->findChild<QAction *>("actionShowAddresses");
auto *assemblyAction = menu->findChild<QAction *>("actionShowDisassembly");
QVERIFY(addressAction && addressAction->isCheckable());
QVERIFY(assemblyAction && assemblyAction->isCheckable());
QVERIFY(!addressAction->isChecked());
QVERIFY(!assemblyAction->isChecked());
if (addresses) {
addressAction->trigger();
}
if (disassembly) {
assemblyAction->trigger();
}
QCOMPARE(widget.isAddressColumnVisible(), addresses);
QCOMPARE(widget.isDisassemblyColumnVisible(), disassembly);
QCOMPARE(addressAction->isChecked(), addresses);
QCOMPARE(assemblyAction->isChecked(), disassembly);
const QString row = lineContaining(edit, "eax = 0x2a");
QCOMPARE(row.contains(RAddressString(4)), addresses);
QCOMPARE(row.contains("mov eax, 0x2a"), disassembly);
const int token = disassembly ? edit->toPlainText().indexOf("mov eax, 0x2a")
: edit->toPlainText().indexOf("eax = 0x2a");
QVERIFY(token >= 0);
QTextCursor cursor(edit->document());
cursor.setPosition(token + 1);
edit->setTextCursor(cursor);
QCOMPARE(widget.getSeekable()->getOffset(), RVA(4));
QCOMPARE(Core()->getOffset(), RVA(0));
widget.setAddressColumnVisible(false);
widget.setDisassemblyColumnVisible(false);
QCOMPARE(edit->toPlainText(), plain);
QCOMPARE(widget.getSeekable()->getOffset(), RVA(4));
}
void referenceAndHighlightRanges_data() { columnCombinations(); }
void referenceAndHighlightRanges()
{
QFETCH(bool, addresses);
QFETCH(bool, disassembly);
RCodeMeta *code = r_codemeta_new("callee();\nreturn;\n");
RCodeMetaItem *offset = r_codemeta_item_new();
offset->type = R_CODEMETA_TYPE_OFFSET;
offset->start = 0;
offset->end = 18;
offset->offset.offset = 0;
r_codemeta_add_item(code, offset);
RCodeMetaItem *reference = r_codemeta_item_new();
reference->type = R_CODEMETA_TYPE_FUNCTION_NAME;
reference->start = 0;
reference->end = 6;
reference->reference.offset = 0x20;
reference->reference.name = strdup("callee");
r_codemeta_add_item(code, reference);
RCodeMetaItem *highlight = r_codemeta_item_new();
highlight->type = R_CODEMETA_TYPE_SYNTAX_HIGHLIGHT;
highlight->start = 0;
highlight->end = 6;
highlight->syntax_highlight.type = R_SYNTAX_HIGHLIGHT_TYPE_FUNCTION_NAME;
r_codemeta_add_item(code, highlight);
MainWindow main;
TestDecompilerWidget widget(&main);
QVERIFY(widget.showCode(code));
widget.setAddressColumnVisible(addresses);
widget.setDisassemblyColumnVisible(disassembly);
auto *edit = widget.findChild<QPlainTextEdit *>("textEdit");
QVERIFY(edit);
// An offset annotation spanning several lines applies to each line.
QCOMPARE(lineContaining(edit, "return;").contains(RAddressString(0)), addresses);
QCOMPARE(lineContaining(edit, "return;").contains("push rbp"), disassembly);
// Exercise repeated remapping while a context reference is selected.
for (int i = 0; i < 3; ++i) {
const int token = edit->toPlainText().indexOf("callee");
QTextCursor cursor(edit->document());
cursor.setPosition(token + 2);
edit->setTextCursor(cursor);
auto *menu = widget.contextMenu();
QVERIFY(QMetaObject::invokeMethod(menu, "aboutToShowSlot", Qt::DirectConnection));
QAction *copyReference = nullptr;
for (QAction *action : menu->actions()) {
if (action->text().startsWith("Copy address of")) {
copyReference = action;
}
}
QVERIFY(copyReference && copyReference->isVisible());
copyReference->trigger();
QCOMPARE(QApplication::clipboard()->text(), RAddressString(0x20));
const auto highlighters = widget.findChildren<DecompilerHighlighter *>();
QVERIFY(!highlighters.isEmpty());
highlighters.last()->rehighlight();
const auto ranges = edit->document()->firstBlock().layout()->formats();
QVERIFY(std::any_of(ranges.begin(), ranges.end(), [token](const auto &range) {
return range.start == token && range.length == 6;
}));
widget.setAddressColumnVisible(!widget.isAddressColumnVisible());
widget.setDisassemblyColumnVisible(!widget.isDisassemblyColumnVisible());
}
}
void rememberColumnsAndLeaveWarningsReadable()
{
MainWindow main;
{
TestDecompilerWidget widget(&main);
widget.setAddressColumnVisible(true);
widget.setDisassemblyColumnVisible(true);
}
TestDecompilerWidget widget(&main);
QVERIFY(widget.isAddressColumnVisible());
QVERIFY(widget.isDisassemblyColumnVisible());
auto *edit = widget.findChild<QPlainTextEdit *>("textEdit");
QVERIFY(edit);
QVERIFY(widget.showCode(Decompiler::makeWarning("No function found")));
QCOMPARE(edit->toPlainText(), QString("No function found"));
}
void hoverAndFollowReference_data()
{
QTest::addColumn<bool>("addresses");
QTest::addColumn<bool>("disassembly");
QTest::addColumn<int>("type");
QTest::addColumn<QString>("token");
for (bool addresses : {false, true}) {
for (bool disassembly : {false, true}) {
for (int type :
{-1,
int(R_CODEMETA_TYPE_FUNCTION_NAME),
int(R_CODEMETA_TYPE_GLOBAL_VARIABLE),
int(R_CODEMETA_TYPE_CONSTANT_VARIABLE),
int(R_CODEMETA_TYPE_LOCAL_VARIABLE)}) {
const QByteArray name
= QString("%1-%2-%3").arg(addresses).arg(disassembly).arg(type).toLatin1();
QTest::newRow(name.constData()) << addresses << disassembly << type
<< QString(type < 0 ? "0X20" : "callee");
}
}
}
QTest::newRow("flag") << false << false << -1 << QString("hover.flag");
QTest::newRow("function") << false << false << -1 << QString("hover_function");
QTest::newRow("import") << false << false << -1 << QString("hover_import");
QTest::newRow("sanitized-import")
<< false << false << -1 << QString("sym_imp_hover_import");
QTest::newRow("sanitized-symbol") << false << false << -1 << QString("hover_symbol");
QTest::newRow("cpp-method") << false << false << -1 << QString("HoverClass::method");
QTest::newRow("sanitized-method") << false << false << -1 << QString("HoverClass__method");
}
void hoverAndFollowReference()
{
QFETCH(bool, addresses);
QFETCH(bool, disassembly);
QFETCH(int, type);
QFETCH(QString, token);
Core()->cmd("f hover.flag 1 @ 0x20");
Core()->cmd("f callee 1 @ 0x20");
Core()->cmd("f sym.imp.hover_import 1 @ 0x20");
Core()->cmd("f sym.hover.symbol 1 @ 0x20");
Core()->cmd("f sym.HoverClass::method 1 @ 0x20");
Core()->cmd("afn hover_function @ 0x20");
MainWindow main;
TestDecompilerWidget widget(&main);
widget.getSeekable()->setSynchronization(false);
widget.setParent(nullptr);
widget.resize(1000, 500);
widget.show();
QVERIFY(QTest::qWaitForWindowExposed(&widget));
QVERIFY(widget.showCode(hoverCode(token, type)));
widget.setAddressColumnVisible(addresses);
widget.setDisassemblyColumnVisible(disassembly);
auto *edit = widget.findChild<QPlainTextEdit *>("textEdit");
QVERIFY(edit);
const int start = edit->toPlainText().indexOf(token);
QTextCursor cursor(edit->document());
cursor.setPosition(start);
const QRect first = edit->cursorRect(cursor);
cursor.setPosition(start + token.size());
const QRect end = edit->cursorRect(cursor);
const int y = first.center().y();
QTest::mouseMove(edit->viewport(), QPoint(first.left() - 2, y));
QCOMPARE(edit->viewport()->cursor().shape(), Qt::ArrowCursor);
QTest::mouseMove(edit->viewport(), QPoint(end.left() + 2, y));
QCOMPARE(edit->viewport()->cursor().shape(), Qt::ArrowCursor);
const bool actionable = type != R_CODEMETA_TYPE_LOCAL_VARIABLE;
const QPoint point(first.left() + 2, y);
QTest::mouseMove(edit->viewport(), point);
QCOMPARE(
edit->viewport()->cursor().shape(),
actionable ? Qt::PointingHandCursor : Qt::ArrowCursor);
QCOMPARE(hoveredText(edit), actionable ? token : QString());
QTest::mouseMove(edit->viewport(), QPoint(end.left() - 2, y));
QCOMPARE(hoveredText(edit), actionable ? token : QString());
// Cursor highlights must preserve the independent hover selection.
edit->moveCursor(QTextCursor::End);
QCOMPARE(hoveredText(edit), actionable ? token : QString());
QTest::mouseClick(edit->viewport(), Qt::LeftButton, Qt::NoModifier, point);
QCOMPARE(widget.getSeekable()->getOffset(), RVA(actionable ? 0x20 : 0));
QCOMPARE(Core()->getOffset(), RVA(0));
if (actionable) {
// A double-click must not follow new text that replaces the clicked reference.
QVERIFY(widget.showCode(hoverCode("0x30")));
QTest::mouseDClick(edit->viewport(), Qt::LeftButton, Qt::NoModifier, point);
QTest::mouseRelease(edit->viewport(), Qt::LeftButton, Qt::NoModifier, point);
QCOMPARE(widget.getSeekable()->getOffset(), RVA(0x20));
}
QEvent leave(QEvent::Leave);
QApplication::sendEvent(edit->viewport(), &leave);
QCOMPARE(edit->viewport()->cursor().shape(), Qt::ArrowCursor);
QVERIFY(hoveredText(edit).isEmpty());
}
void keyboardFollowsReference_data()
{
QTest::addColumn<QString>("token");
QTest::addColumn<int>("type");
QTest::addColumn<int>("key");
QUrl link = QUrl::fromLocalFile(QFileInfo(Core()->getFilePath()).absoluteFilePath());
link.setScheme("iaito");
link.setQuery("addr=0x20");
for (Qt::Key key : {Qt::Key_Return, Qt::Key_Enter}) {
for (const QString &token :
{QString("0x20"),
QString("keyboard.flag"),
QString("callee"),
link.toString(QUrl::FullyEncoded)}) {
const QByteArray name = token.toUtf8() + '-' + QByteArray::number(key);
QTest::newRow(name.constData())
<< token << (token == "callee" ? int(R_CODEMETA_TYPE_FUNCTION_NAME) : -1)
<< int(key);
}
}
}
void keyboardFollowsReference()
{
QFETCH(QString, token);
QFETCH(int, type);
QFETCH(int, key);
Core()->cmd("f keyboard.flag 1 @ 0x20");
MainWindow main;
TestDecompilerWidget widget(&main);
widget.getSeekable()->setSynchronization(false);
widget.setParent(nullptr);
widget.resize(1000, 500);
widget.show();
QVERIFY(QTest::qWaitForWindowExposed(&widget));
QVERIFY(widget.showCode(hoverCode(token, type)));
auto *edit = widget.findChild<QPlainTextEdit *>("textEdit");
QVERIFY(edit);
QTextCursor cursor(edit->document());
cursor.setPosition(edit->toPlainText().indexOf(token) + 2);
edit->setTextCursor(cursor);
widget.activateWindow();
edit->setFocus();
QTRY_VERIFY(edit->hasFocus());
QTest::keyClick(edit, Qt::Key(key));
if (token.startsWith("iaito://")) {
QCOMPARE(Core()->getOffset(), RVA(0x20));
} else {
QCOMPARE(widget.getSeekable()->getOffset(), RVA(0x20));
QCOMPARE(Core()->getOffset(), RVA(0));
}
}
void hoverLinksAndDragSelection()
{
MainWindow main;
TestDecompilerWidget widget(&main);
widget.getSeekable()->setSynchronization(false);
widget.setParent(nullptr);
widget.resize(1000, 500);
widget.show();
QVERIFY(QTest::qWaitForWindowExposed(&widget));
const QString link = "iaito://seek?addr=0x20";
QVERIFY(widget.showCode(hoverCode(link)));
auto *edit = widget.findChild<QPlainTextEdit *>("textEdit");
QVERIFY(edit);
QTextCursor cursor(edit->document());
cursor.setPosition(edit->toPlainText().indexOf(link) + 2);
QTest::mouseMove(edit->viewport(), edit->cursorRect(cursor).center());
QCOMPARE(edit->viewport()->cursor().shape(), Qt::PointingHandCursor);
QCOMPARE(hoveredText(edit), link);
QVERIFY(widget.showCode(Decompiler::makeWarning("No code")));
QVERIFY(hoveredText(edit).isEmpty());
QVERIFY(widget.showCode(hoverCode("0x20")));
const int start = edit->toPlainText().indexOf("0x20");
cursor = QTextCursor(edit->document());
cursor.setPosition(start);
const QPoint from = edit->cursorRect(cursor).center();
cursor.setPosition(start + 4);
const QPoint to = edit->cursorRect(cursor).center();
QTest::mousePress(edit->viewport(), Qt::LeftButton, Qt::NoModifier, from);
QMouseEvent move(
QEvent::MouseMove,
to,
edit->viewport()->mapToGlobal(to),
Qt::NoButton,
Qt::LeftButton,
Qt::NoModifier);
QApplication::sendEvent(edit->viewport(), &move);
QCOMPARE(edit->viewport()->cursor().shape(), Qt::IBeamCursor);
QVERIFY(hoveredText(edit).isEmpty());
QTest::mouseRelease(edit->viewport(), Qt::LeftButton, Qt::NoModifier, to);
QVERIFY(edit->textCursor().hasSelection());
QCOMPARE(widget.getSeekable()->getOffset(), RVA(0));
}
void editFunctionAnnotation_data()
{
QTest::addColumn<bool>("decompiler");
QTest::addColumn<QString>("replacement");
QTest::addColumn<bool>("save");
const QString text = QString::fromUtf8("caf\xc3\xa9\nsecond line");
QTest::newRow("decompiler-delete") << true << QString() << true;
QTest::newRow("decompiler-edit") << true << text << true;
QTest::newRow("decompiler-cancel") << true << QString() << false;
QTest::newRow("disassembly-delete") << false << QString() << true;
QTest::newRow("disassembly-edit") << false << text << true;
QTest::newRow("disassembly-cancel") << false << QString() << false;
}
void editFunctionAnnotation()
{
QFETCH(bool, decompiler);
QFETCH(QString, replacement);
QFETCH(bool, save);
Core()->cmdRawAt("ano=original annotation", 0);
Core()->cmdRawAt("ano=other annotation", 0x20);
MainWindow main;
TestDecompilerWidget widget(&main);
widget.getSeekable()->setSynchronization(false);
QMenu *menu;
const char *slot;
if (decompiler) {
widget.contextMenu()->setDecompiledFunctionAddress(0);
menu = widget.contextMenu();
slot = "actionEditAnnotationTriggered";
} else {
auto *disassemblyMenu = new DisassemblyContextMenu(&widget, &main);
disassemblyMenu->setOffset(4);
menu = disassemblyMenu;
slot = "on_actionEditAnnotation_triggered";
}
// Editing an unsynchronized view must not change the globally sought function.
Core()->seek(0x20);
QSignalSpy refresh(Core(), &IaitoCore::refreshAll);
bool visitedDialog = false;
QString initialText;
QTimer::singleShot(0, [&]() {
auto *dialog = qobject_cast<TextEditDialog *>(QApplication::activeModalWidget());
if (!dialog) {
if (auto *modal = qobject_cast<QDialog *>(QApplication::activeModalWidget())) {
modal->reject();
}
return;
}
auto *editor = dialog->findChild<QTextEdit *>();
auto *buttons = dialog->findChild<QDialogButtonBox *>();
if (!editor || !buttons) {
dialog->reject();
return;
}
visitedDialog = true;
initialText = editor->toPlainText();
editor->setPlainText(replacement);
buttons->button(save ? QDialogButtonBox::Save : QDialogButtonBox::Cancel)->click();
});
QVERIFY(QMetaObject::invokeMethod(menu, slot, Qt::DirectConnection));
QVERIFY(visitedDialog);
QCOMPARE(initialText, QString("original annotation\n"));
const QString expected = !save ? QString("original annotation\n")
: replacement.isEmpty() ? QString()
: replacement + QLatin1Char('\n');
QCOMPARE(Core()->cmdRawAt("anos", 0), expected);
QCOMPARE(Core()->cmdRawAt("ano*", 0).isEmpty(), save && replacement.isEmpty());
QCOMPARE(Core()->cmdRawAt("anos", 0x20), QString("other annotation\n"));
QCOMPARE(Core()->getOffset(), RVA(0x20));
QCOMPARE(refresh.count(), save ? 1 : 0);
}
void lineNavigationAndCommentEntry_data() { columnCombinations(); }
void lineNavigationAndCommentEntry()
{
QFETCH(bool, addresses);
QFETCH(bool, disassembly);
const QString text = "first();\nsecond();\nthird();\n";
RCodeMeta *code = r_codemeta_new(text.toUtf8().constData());
int start = 0;
const QList<RVA> offsets = {0, 4, 9};
for (int line = 0; line < offsets.size(); ++line) {
RCodeMetaItem *offset = r_codemeta_item_new();
offset->type = R_CODEMETA_TYPE_OFFSET;
// Include the preceding newline, as pdc does.
offset->start = qMax(0, start - 1);
offset->end = text.indexOf(QLatin1Char('\n'), start);
offset->offset.offset = offsets.at(line);
r_codemeta_add_item(code, offset);
start = offset->end + 1;
}
MainWindow main;
TestDecompilerWidget widget(&main);
widget.getSeekable()->setSynchronization(false);
QVERIFY(widget.showCode(code));
widget.setAddressColumnVisible(addresses);
widget.setDisassemblyColumnVisible(disassembly);
auto *edit = widget.findChild<QPlainTextEdit *>("textEdit");
QVERIFY(edit);
QTextCursor cursor(edit->document());
cursor.setPosition(edit->toPlainText().indexOf("first") + 2);
edit->setTextCursor(cursor);
Core()->seek(0x20);
QTest::keyClick(edit, Qt::Key_J);
QCOMPARE(edit->textCursor().blockNumber(), 1);
QCOMPARE(widget.getSeekable()->getOffset(), RVA(4));
auto *menu = widget.contextMenu();
QCOMPARE(menu->getFirstOffsetInLine(), RVA(4));
QVERIFY(QMetaObject::invokeMethod(menu, "aboutToShowSlot", Qt::DirectConnection));
auto *comment = menu->findChild<QAction *>("actionAddComment");
QVERIFY(comment && comment->isVisible() && comment->isEnabled());
QCOMPARE(comment->text(), QString("Add Comment"));
QTest::keyClick(edit, Qt::Key_Down);
QCOMPARE(edit->textCursor().blockNumber(), 2);
QCOMPARE(widget.getSeekable()->getOffset(), RVA(9));
QTest::keyClick(edit, Qt::Key_Up);
QCOMPARE(edit->textCursor().blockNumber(), 1);
QTest::keyClick(edit, Qt::Key_K);
QCOMPARE(edit->textCursor().blockNumber(), 0);
QTest::keyClick(edit, Qt::Key_J);
const int position = edit->textCursor().position();
QTest::keyClick(edit, Qt::Key_H);
QCOMPARE(edit->textCursor().position(), position - 1);
QTest::keyClick(edit, Qt::Key_L);
QCOMPARE(edit->textCursor().position(), position);
QTest::keyClick(edit, Qt::Key_Left);
QCOMPARE(edit->textCursor().position(), position - 1);
QTest::keyClick(edit, Qt::Key_Right);
QCOMPARE(edit->textCursor().position(), position);
QTest::keyClick(edit, Qt::Key_Right, Qt::ShiftModifier);
QVERIFY(edit->textCursor().hasSelection());
QCOMPARE(widget.getSeekable()->getOffset(), RVA(4));
QCOMPARE(Core()->getOffset(), RVA(0x20));
QVERIFY(widget.showCode(Decompiler::makeWarning("No function found")));
// Opening the menu on unmapped text still offers the entry, disabled.
QVERIFY(QMetaObject::invokeMethod(menu, "aboutToShowSlot", Qt::DirectConnection));
QVERIFY(comment->isVisible());
QVERIFY(!comment->isEnabled());
}
void commentChangesRefreshOutput_data()
{
QTest::addColumn<bool>("columns");
QTest::addColumn<bool>("keyboard");
QTest::newRow("context-plain") << false << false;
QTest::newRow("keyboard-plain") << false << true;
QTest::newRow("context-columns") << true << false;
QTest::newRow("keyboard-columns") << true << true;
}
void commentChangesRefreshOutput()
{
QFETCH(bool, columns);
QFETCH(bool, keyboard);
MainWindow main;
TestDecompilerWidget widget(&main);
widget.getSeekable()->setSynchronization(false);
widget.getSeekable()->seek(0);
// Exercise refreshes without running MainWindow's file-opening workflow.
widget.setParent(nullptr);
widget.resize(900, 500);
widget.show();
QTRY_VERIFY(widget.isVisibleToUser());
QVERIFY(QMetaObject::invokeMethod(&widget, "refreshDecompiler", Qt::DirectConnection));
auto *edit = widget.findChild<QPlainTextEdit *>("textEdit");
QVERIFY(edit);
QTRY_VERIFY(edit->toPlainText().contains("eax = 0x2a"));
widget.setAddressColumnVisible(columns);
widget.setDisassemblyColumnVisible(columns);
QTextCursor cursor(edit->document());
cursor.setPosition(edit->toPlainText().indexOf("eax = 0x2a") + 2);
edit->setTextCursor(cursor);
edit->setFocus();
const int block = edit->textCursor().blockNumber();
const int column = edit->textCursor().positionInBlock();
auto *menu = widget.contextMenu();
QVERIFY(QMetaObject::invokeMethod(menu, "aboutToShowSlot", Qt::DirectConnection));
auto *comment = menu->findChild<QAction *>("actionAddComment");
auto *remove = menu->findChild<QAction *>("actionDeleteComment");
QVERIFY(comment && comment->isVisible() && comment->isEnabled());
QVERIFY(remove);
QCOMPARE(comment->text(), QString("Add Comment"));
const auto editComment = [&](const QString &text) {
if (keyboard) {
// The offscreen platform drops window activation after modal dialogs.
widget.activateWindow();
if (!QTest::qWaitForWindowActive(&widget)) {
return false;
}
edit->setFocus();
}
bool visited = false;
QTimer timer;
timer.setSingleShot(true);
connect(&timer, &QTimer::timeout, [&]() {
auto *dialog = qobject_cast<CommentsDialog *>(QApplication::activeModalWidget());
if (!dialog) {
if (auto *modal = qobject_cast<QDialog *>(QApplication::activeModalWidget())) {
modal->reject();
}
return;
}
visited = true;
dialog->setComment(text);
auto *editor = dialog->findChild<QPlainTextEdit *>("textEdit");
QTest::keyClick(editor, Qt::Key_Return, Qt::ControlModifier);
});
timer.start(0);
if (keyboard) {
QTest::keyClick(edit, Qt::Key_Semicolon);
} else {
comment->trigger();
}
return visited;
};
QVERIFY(editComment("new-comment"));
QCOMPARE(Core()->getCommentAt(4), QString("new-comment"));
QTRY_VERIFY(edit->toPlainText().contains("new-comment"));
QCOMPARE(edit->textCursor().blockNumber(), block);
QCOMPARE(edit->textCursor().positionInBlock(), column);
QVERIFY(QMetaObject::invokeMethod(menu, "aboutToShowSlot", Qt::DirectConnection));
QCOMPARE(comment->text(), QString("Edit Comment"));
QVERIFY(editComment("edited-comment"));
QTRY_VERIFY(edit->toPlainText().contains("edited-comment"));
QVERIFY(!edit->toPlainText().contains("new-comment"));
QVERIFY(QMetaObject::invokeMethod(menu, "aboutToShowSlot", Qt::DirectConnection));
QVERIFY(remove->isVisible());
remove->trigger();
QVERIFY(Core()->getCommentAt(4).isEmpty());
QTRY_VERIFY(!edit->toPlainText().contains("edited-comment"));
QCOMPARE(edit->textCursor().blockNumber(), block);
QCOMPARE(edit->textCursor().positionInBlock(), column);
}
void refreshCommentOutsideAnnotationRange()
{
Config()->setSelectedDecompiler(headerDecompiler->getId());
MainWindow main;
TestDecompilerWidget widget(&main);
widget.setParent(nullptr);
widget.resize(900, 500);
widget.show();
QTRY_VERIFY(widget.isVisibleToUser());
QVERIFY(QMetaObject::invokeMethod(&widget, "refreshDecompiler", Qt::DirectConnection));
auto *edit = widget.findChild<QPlainTextEdit *>("textEdit");
QVERIFY(edit);
QTRY_VERIFY(edit->toPlainText().contains("first();"));
const int calls = headerDecompiler->calls;
// Only address 4 is annotated, but the function starts at address 0.
Core()->setComment(0, "header-comment");
QTRY_VERIFY(edit->toPlainText().contains("header-comment"));
QCOMPARE(headerDecompiler->calls, calls + 1);
Core()->setComment(0x20, "other-function");
QCOMPARE(headerDecompiler->calls, calls + 1);
}
};
QTEST_MAIN(DecompilerColumnTests)
#include "test_decompilercolumns.moc"
+11
View File
@@ -0,0 +1,11 @@
include ../../config.mk
.PHONY: all check
all:
$(MAKE) -C ../.. -j8 > /dev/null
mkdir -p build
cd build && $(QMAKE) ../decompilerfolding.pro
$(MAKE) -C build -j8 > /dev/null
check: all
QT_QPA_PLATFORM=offscreen ./build/test_decompilerfolding
@@ -0,0 +1,15 @@
QT += core gui widgets network svg testlib
greaterThan(QT_MAJOR_VERSION, 5): QT += svgwidgets
CONFIG += console testcase c++20 link_pkgconfig
CONFIG -= app_bundle
TEMPLATE = app
TARGET = test_decompilerfolding
DEFINES += IAITO_SOURCE_BUILD
PKGCONFIG += r_core
INCLUDEPATH += ../../src ../../src/core ../../src/common ../../src/widgets ../../build
SOURCES += test_decompilerfolding.cpp
IAITO_BUILD = $$clean_path($$PWD/../../build)
IAITO_OBJECTS = $$files($$IAITO_BUILD/*.o)
IAITO_OBJECTS -= $$IAITO_BUILD/Main.o $$IAITO_BUILD/main.o
LIBS += $$IAITO_OBJECTS
PRE_TARGETDEPS += $$IAITO_OBJECTS
@@ -0,0 +1,340 @@
#include "common/Configuration.h"
#include "common/Decompiler.h"
#include "common/IaitoSeekable.h"
#include "core/MainWindow.h"
#include "widgets/DecompilerTextEdit.h"
#include "widgets/DecompilerWidget.h"
#include <QLabel>
#include <QScrollBar>
#include <QSettings>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QTest>
#include <QTextBlock>
static const QString source = QStringLiteral(
"void f() {\n"
" if (x) {\n"
" for (;;) {\n"
" work();\n"
" }\n"
" } else {\n"
" fallback();\n"
" }\n"
" switch (x) {\n"
" case 1: {\n"
" work();\n"
" }\n"
" }\n"
"}\n");
class FoldingDecompiler : public Decompiler
{
public:
FoldingDecompiler()
: Decompiler("test-folding", "Folding test")
{}
RCodeMeta *decompileSync(RVA) override
{
RCodeMeta *code = r_codemeta_new(source.toUtf8().constData());
int start = 0;
int line = 0;
for (const QString &text : source.split(QLatin1Char('\n'))) {
auto *offset = r_codemeta_item_new();
offset->type = R_CODEMETA_TYPE_OFFSET;
offset->start = start;
offset->end = start + text.size();
offset->offset.offset = line == 3 ? 4 : 0;
r_codemeta_add_item(code, offset);
start += text.size() + 1;
++line;
}
return code;
}
void decompileAt(RVA address) override { emit finished(decompileSync(address)); }
};
class DecompilerFoldingTests : public QObject
{
Q_OBJECT
QTemporaryDir settings;
private slots:
void initTestCase()
{
QCoreApplication::setOrganizationName("iaito-tests");
QCoreApplication::setApplicationName("decompilerfolding");
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, settings.path());
Core()->initialize(false);
qRegisterMetaType<RVA>("RVA");
Config()->loadInitial();
Config()->setOutputRedirectionEnabled(false);
Config()->setDecompilerAutoRefreshEnabled(false);
QVERIFY(Core()->registerDecompiler(new FoldingDecompiler));
Config()->setSelectedDecompiler("test-folding");
Core()->cmd("o malloc://64");
Core()->setConfig("asm.arch", "x86");
Core()->setConfig("asm.bits", 64);
Core()->cmd("wx 554889e5b82a0000005dc3 @ 0");
Core()->cmd("ompg +x");
Core()->cmd("af @ 0");
}
void init()
{
Config()->setDecompilerShowAddresses(false);
Config()->setDecompilerShowDisassembly(false);
Core()->seek(0);
}
void braceBlocks_data()
{
QTest::addColumn<QString>("header");
for (const QString &header :
{"if (x)", "else", "for (;;)", "while (x)", "switch (x)", "case 1:", "do", ""}) {
QTest::newRow(header.toUtf8().constData()) << header;
}
}
void braceBlocks()
{
QFETCH(QString, header);
const QString text = header + " {\n body();\n}\n";
DecompilerTextEdit edit;
edit.setDecompiledCode(text, text, false);
const int position = edit.document()->findBlockByNumber(1).position();
QVERIFY(edit.isFoldable(0));
edit.toggleFold(0);
QVERIFY(edit.isFolded(0));
QVERIFY(!edit.document()->findBlockByNumber(1).isVisible());
QVERIFY(edit.document()->findBlockByNumber(2).isVisible());
QCOMPARE(edit.toPlainText(), text);
QCOMPARE(edit.document()->findBlockByNumber(1).position(), position);
edit.toggleFold(0);
QVERIFY(!edit.isFolded(0));
QVERIFY(edit.document()->findBlockByNumber(1).isVisible());
}
void nestedAndElseBlocks()
{
DecompilerTextEdit edit;
edit.setDecompiledCode(source, source, false);
edit.toggleFold(2);
edit.toggleFold(1);
QVERIFY(!edit.document()->findBlockByNumber(2).isVisible());
QVERIFY(edit.document()->findBlockByNumber(5).isVisible());
QVERIFY(edit.isFoldable(5));
edit.toggleFold(1);
QVERIFY(edit.document()->findBlockByNumber(2).isVisible());
QVERIFY(!edit.document()->findBlockByNumber(3).isVisible());
edit.toggleFold(5);
QVERIFY(!edit.document()->findBlockByNumber(6).isVisible());
QVERIFY(edit.document()->findBlockByNumber(7).isVisible());
edit.expandAll();
for (auto block = edit.document()->begin(); block.isValid(); block = block.next()) {
QVERIFY(block.isVisible());
}
edit.collapseAll();
QVERIFY(edit.isFolded(0));
QVERIFY(!edit.document()->findBlockByNumber(1).isVisible());
QCOMPARE(edit.toPlainText(), source);
}
void ignoreLiteralsAndComments()
{
const QString text = QStringLiteral(
"void f() {\n"
" puts(\"{\\\"}\");\n"
" char c = '}';\n"
" // { ignored\n"
" /* {\n"
" } */\n"
" auto s = R\"tag({\n"
" ignored();\n"
" })tag\";\n"
" if (x) {\n"
" work();\n"
" }\n"
"}\n");
DecompilerTextEdit edit;
edit.setDecompiledCode(text, text, false);
for (int line = 0; line < edit.blockCount(); ++line) {
QCOMPARE(edit.isFoldable(line), line == 0 || line == 9);
}
const QString incomplete = "}\nif (x) {\n work();\n";
edit.setDecompiledCode(incomplete, incomplete, false);
QVERIFY(!edit.isFoldable(0));
QVERIFY(!edit.isFoldable(1));
}
void cursorRevealsEnclosingFolds()
{
DecompilerTextEdit edit;
edit.setDecompiledCode(source, source, false);
edit.collapseAll();
QTextCursor cursor(edit.document()->findBlockByNumber(3));
edit.setTextCursor(cursor);
QVERIFY(edit.document()->findBlockByNumber(3).isVisible());
QVERIFY(!edit.isFolded(0));
QVERIFY(!edit.isFolded(1));
QVERIFY(!edit.isFolded(2));
QVERIFY(edit.isFolded(5));
QVERIFY(edit.isFolded(8));
}
void matchingBraces_data()
{
QTest::addColumn<QString>("prefix");
QTest::newRow("plain") << QString();
QTest::newRow("columns") << QString("0x0040 { } | ");
}
void matchingBraces()
{
QFETCH(QString, prefix);
const QString text = "void f() {\n"
" puts(\"}\"); // {\n"
" if (x) { work(); }\n"
" /* } */\n"
"}\n";
QStringList lines = text.split(QLatin1Char('\n'));
for (QString &line : lines) {
line.prepend(prefix);
}
const QString display = lines.join(QLatin1Char('\n'));
DecompilerTextEdit edit;
edit.setDecompiledCode(display, text, false);
auto jump = [&](int from, int to) {
QTextCursor cursor(edit.document());
cursor.setPosition(from);
edit.setTextCursor(cursor);
edit.jumpToMatchingBrace();
QCOMPARE(edit.textCursor().position(), to);
};
const int opening = display.indexOf("f() {") + 4;
const int closing = edit.document()->findBlockByNumber(4).position() + prefix.size();
jump(display.indexOf("void"), closing);
jump(closing, opening);
const int innerOpening = display.indexOf("if (x) {") + 7;
const int innerClosing = display.indexOf("work(); }") + 8;
jump(innerOpening, innerClosing);
jump(innerClosing, innerOpening);
const int literal = display.indexOf("puts");
jump(literal, literal);
QCOMPARE(edit.toPlainText(), display);
const QString incomplete = "if (x) {\n";
edit.setDecompiledCode(incomplete, incomplete, false);
jump(0, 0);
}
void bracesSharingAnOpeningLine()
{
const QString text = "if (a) { if (b) {\n work();\n} }\n";
DecompilerTextEdit edit;
edit.setDecompiledCode(text, text, false);
edit.collapseAll();
QVERIFY(!edit.document()->findBlockByNumber(1).isVisible());
edit.toggleFold(0);
QVERIFY(edit.document()->findBlockByNumber(1).isVisible());
QVERIFY(!edit.isFolded(0));
}
void gutterAndScrolling()
{
DecompilerTextEdit edit;
edit.resize(500, 250);
const QString text = "void f() {\n" + QString(" work();\n").repeated(100) + "}\n";
edit.setDecompiledCode(text, text, false);
edit.show();
QVERIFY(QTest::qWaitForWindowExposed(&edit));
auto *area = edit.findChild<QWidget *>("decompilerFoldArea");
QVERIFY(area && area->isVisible());
QTRY_VERIFY(edit.verticalScrollBar()->maximum() > 0);
const int maximum = edit.verticalScrollBar()->maximum();
QTextCursor cursor(edit.document()->findBlockByNumber(1));
edit.setTextCursor(cursor);
QSignalSpy seeks(&edit, &QPlainTextEdit::cursorPositionChanged);
const int y = edit.cursorRect(QTextCursor(edit.document()->firstBlock())).center().y();
const QPoint point(
area->width() / 2, edit.viewport()->mapTo(&edit, QPoint(0, y)).y() - area->pos().y());
QTest::mouseMove(area, point);
QCOMPARE(area->cursor().shape(), Qt::PointingHandCursor);
QTest::mouseClick(area, Qt::LeftButton, Qt::NoModifier, point);
QVERIFY(edit.isFolded(0));
QCOMPARE(edit.textCursor().blockNumber(), 0);
QCOMPARE(seeks.count(), 0);
QTRY_VERIFY(edit.verticalScrollBar()->maximum() < maximum);
QTest::mouseClick(area, Qt::LeftButton, Qt::NoModifier, point);
QVERIFY(!edit.isFolded(0));
QTRY_COMPARE(edit.verticalScrollBar()->maximum(), maximum);
}
void preserveFoldsAcrossColumnsAndRefresh()
{
MainWindow main;
DecompilerWidget widget(&main);
widget.getSeekable()->setSynchronization(false);
widget.setParent(nullptr);
widget.resize(1000, 600);
widget.show();
QTRY_VERIFY(widget.isVisibleToUser());
QVERIFY(QMetaObject::invokeMethod(&widget, "refreshDecompiler"));
auto *edit = widget.findChild<DecompilerTextEdit *>("textEdit");
QVERIFY(edit);
QTRY_COMPARE(edit->toPlainText(), source);
QTextCursor cursor(edit->document()->findBlockByNumber(3));
edit->setTextCursor(cursor);
QCOMPARE(widget.getSeekable()->getOffset(), RVA(4));
widget.activateWindow();
edit->setFocus();
QTRY_VERIFY(edit->hasFocus());
QTest::keyClick(edit, Qt::Key_Space);
QVERIFY(edit->isFolded(2));
QCOMPARE(edit->textCursor().blockNumber(), 2);
QTest::keyClick(edit, Qt::Key_Space);
QVERIFY(!edit->isFolded(2));
QTest::keyClick(edit, Qt::Key_Space);
QVERIFY(edit->isFolded(2));
QCOMPARE(edit->toPlainText(), source);
edit->toggleFold(1);
QCOMPARE(widget.getSeekable()->getOffset(), RVA(4));
for (bool addresses : {true, false}) {
widget.setAddressColumnVisible(addresses);
widget.setDisassemblyColumnVisible(addresses);
QVERIFY(edit->isFolded(1));
QVERIFY(edit->isFolded(2));
QVERIFY(!edit->document()->findBlockByNumber(3).isVisible());
QVERIFY(edit->textCursor().block().isVisible());
QTest::keyClick(edit, Qt::Key_Percent);
QCOMPARE(edit->textCursor().blockNumber(), 5);
QCOMPARE(
edit->textCursor().block().text().at(edit->textCursor().positionInBlock()),
QLatin1Char('}'));
QTest::keyClick(edit, Qt::Key_Percent, Qt::ShiftModifier);
QCOMPARE(edit->textCursor().blockNumber(), 1);
QCOMPARE(
edit->textCursor().block().text().at(edit->textCursor().positionInBlock()),
QLatin1Char('{'));
}
QVERIFY(QMetaObject::invokeMethod(&widget, "refreshDecompiler"));
auto *progress = widget.findChild<QLabel *>("progressLabel");
QVERIFY(progress);
QTRY_VERIFY(!progress->isVisible());
QVERIFY(edit->isFolded(1));
QVERIFY(edit->isFolded(2));
widget.getSeekable()->seek(0);
widget.getSeekable()->seek(4);
QVERIFY(!edit->isFolded(1));
QVERIFY(!edit->isFolded(2));
QCOMPARE(edit->textCursor().blockNumber(), 3);
QVERIFY(edit->textCursor().block().isVisible());
QCOMPARE(Core()->getOffset(), RVA(0));
}
};
QTEST_MAIN(DecompilerFoldingTests)
#include "test_decompilerfolding.moc"
+11
View File
@@ -0,0 +1,11 @@
include ../../config.mk
.PHONY: all check
all:
$(MAKE) -C ../.. -j8 > /dev/null
mkdir -p build
cd build && $(QMAKE) ../filesystem.pro
$(MAKE) -C build -j8 > /dev/null
check: all
QT_QPA_PLATFORM=offscreen ./build/test_filesystem
+15
View File
@@ -0,0 +1,15 @@
QT += core gui widgets network svg testlib
greaterThan(QT_MAJOR_VERSION, 5): QT += svgwidgets
CONFIG += console testcase c++20 link_pkgconfig
CONFIG -= app_bundle
TEMPLATE = app
TARGET = test_filesystem
DEFINES += IAITO_SOURCE_BUILD
PKGCONFIG += r_core
INCLUDEPATH += ../../src ../../src/core ../../src/common ../../src/widgets ../../build
SOURCES += test_filesystem.cpp
IAITO_BUILD = $$clean_path($$PWD/../../build)
IAITO_OBJECTS = $$files($$IAITO_BUILD/*.o)
IAITO_OBJECTS -= $$IAITO_BUILD/Main.o $$IAITO_BUILD/main.o
LIBS += $$IAITO_OBJECTS
PRE_TARGETDEPS += $$IAITO_OBJECTS
+201
View File
@@ -0,0 +1,201 @@
#include "common/SourceLineReference.h"
#include "core/Iaito.h"
#include "widgets/FilesystemWidget.h"
#include <QFile>
#include <QTemporaryDir>
#include <QtTest>
class FilesystemTest : public QObject
{
Q_OBJECT
QTemporaryDir temporary;
void addReference(quint64 address, const QString &file, int line)
{
const QByteArray encoded
= (file + QLatin1Char(':') + QString::number(line)).toUtf8().toBase64();
Core()->cmd(QStringLiteral("CL %1 base64:%2")
.arg(RAddressString(address), QString::fromLatin1(encoded)));
}
private slots:
void initTestCase()
{
QVERIFY(temporary.isValid());
Core()->initialize(false);
QFile binary(temporary.filePath("binary"));
QVERIFY(binary.open(QIODevice::WriteOnly));
QCOMPARE(binary.write(QByteArray(1024, '\0')), qint64(1024));
binary.close();
Core()->cmd(QStringLiteral("o %1").arg(binary.fileName()));
Core()->cmd(
"af+ 0x100 first;afb+ 0x100 0x100 0x20;"
"af+ 0x200 second;afb+ 0x200 0x200 0x10");
qRegisterMetaType<RVA>("RVA");
}
void sourceRows()
{
const auto rows = SourceLineReference::parse(
"0x00000000\t0\t\n0xffff800000000001\t12\t\treturn a < b;\r\n"
"0x100\t3\tmissing source text is allowed\n"
"bogus\t1\tbad\n0xno\t2\tbad\n0x100\t-2\tbad\n0x100\tbad\tbad\n");
QCOMPARE(rows.size(), 3);
QCOMPARE(rows[0].address, quint64(0));
QCOMPARE(rows[0].line, 0);
QCOMPARE(rows[1].address, Q_UINT64_C(0xffff800000000001));
QCOMPARE(rows[1].code, QString("\treturn a < b;"));
QVERIFY(SourceLineReference::parse("plain file contents").isEmpty());
}
void browseAndNavigate()
{
// The source file intentionally does not exist on this machine.
const QString file = QStringLiteral("/project with spaces/src/main;@.c");
addReference(0x100, file, 3);
addReference(0x110, file, 4);
addReference(0x200, file, 8);
addReference(0x300, file, 10);
addReference(0x400, QStringLiteral("/project with spaces/lib/helper.c"), 2);
Core()->cmd("m /custom r2");
FilesystemWidget widget(nullptr);
widget.resize(900, 800);
widget.show();
widget.showSourceFiles();
auto *tree = widget.findChild<QTreeView *>("filesystemTree");
auto *path = widget.findChild<QLineEdit *>("filesystemPath");
auto *functions = widget.findChild<QTreeWidget *>("sourceFunctions");
auto *lines = widget.findChild<QTreeWidget *>("sourceLines");
QVERIFY(tree && path && functions && lines);
QCOMPARE(path->text(), QStringLiteral("/custom/cl"));
const int mountCount = Core()->cmdj("mj").object()["mountpoints"].toArray().size();
widget.showSourceFiles();
QCOMPARE(Core()->cmdj("mj").object()["mountpoints"].toArray().size(), mountCount);
auto *model = qobject_cast<FilesystemTreeModel *>(tree->model());
QVERIFY(model);
QCOMPARE(model->rowCount(), 1);
const QModelIndex project = model->index(0, 0);
QCOMPARE(project.data().toString(), QStringLiteral("project with spaces"));
tree->expand(project);
QCOMPARE(model->rowCount(project), 2);
QModelIndex source;
for (int row = 0; row < model->rowCount(project); ++row) {
const auto candidate = model->index(row, 0, project);
if (candidate.data().toString() == QStringLiteral("src")) {
source = candidate;
}
}
QVERIFY(source.isValid());
tree->expand(source);
const auto sourceFile = model->index(0, 0, source);
QCOMPARE(sourceFile.data().toString(), QStringLiteral("main;@.c"));
QCOMPARE(
sourceFile.data(Qt::UserRole).toString(),
QStringLiteral("/custom/cl/project with spaces/src/main;@.c"));
// Selection on a secondary column must still preview the correct file.
tree->setCurrentIndex(sourceFile.sibling(sourceFile.row(), 1));
QCOMPARE(functions->topLevelItemCount(), 2);
QCOMPARE(lines->topLevelItemCount(), 4);
QCOMPARE(functions->topLevelItem(0)->text(0), QStringLiteral("first"));
QCOMPARE(functions->topLevelItem(0)->data(0, Qt::UserRole).toULongLong(), quint64(0x100));
QVERIFY(lines->topLevelItem(0)->text(2).isEmpty());
QApplication::processEvents();
const QString previewImage = qEnvironmentVariable("IAITO_FILESYSTEM_PREVIEW");
if (!previewImage.isEmpty()) {
QVERIFY(widget.grab().save(previewImage));
}
QSignalSpy seeks(Core(), &IaitoCore::seekChanged);
QSignalSpy showCode(Core(), &IaitoCore::showMemoryWidgetRequested);
QTest::mouseClick(
functions->viewport(),
Qt::LeftButton,
Qt::NoModifier,
functions->visualItemRect(functions->topLevelItem(1)).center());
QCOMPARE(Core()->getOffset(), RVA(0x200));
QCOMPARE(seeks.size(), 1);
// Unanalyzed addresses remain navigable through the source-line preview.
widget.findChild<QTabWidget *>()->setCurrentWidget(lines);
QApplication::processEvents();
lines->setCurrentItem(lines->topLevelItem(3));
lines->setFocus();
QVERIFY(lines->currentItem());
QCOMPARE(lines->currentItem()->data(0, Qt::UserRole).toULongLong(), quint64(0x300));
QSignalSpy activated(lines, &QTreeWidget::itemActivated);
#ifdef Q_OS_MACOS
QTest::keyClick(lines, Qt::Key_O, Qt::ControlModifier);
#else
QTest::keyClick(lines, Qt::Key_Return);
#endif
QCOMPARE(activated.size(), 1);
QCOMPARE(Core()->getOffset(), RVA(0x300));
QCOMPARE(seeks.size(), 2);
QCOMPARE(showCode.size(), 2);
Core()->cmd("afn renamed 0x100");
Core()->functionsChanged();
QCOMPARE(functions->topLevelItem(0)->text(0), QStringLiteral("renamed"));
QCOMPARE(
tree->currentIndex().data(Qt::UserRole).toString(),
QStringLiteral("/custom/cl/project with spaces/src/main;@.c"));
QVERIFY(tree->isExpanded(tree->currentIndex().parent()));
QCOMPARE(tree->header()->sectionResizeMode(0), QHeaderView::Stretch);
path->setText(QStringLiteral("/"));
path->returnPressed();
bool foundMount = false;
for (int row = 0; row < model->rowCount(); ++row) {
foundMount |= model->index(row, 0).data(Qt::UserRole).toString() == "/custom";
}
QVERIFY(foundMount);
QCOMPARE(lines->topLevelItemCount(), 0);
}
void filePreviews()
{
QFile source(temporary.filePath("available source.c"));
QVERIFY(source.open(QIODevice::WriteOnly));
QCOMPARE(source.write("\treturn a < b;\n"), qint64(15));
source.close();
addReference(0x100, source.fileName(), 1);
FilesystemWidget widget(nullptr);
widget.showSourceFiles();
auto *path = widget.findChild<QLineEdit *>("filesystemPath");
path->setText(QStringLiteral("/custom/cl") + temporary.path());
path->returnPressed();
auto *tree = widget.findChild<QTreeView *>("filesystemTree");
tree->setCurrentIndex(tree->model()->index(0, 0));
auto *lines = widget.findChild<QTreeWidget *>("sourceLines");
QCOMPARE(lines->topLevelItemCount(), 1);
QCOMPARE(lines->topLevelItem(0)->text(2), QStringLiteral("\treturn a < b;"));
path->setText(QStringLiteral("/custom"));
path->returnPressed();
QModelIndex version;
for (int row = 0; row < tree->model()->rowCount(); ++row) {
const auto candidate = tree->model()->index(row, 0);
if (candidate.data().toString() == QStringLiteral("version")) {
version = candidate;
}
}
QVERIFY(version.isValid());
tree->setCurrentIndex(version);
const auto *contents = widget.findChild<QPlainTextEdit *>("filesystemContents");
QVERIFY(contents);
QCOMPARE(contents->toPlainText().trimmed(), Core()->cmd("?V").trimmed());
QCOMPARE(lines->topLevelItemCount(), 0);
}
void automaticMount()
{
Core()->cmd("m-/custom;m /r2 tmp");
FilesystemWidget widget(nullptr);
widget.showSourceFiles();
QCOMPARE(widget.findChild<QLineEdit *>("filesystemPath")->text(), QString("/r2_1/cl"));
}
};
QTEST_MAIN(FilesystemTest)
#include "test_filesystem.moc"
+11
View File
@@ -0,0 +1,11 @@
include ../../config.mk
.PHONY: all check
all:
$(MAKE) -C ../.. -j8 > /dev/null
mkdir -p build
cd build && $(QMAKE) ../flags.pro
$(MAKE) -C build -j8 > /dev/null
check: all
QT_QPA_PLATFORM=offscreen ./build/test_flags
+15
View File
@@ -0,0 +1,15 @@
QT += core gui widgets network svg testlib
greaterThan(QT_MAJOR_VERSION, 5): QT += svgwidgets
CONFIG += console testcase c++20 link_pkgconfig
CONFIG -= app_bundle
TEMPLATE = app
TARGET = test_flags
DEFINES += IAITO_SOURCE_BUILD
PKGCONFIG += r_core
INCLUDEPATH += ../../src ../../src/core ../../src/common ../../build
SOURCES += test_flags.cpp
IAITO_BUILD = $$clean_path($$PWD/../../build)
IAITO_OBJECTS = $$files($$IAITO_BUILD/*.o)
IAITO_OBJECTS -= $$IAITO_BUILD/Main.o $$IAITO_BUILD/main.o
LIBS += $$IAITO_OBJECTS
PRE_TARGETDEPS += $$IAITO_OBJECTS
+328
View File
@@ -0,0 +1,328 @@
#include "common/Configuration.h"
#include "common/IaitoSeekable.h"
#include "common/ShortcutManager.h"
#include "dialogs/FlagDialog.h"
#include "menus/DisassemblyContextMenu.h"
#include "widgets/DisassemblyWidget.h"
#include "widgets/HexWidget.h"
#include <QColorDialog>
#include <QContextMenuEvent>
#include <QDialogButtonBox>
#include <QFontMetricsF>
#include <QLineEdit>
#include <QPushButton>
#include <QSettings>
#include <QTemporaryDir>
#include <QTest>
#include <QTimer>
class TestDisassemblyWidget : public DisassemblyWidget
{
public:
TestDisassemblyWidget()
: DisassemblyWidget(nullptr)
{
getSeekable()->setSynchronization(false);
}
void showInstructions()
{
cachedLines.clear();
for (int i = 0; i < 64; ++i) {
DisassemblyLine line;
line.offset = 0x100 + i * 4;
line.text = QStringLiteral("nop");
cachedLines.append(line);
}
useCachedLines = true;
refreshDisasm(0x100);
}
};
class FlagTests : public QObject
{
Q_OBJECT
QTemporaryDir settings;
void pressFlagKey(
QWidget *widget,
const std::function<void(FlagDialog *)> &edit,
Qt::Key key = Qt::Key_F,
Qt::KeyboardModifiers modifiers = Qt::NoModifier)
{
widget->window()->activateWindow();
widget->setFocus();
QTRY_VERIFY(widget->hasFocus());
bool opened = false;
QTimer::singleShot(0, this, [&]() {
auto *dialog = qobject_cast<FlagDialog *>(QApplication::activeModalWidget());
if (!dialog) {
return;
}
opened = true;
edit(dialog);
if (dialog->isVisible()) {
dialog->reject();
}
});
QTest::keyClick(widget, key, modifiers);
QCoreApplication::processEvents();
QVERIFY(opened);
}
static QLineEdit *field(FlagDialog *dialog, const char *name)
{
return dialog->findChild<QLineEdit *>(QString::fromLatin1(name));
}
static void save(FlagDialog *dialog)
{
dialog->findChild<QDialogButtonBox *>()->button(QDialogButtonBox::Ok)->click();
}
private slots:
void initTestCase()
{
QCoreApplication::setOrganizationName("iaito-tests");
QCoreApplication::setApplicationName("flags");
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, settings.path());
Core()->initialize(false);
qRegisterMetaType<RVA>("RVA");
Config()->loadInitial();
Core()->cmd("o malloc://16384");
Core()->setConfig("asm.arch", "arm");
Core()->setConfig("asm.bits", 64);
Core()->cmd("wx 1f2003d51f2003d51f2003d5 @ 0x100");
}
void cleanup()
{
Core()->cmd("f-*");
ShortcutMgr()->resetToDefault("hex.flag");
ShortcutMgr()->resetToDefault("disasm.flag");
}
void hexFlags_data()
{
QTest::addColumn<int>("selectedBytes");
QTest::newRow("cursor") << 0;
QTest::newRow("one-byte") << 1;
QTest::newRow("range") << 17;
}
void hexFlags()
{
QFETCH(int, selectedBytes);
HexWidget widget;
widget.resize(1000, 500);
widget.show();
QVERIFY(QTest::qWaitForWindowExposed(&widget));
widget.seek(0x100);
if (selectedBytes) {
widget.selectRange(0x100, 0x100 + selectedBytes - 1);
}
widget.setFocus();
pressFlagKey(&widget, [&](FlagDialog *dialog) {
QCOMPARE(field(dialog, "offsetEdit")->text(), RAddressString(0x100));
QCOMPARE(field(dialog, "sizeEdit")->text(), QString::number(qMax(1, selectedBytes)));
field(dialog, "nameEdit")->setText("test.flag");
field(dialog, "commentEdit")->setText("selected bytes");
bool pickedColor = false;
QTimer::singleShot(0, this, [&]() {
auto *picker = qobject_cast<QColorDialog *>(QApplication::activeModalWidget());
if (picker) {
pickedColor = true;
picker->setCurrentColor(QColor("#33aaff"));
picker->accept();
}
});
dialog->findChild<QPushButton *>("colorButton")->click();
QVERIFY(pickedColor);
QCOMPARE(field(dialog, "colorEdit")->text(), QString("#33aaff"));
save(dialog);
});
RFlagItem *flag = r_flag_get(Core()->core()->flags, "test.flag");
QVERIFY(flag);
QCOMPARE(flag->addr, ut64(0x100));
QCOMPARE(flag->size, ut64(qMax(1, selectedBytes)));
auto *meta = r_flag_get_meta(Core()->core()->flags, flag->id);
QVERIFY(meta);
QCOMPARE(QString::fromUtf8(meta->color), QString("#33aaff"));
pressFlagKey(&widget, [&](FlagDialog *dialog) {
QCOMPARE(field(dialog, "nameEdit")->text(), QString("test.flag"));
QCOMPARE(field(dialog, "colorEdit")->text(), QString("#33aaff"));
QCOMPARE(field(dialog, "commentEdit")->text(), QString("selected bytes"));
field(dialog, "nameEdit")->setText("test.edited");
field(dialog, "sizeEdit")->setText("5");
save(dialog);
});
QVERIFY(!r_flag_get(Core()->core()->flags, "test.flag"));
flag = r_flag_get(Core()->core()->flags, "test.edited");
QVERIFY(flag);
QCOMPARE(flag->size, ut64(5));
}
void disassemblyFlags_data()
{
QTest::addColumn<int>("direction");
QTest::newRow("cursor") << 0;
QTest::newRow("forward-range") << 1;
QTest::newRow("backward-range") << -1;
}
void disassemblyFlags()
{
QFETCH(int, direction);
TestDisassemblyWidget widget;
widget.resize(1000, 500);
widget.show();
QVERIFY(QTest::qWaitForWindowExposed(&widget));
widget.showInstructions();
auto *edit = qobject_cast<QPlainTextEdit *>(widget.getTextWidget());
QVERIFY(edit);
QTextCursor cursor(edit->document());
if (direction) {
const int end = 11; // Three "nop" lines, including the final instruction.
cursor.setPosition(direction > 0 ? 0 : end);
cursor.setPosition(direction > 0 ? end : 0, QTextCursor::KeepAnchor);
}
edit->setTextCursor(cursor);
edit->setFocus();
pressFlagKey(edit, [&](FlagDialog *dialog) {
QCOMPARE(field(dialog, "offsetEdit")->text(), RAddressString(0x100));
QCOMPARE(field(dialog, "sizeEdit")->text(), QString(direction ? "12" : "1"));
field(dialog, "nameEdit")->setText("test.disasm");
field(dialog, "colorEdit")->setText("#ff8844");
save(dialog);
});
RFlagItem *flag = r_flag_get(Core()->core()->flags, "test.disasm");
QVERIFY(flag);
QCOMPARE(flag->addr, ut64(0x100));
QCOMPARE(flag->size, ut64(direction ? 12 : 1));
}
void flagMenuDeletion()
{
QWidget parent;
Core()->addFlag(0x100, "test.delete", 8);
Core()->addFlag(0x108, "test.keep", 1);
HexWidget hex;
QFont font = Config()->getFont();
font.setPointSize(10);
hex.setMonospaceFont(font);
hex.setFixedLineSize(16);
hex.resize(1000, 500);
hex.show();
QVERIFY(QTest::qWaitForWindowExposed(&hex));
hex.seek(0);
hex.selectRange(0x100, 0x107);
bool menuOpened = false;
connect(&hex, &HexWidget::contextMenuAboutToShow, this, [&](QMenu *menu, RVA start, RVA end) {
menuOpened = true;
// Always close the popup, including when a check fails.
QTimer::singleShot(0, menu, &QMenu::close);
QCOMPARE(start, RVA(0x100));
QCOMPARE(end, RVA(0x107));
QAction *remove = nullptr;
QAction *edit = nullptr;
for (auto *action : menu->actions()) {
if (action->text() == "Delete flag") {
remove = action;
} else if (action->text() == "Edit flag...") {
edit = action;
}
}
QVERIFY(edit);
QVERIFY(remove);
QVERIFY(edit->isEnabled());
QVERIFY(remove->isEnabled());
QCOMPARE(edit->shortcut(), QKeySequence(Qt::Key_F));
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QVERIFY(edit->isShortcutVisibleInContextMenu());
#endif
remove->trigger();
});
QFontMetricsF metrics(font);
const QPoint point(
qRound(22 * metrics.size(Qt::TextSingleLine, "F").width()),
qRound(17.5 * qCeil(metrics.height())));
QContextMenuEvent event(QContextMenuEvent::Mouse, point, hex.viewport()->mapToGlobal(point));
QApplication::sendEvent(hex.viewport(), &event);
QVERIFY(menuOpened);
QVERIFY(!r_flag_get(Core()->core()->flags, "test.delete"));
QVERIFY(r_flag_get(Core()->core()->flags, "test.keep"));
Core()->addFlag(0x100, "test.delete", 8);
DisassemblyContextMenu disasmMenu(&parent, nullptr);
disasmMenu.setOffset(0x108);
disasmMenu.setSelectionRange(0x100, 8);
disasmMenu.popup(QPoint());
QAction *remove = nullptr;
for (auto *action : disasmMenu.findChildren<QAction *>()) {
if (action->text().startsWith("Delete flag at")) {
remove = action;
}
}
QVERIFY(remove);
QVERIFY(remove->isEnabled());
remove->trigger();
disasmMenu.close();
QVERIFY(!r_flag_get(Core()->core()->flags, "test.delete"));
QVERIFY(r_flag_get(Core()->core()->flags, "test.keep"));
}
void largeFlagSize()
{
const ut64 size = ut64(1) << 32;
FlagDialog dialog(0x100, size);
QCOMPARE(field(&dialog, "sizeEdit")->text(), QString::number(size));
field(&dialog, "nameEdit")->setText("test.large");
QVERIFY(field(&dialog, "sizeEdit")->hasAcceptableInput());
save(&dialog);
auto *flag = r_flag_get(Core()->core()->flags, "test.large");
QVERIFY(flag);
QCOMPARE(flag->size, size);
}
void configurableShortcuts()
{
const QKeySequence custom(Qt::CTRL | Qt::SHIFT | Qt::Key_F7);
HexWidget hex;
hex.resize(1000, 500);
hex.show();
QVERIFY(QTest::qWaitForWindowExposed(&hex));
ShortcutMgr()->setSequence("hex.flag", custom);
hex.setFocus();
pressFlagKey(&hex, [](FlagDialog *) {}, Qt::Key_F7, Qt::ControlModifier | Qt::ShiftModifier);
TestDisassemblyWidget disasm;
disasm.resize(1000, 500);
disasm.show();
QVERIFY(QTest::qWaitForWindowExposed(&disasm));
disasm.showInstructions();
ShortcutMgr()->setSequence("disasm.flag", custom);
auto *edit = disasm.getTextWidget();
edit->setFocus();
pressFlagKey(edit, [](FlagDialog *) {}, Qt::Key_F7, Qt::ControlModifier | Qt::ShiftModifier);
auto *menu = disasm.findChild<DisassemblyContextMenu *>();
QVERIFY(menu);
bool shownInMenu = false;
for (auto *action : menu->findChildren<QAction *>()) {
shownInMenu |= action->shortcut() == custom && action->text().contains("flag");
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
if (action->shortcut() == custom) {
QVERIFY(action->isShortcutVisibleInContextMenu());
}
#endif
}
QVERIFY(shownInMenu);
QCOMPARE(ShortcutMgr()->sequence("graph.takeFalse"), QKeySequence(Qt::Key_F));
}
};
QTEST_MAIN(FlagTests)
#include "test_flags.moc"
+11
View File
@@ -0,0 +1,11 @@
include ../../config.mk
.PHONY: all check
all:
$(MAKE) -C ../..
mkdir -p build
cd build && $(QMAKE) ../globalvariables.pro
$(MAKE) -C build
check: all
QT_QPA_PLATFORM=offscreen ./build/test_globalvariables
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more