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
94 changed files with 7879 additions and 436 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:
+1 -1
View File
@@ -1,4 +1,4 @@
include config.mk
-include config.mk
ifeq ($(shell uname),Darwin)
BIN=build/iaito.app/Contents/MacOS/iaito
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>
+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
View File
@@ -44,6 +44,7 @@ static const char *refreshAllCommandPrefixes[] = {
"C",
"aa",
"af",
"avg",
"w",
};
+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 -2
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()};
@@ -1547,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,
@@ -1557,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();
}
+19 -5
View File
@@ -139,16 +139,21 @@ 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) {
m_blocks.append(Core()->ioRead(addr, blockSize));
@@ -316,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;
@@ -639,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;
+520
View File
@@ -14,20 +14,49 @@
#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>
#include <QTextOption>
#include <QtEndian>
namespace {
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;
}
QColor fieldColor(const QString &path)
{
// 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)
, ui(new Ui::HexdumpWidget)
@@ -217,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
@@ -350,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()
@@ -436,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()
@@ -456,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
View File
@@ -14,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"
@@ -24,6 +25,7 @@ class HexdumpWidget;
// Forward declarations
class QLineEdit;
class QTreeWidgetItem;
class RefreshDeferrer;
class QSyntaxHighlighter;
@@ -75,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
+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_globalvariables
DEFINES += IAITO_SOURCE_BUILD
PKGCONFIG += r_core
INCLUDEPATH += ../../src ../../src/core ../../src/common ../../src/widgets ../../build
SOURCES += test_globalvariables.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,288 @@
#include "common/Configuration.h"
#include "common/NameListModel.h"
#include "core/MainWindow.h"
#include "dialogs/XrefsDialog.h"
#include "widgets/GlobalVariablesWidget.h"
#include <QAbstractItemModelTester>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QJsonArray>
#include <QJsonObject>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QSettings>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QTest>
#include <QTimer>
#include <QTreeView>
class GlobalVariablesTests : public QObject
{
Q_OBJECT
QTemporaryDir settings;
private slots:
void initTestCase()
{
QVERIFY(R2_ABIVERSION >= 154);
QCoreApplication::setOrganizationName("iaito-tests");
QCoreApplication::setApplicationName("globalvariables");
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);
Core()->cmd("o malloc://16384");
}
void init()
{
// Remove globals through avg- so their type links are also cleaned up.
for (const auto &variable : Core()->getAllGlobalVariables()) {
Core()->cmdRawAt("avg-", variable.address);
}
Core()->cmd("f--;C-*;ax-*");
Core()->seek(0x100);
}
void listExternalGlobals()
{
Core()->cmd(
"avg int count @ 0;avg char * text @ 0x20000000000001;"
"avg int kernel @ 0x8000000000000001;fs scratch");
const auto variables = Core()->getAllGlobalVariables();
QCOMPARE(variables.size(), 3);
QCOMPARE(variables[0].address, RVA(0));
QCOMPARE(variables[0].name, QString("count"));
QCOMPARE(variables[0].type, QString("int"));
QCOMPARE(variables[0].size, RVA(4));
QCOMPARE(variables[1].address, RVA(0x20000000000001));
QCOMPARE(variables[1].type, QString("char *"));
QCOMPARE(variables[2].address, RVA(0x8000000000000001));
QCOMPARE(Core()->getOffset(), RVA(0x100));
const auto spaces = Core()->cmdj("fsj").array();
bool selected = false;
for (const auto &space : spaces) {
const auto object = space.toObject();
selected |= object["name"].toString() == "scratch" && object["selected"].toBool();
}
QVERIFY(selected);
}
void registerEditAndDelete()
{
QSignalSpy flags(Core(), &IaitoCore::flagsChanged);
QSignalSpy code(Core(), &IaitoCore::refreshCodeViews);
QString error;
QVERIFY2(
Core()->setGlobalVariable(0x20, "counter", "unsigned int", {}, &error),
qPrintable(error));
auto variables = Core()->getAllGlobalVariables();
QCOMPARE(variables.size(), 1);
QCOMPARE(variables[0].type, QString("unsigned int"));
QCOMPARE(variables[0].size, RVA(4));
Core()->cmd("axd 0x20 0x80");
QVERIFY2(
Core()->setGlobalVariable(0x20, "app.counter", "int[4]", "counter", &error),
qPrintable(error));
variables = Core()->getAllGlobalVariables();
QCOMPARE(variables.size(), 1);
QCOMPARE(variables[0].name, QString("app.counter"));
QCOMPARE(variables[0].type, QString("int[4]"));
QCOMPARE(variables[0].size, RVA(16));
QCOMPARE(Core()->cmdj("axtj @ 0x20").array().size(), 1);
// An unrelated symbol at the same address must survive avg-.
Core()->cmd("fs symbols;f sym.counter 4 @ 0x20");
QVERIFY(Core()->deleteGlobalVariable(0x20, "app.counter"));
QVERIFY(Core()->getAllGlobalVariables().isEmpty());
QVERIFY(!Core()->cmdj("fj").array().isEmpty());
QCOMPARE(Core()->cmdj("axtj @ 0x20").array().size(), 1);
QCOMPARE(Core()->getOffset(), RVA(0x100));
QCOMPARE(flags.count(), 3);
QCOMPARE(code.count(), 3);
}
void rejectedEditsPreserveVariables()
{
QVERIFY(Core()->setGlobalVariable(0x20, "counter", "int"));
Core()->cmd("fs symbols;f occupied 4 @ 0x40");
QSignalSpy flags(Core(), &IaitoCore::flagsChanged);
QString error;
QVERIFY(!Core()->setGlobalVariable(0x20, "other", "char", {}, &error));
QVERIFY(!error.isEmpty());
QVERIFY(!Core()->setGlobalVariable(0x40, "occupied", "int", {}, &error));
QVERIFY(!Core()->setGlobalVariable(0x20, "occupied", "char", "counter", &error));
QVERIFY(!Core()->setGlobalVariable(0x20, "renamed", "unknown_type", "counter", &error));
QVERIFY(
!Core()->setGlobalVariable(0x20, "counter", "int;f injected @ 0x70", "counter", &error));
QVERIFY(!r_flag_get(Core()->core()->flags, "injected"));
QVERIFY(!Core()->setGlobalVariable(0x20, "bad;name", "char", "counter", &error));
QVERIFY(!Core()->setGlobalVariable(0x20, "renamed", "char", "missing", &error));
QVERIFY(!Core()->setGlobalVariable(RVA_INVALID, "invalid", "int", {}, &error));
QVERIFY(!Core()->deleteGlobalVariable(0x40, "occupied"));
QVERIFY(!Core()->deleteGlobalVariable(0x40, "counter"));
const auto variables = Core()->getAllGlobalVariables();
QCOMPARE(variables.size(), 1);
QCOMPARE(variables[0].name, QString("counter"));
QCOMPARE(variables[0].type, QString("int"));
QCOMPARE(variables[0].size, RVA(4));
QCOMPARE(flags.count(), 0);
}
void modelSortingFilteringAndHierarchy()
{
GlobalVariablesModel model;
QAbstractItemModelTester
tester(&model, QAbstractItemModelTester::FailureReportingMode::QtTest);
model.setVariables(
{{0x8000000000000002, 16, "app.z", "int[4]"},
{0x8000000000000001, 4, "app.a", "int"},
{0, 1, "zero", "char"}});
AddressableFilterProxyModel proxy(&model, nullptr);
proxy.setSortRole(Qt::EditRole);
proxy.setFilterKeyColumn(-1);
proxy.sort(GlobalVariablesModel::AddressColumn);
QCOMPARE(proxy.address(proxy.index(0, 0)), RVA(0));
QCOMPARE(proxy.address(proxy.index(1, 0)), RVA(0x8000000000000001));
QCOMPARE(proxy.address(proxy.index(2, 0)), RVA(0x8000000000000002));
proxy.setTypeFilterColumn(GlobalVariablesModel::TypeColumn);
proxy.setTypeFilter("int");
QCOMPARE(proxy.rowCount(), 1);
QCOMPARE(proxy.name(proxy.index(0, 0)), QString("app.a"));
proxy.setTypeFilter({});
proxy.setFilterWildcard("*app*");
QCOMPARE(proxy.rowCount(), 2);
NameListModel hierarchy(&proxy, GlobalVariablesModel::NameColumn);
hierarchy.setHierarchy(true);
const auto group = hierarchy.index(0, 0);
QCOMPARE(hierarchy.address(group), RVA_INVALID);
const auto leaf = hierarchy.index(0, GlobalVariablesModel::NameColumn, group);
QCOMPARE(hierarchy.name(leaf), QString("app.a"));
QCOMPARE(
leaf.data(GlobalVariablesModel::DescriptionRole)
.value<GlobalVariableDescription>()
.address,
RVA(0x8000000000000001));
QVERIFY(!model.data({}).isValid());
}
void ambiguousDeletionPreservesMetadata()
{
Core()->cmd("avg char first @ 0x40;avg int second @ 0x40");
const auto metadata = Core()->cmdj("Cj");
QVERIFY(!Core()->deleteGlobalVariable(0x40, "first"));
QVERIFY(!Core()->deleteGlobalVariable(0x40, "second"));
QCOMPARE(Core()->getAllGlobalVariables().size(), 2);
QCOMPARE(Core()->cmdj("Cj"), metadata);
}
void widgetWorkflow()
{
MainWindow main;
main.resize(1100, 700);
// Keep unrelated panels from exercising platform font fallbacks in this UI test.
for (auto panel : main.findChildren<QDockWidget *>()) {
panel->hide();
}
main.show();
auto dock = main.findChild<GlobalVariablesWidget *>("GlobalVariablesWidget");
QVERIFY(dock);
dock->show();
dock->raise();
QTRY_VERIFY(dock->isVisibleToUser());
auto view = dock->findChild<QTreeView *>("treeView");
QVERIFY(view);
auto add = dock->findChild<QAction *>("actionAddGlobalVariable");
auto edit = dock->findChild<QAction *>("actionEditGlobalVariable");
auto remove = dock->findChild<QAction *>("actionDeleteGlobalVariable");
QVERIFY(add && edit && remove);
QVERIFY(add->isEnabled());
QVERIFY(!edit->isEnabled());
QVERIFY(!remove->isEnabled());
auto runEditor = [&](QAction *action, const QString &newName, const QString &newType) {
bool processed = false;
QString errorText;
QTimer::singleShot(0, this, [&]() {
auto dialog = qobject_cast<QDialog *>(QApplication::activeModalWidget());
if (!dialog) {
return;
}
auto name = dialog->findChild<QLineEdit *>("globalVariableName");
auto type = dialog->findChild<QComboBox *>("globalVariableType");
if (!name || !type) {
errorText = "Unexpected modal dialog";
dialog->reject();
return;
}
name->setText(newName);
type->setCurrentText(newType);
auto buttons = dialog->findChild<QDialogButtonBox *>();
emit buttons->accepted();
processed = true;
if (dialog->result() != QDialog::Accepted) {
errorText = dialog->findChild<QLabel *>("globalVariableError")->text();
dialog->reject();
}
});
action->trigger();
QVERIFY(processed);
QVERIFY2(errorText.isEmpty(), qPrintable(errorText));
};
runEditor(add, "app.counter", "unsigned int");
QTRY_COMPARE(view->model()->rowCount(), 1);
const QString screenshot = qEnvironmentVariable("IAITO_GLOBAL_VARIABLES_SCREENSHOT");
if (!screenshot.isEmpty()) {
dock->setFloating(true);
dock->resize(620, 260);
QTest::qWait(20);
QVERIFY(dock->grab().save(screenshot));
}
const auto item = view->model()->index(0, 0);
view->setCurrentIndex(item);
QVERIFY(edit->isEnabled());
QVERIFY(remove->isEnabled());
Core()->seek(0x200);
emit view->activated(item);
QCOMPARE(Core()->getOffset(), RVA(0x100));
Core()->cmd("axd 0x100 0x80");
auto addressMenu = dock->findChild<AddressableItemContextMenu *>();
QVERIFY(addressMenu);
QAction *xrefs = nullptr;
for (auto action : addressMenu->actions()) {
if (action->text().contains("X-Refs")) {
xrefs = action;
}
}
QVERIFY(xrefs);
QVERIFY(xrefs->isEnabled());
bool refsShown = false;
QTimer::singleShot(0, this, [&]() {
auto dialog = qobject_cast<XrefsDialog *>(QApplication::activeModalWidget());
if (dialog) {
for (auto tree : dialog->findChildren<QTreeView *>()) {
refsShown |= tree->model() && tree->model()->rowCount() > 0;
}
dialog->reject();
}
});
xrefs->trigger();
QVERIFY(refsShown);
runEditor(edit, "app.renamed", "char *");
QTRY_COMPARE(view->model()->rowCount(), 1);
QCOMPARE(Core()->getAllGlobalVariables()[0].name, QString("app.renamed"));
view->setCurrentIndex(view->model()->index(0, 0));
remove->trigger();
QTRY_COMPARE(view->model()->rowCount(), 0);
QVERIFY(!edit->isEnabled());
QVERIFY(!remove->isEnabled());
QVERIFY(add->isEnabled());
}
};
QTEST_MAIN(GlobalVariablesTests)
#include "test_globalvariables.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) ../panelmaximize.pro
$(MAKE) -C build -j8 > /dev/null
check: all
QT_QPA_PLATFORM=offscreen ./build/test_panelmaximize
+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_panelmaximize
DEFINES += IAITO_SOURCE_BUILD
PKGCONFIG += r_core
INCLUDEPATH += ../../src ../../src/core ../../src/common ../../src/widgets ../../build
SOURCES += test_panelmaximize.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
+501
View File
@@ -0,0 +1,501 @@
#include "common/Configuration.h"
#include "core/DockManager.h"
#include "core/MainWindow.h"
#include "widgets/IaitoDockWidget.h"
#include <QApplication>
#include <QLineEdit>
#include <QSettings>
#include <QTabBar>
#include <QTemporaryDir>
#include <QTest>
class PanelMaximizeTests : public QObject
{
Q_OBJECT
QTemporaryDir settings;
#ifdef Q_OS_MACOS
const Qt::KeyboardModifier panelModifier = Qt::ControlModifier;
#else
const Qt::KeyboardModifier panelModifier = Qt::MetaModifier;
#endif
void hideBuiltinPanels(MainWindow &main)
{
main.setAnimated(false);
main.resize(1100, 700);
for (auto *dock : main.findChildren<IaitoDockWidget *>()) {
dock->hide();
}
}
IaitoDockWidget *addPanel(MainWindow &main, const QString &name)
{
auto *dock = new IaitoDockWidget(&main);
dock->setObjectName(name);
dock->setWindowTitle(name);
dock->setWidget(new QLineEdit(dock));
dock->widget()->setObjectName(name + "-input");
main.addWidget(dock);
main.addDockWidget(Qt::TopDockWidgetArea, dock);
return dock;
}
private slots:
void initTestCase()
{
QCoreApplication::setOrganizationName("iaito-tests");
QCoreApplication::setApplicationName("panelmaximize");
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);
}
void restoreTiledLayout_data()
{
QTest::addColumn<int>("key");
QTest::newRow("return") << int(Qt::Key_Return);
QTest::newRow("keypad-enter") << int(Qt::Key_Enter);
}
void restoreTiledLayout()
{
QFETCH(int, key);
MainWindow main;
hideBuiltinPanels(main);
auto *target = addPanel(main, "target");
auto *tab = addPanel(main, "tab");
auto *side = addPanel(main, "side");
auto *hidden = addPanel(main, "hidden");
main.splitDockWidget(target, side, Qt::Horizontal);
main.tabifyDockWidget(target, tab);
hidden->hide();
target->setMaximumSize(450, 500);
main.show();
QVERIFY(QTest::qWaitForWindowExposed(&main));
target->raise();
target->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), target->widget());
QCoreApplication::processEvents();
const QByteArray state = main.saveState();
const QRect targetGeometry = target->geometry();
const QRect sideGeometry = side->geometry();
const QRect panelArea = targetGeometry.united(sideGeometry);
const QSize maximumSize = target->maximumSize();
const auto modifiers = Qt::KeyboardModifiers(panelModifier)
| (key == Qt::Key_Enter ? Qt::KeypadModifier : Qt::NoModifier);
QTest::keyClick(target->widget(), Qt::Key(key), modifiers);
QTRY_VERIFY(side->isHidden());
QVERIFY(tab->isHidden());
QVERIFY(hidden->isHidden());
QTRY_COMPARE(target->x(), panelArea.x());
QTRY_COMPARE(target->width(), panelArea.width());
QVERIFY(target->height() > targetGeometry.height());
QCOMPARE(QApplication::focusWidget(), target->widget());
auto *manager = main.findChild<DockManager *>();
QVERIFY(manager);
QCOMPARE(manager->saveState(), state);
// Saving while expanded must persist the original tiled layout.
main.saveSettings();
QSettings saved;
const int count = saved.beginReadArray("layouts");
QVERIFY(count > 0);
saved.setArrayIndex(0);
QCOMPARE(saved.value("state").toByteArray(), state);
saved.endArray();
QTest::keyClick(target->widget(), Qt::Key_Return, panelModifier);
QTRY_VERIFY(!side->isHidden());
QVERIFY(!tab->isHidden());
QVERIFY(hidden->isHidden());
QCOMPARE(target->maximumSize(), maximumSize);
QTRY_COMPARE(target->geometry(), targetGeometry);
QTRY_COMPARE(side->geometry(), sideGeometry);
QVERIFY(main.tabifiedDockWidgets(target).contains(tab));
QCOMPARE(QApplication::focusWidget(), target->widget());
}
void closingExpandedPanelRestoresOthers()
{
MainWindow main;
hideBuiltinPanels(main);
auto *target = addPanel(main, "target");
auto *side = addPanel(main, "side");
main.splitDockWidget(target, side, Qt::Horizontal);
target->setTransient(true);
main.show();
QVERIFY(QTest::qWaitForWindowExposed(&main));
target->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), target->widget());
QTest::keyClick(target->widget(), Qt::Key_Return, panelModifier);
QTRY_VERIFY(side->isHidden());
QPointer<IaitoDockWidget> guardedTarget(target);
target->close();
QTRY_VERIFY(!side->isHidden());
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
QVERIFY(!guardedTarget);
}
void openingAnotherPanelRestoresLayout()
{
MainWindow main;
hideBuiltinPanels(main);
auto *target = addPanel(main, "target");
auto *side = addPanel(main, "side");
auto *hidden = addPanel(main, "hidden");
main.splitDockWidget(target, side, Qt::Horizontal);
hidden->hide();
main.show();
QVERIFY(QTest::qWaitForWindowExposed(&main));
target->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), target->widget());
QTest::keyClick(target->widget(), Qt::Key_Return, panelModifier);
QTRY_VERIFY(side->isHidden());
hidden->show();
QTRY_VERIFY(!side->isHidden());
QVERIFY(!hidden->isHidden());
QVERIFY(target->width() < main.width());
}
void selectedTabsSurviveLayoutChanges_data()
{
QTest::addColumn<QString>("mutation");
QTest::newRow("resize") << QString("resize");
QTest::newRow("add-remove-panel") << QString("add-remove-panel");
QTest::newRow("save-restore") << QString("save-restore");
QTest::newRow("maximize-restore") << QString("maximize-restore");
}
void selectedTabsSurviveLayoutChanges()
{
QFETCH(QString, mutation);
MainWindow main;
hideBuiltinPanels(main);
auto *first = addPanel(main, "first");
auto *selected = addPanel(main, "selected");
auto *last = addPanel(main, "last");
auto *target = addPanel(main, "target");
auto *otherFirst = addPanel(main, "other-first");
auto *otherSelected = addPanel(main, "other-selected");
auto *otherLast = addPanel(main, "other-last");
main.splitDockWidget(first, target, Qt::Horizontal);
main.splitDockWidget(target, otherFirst, Qt::Horizontal);
main.tabifyDockWidget(first, selected);
main.tabifyDockWidget(first, last);
main.tabifyDockWidget(otherFirst, otherSelected);
main.tabifyDockWidget(otherFirst, otherLast);
main.show();
QVERIFY(QTest::qWaitForWindowExposed(&main));
selected->raise();
otherSelected->raise();
target->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), target->widget());
QTRY_VERIFY(!selected->visibleRegion().isEmpty());
QTRY_VERIFY(!otherSelected->visibleRegion().isEmpty());
auto *manager = main.findChild<DockManager *>();
QVERIFY(manager);
// Chrome reconciliation must leave Qt's selected tab identities intact.
manager->updateDockTabBars();
if (mutation == "resize") {
main.resize(1200, 750);
} else if (mutation == "add-remove-panel") {
auto *added = addPanel(main, "added");
QCoreApplication::processEvents();
added->setTransient(true);
added->close();
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
} else if (mutation == "save-restore") {
const QByteArray state = main.saveState();
QVERIFY(main.restoreState(state));
} else {
for (int i = 0; i < 3; ++i) {
QTest::keyClick(target->widget(), Qt::Key_Return, panelModifier);
QTRY_VERIFY(selected->isHidden());
QTest::keyClick(target->widget(), Qt::Key_Return, panelModifier);
QTRY_VERIFY(!selected->isHidden());
QCoreApplication::processEvents();
}
}
QTest::qWait(50);
QTRY_VERIFY(!selected->visibleRegion().isEmpty());
QTRY_VERIFY(!otherSelected->visibleRegion().isEmpty());
QVERIFY(first->visibleRegion().isEmpty());
QVERIFY(last->visibleRegion().isEmpty());
QVERIFY(otherFirst->visibleRegion().isEmpty());
QVERIFY(otherLast->visibleRegion().isEmpty());
}
void closeCorrectDockWithDuplicateTitles()
{
MainWindow main;
hideBuiltinPanels(main);
auto *first = addPanel(main, "first");
auto *second = addPanel(main, "second");
first->setWindowTitle("Shared panel");
second->setWindowTitle("Shared panel");
// Deliberately put the panels in the opposite order to their registration.
main.tabifyDockWidget(second, first);
main.show();
QVERIFY(QTest::qWaitForWindowExposed(&main));
first->raise();
QTRY_VERIFY(!first->visibleRegion().isEmpty());
auto *manager = main.findChild<DockManager *>();
QVERIFY(manager);
manager->updateDockTabBars();
QTabBar *tabs = nullptr;
for (auto *bar : main.findChildren<QTabBar *>()) {
if (bar->isVisible() && bar->count() == 2 && bar->tabText(0) == "Shared panel"
&& bar->tabText(1) == "Shared panel") {
tabs = bar;
break;
}
}
QVERIFY(tabs);
tabs->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), tabs);
QVERIFY(manager->closeCurrentTab());
QTRY_VERIFY(first->isHidden());
QVERIFY(!second->isHidden());
QTRY_VERIFY(!second->visibleRegion().isEmpty());
}
void numberedTabShortcuts_data()
{
QTest::addColumn<bool>("maximized");
QTest::newRow("tiled") << false;
QTest::newRow("maximized") << true;
}
void numberedTabShortcuts()
{
QFETCH(bool, maximized);
MainWindow main;
hideBuiltinPanels(main);
QList<IaitoDockWidget *> panels;
for (int i = 0; i < 5; ++i) {
panels.append(addPanel(main, QStringLiteral("tab-%1").arg(i + 1)));
}
auto *otherFirst = addPanel(main, "other-first");
auto *otherSelected = addPanel(main, "other-selected");
main.splitDockWidget(panels.first(), otherFirst, Qt::Horizontal);
for (int i = 1; i < panels.size(); ++i) {
main.tabifyDockWidget(panels.first(), panels.at(i));
}
main.tabifyDockWidget(otherFirst, otherSelected);
main.show();
QVERIFY(QTest::qWaitForWindowExposed(&main));
otherSelected->raise();
panels.last()->raise();
panels.last()->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), panels.last()->widget());
if (maximized) {
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return, panelModifier);
QTRY_VERIFY(otherSelected->isHidden());
}
for (int i = 0; i < panels.size(); ++i) {
QTest::keyClick(QApplication::focusWidget(), Qt::Key(Qt::Key_1 + i), panelModifier);
auto *selected = panels.at(i);
QTRY_VERIFY(!selected->visibleRegion().isEmpty());
QVERIFY(
selected == QApplication::focusWidget()
|| selected->isAncestorOf(QApplication::focusWidget()));
if (maximized) {
QVERIFY(otherSelected->isHidden());
for (auto *panel : panels) {
QCOMPARE(panel->isHidden(), panel != selected);
}
} else {
QVERIFY(!otherSelected->visibleRegion().isEmpty());
}
}
if (maximized) {
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return, panelModifier);
QTRY_VERIFY(!otherSelected->isHidden());
}
// Moving focus to another group makes the same keys operate on that group.
otherSelected->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), otherSelected->widget());
QTest::keyClick(QApplication::focusWidget(), Qt::Key_1, panelModifier);
QTRY_VERIFY(!otherFirst->visibleRegion().isEmpty());
QVERIFY(!panels.last()->visibleRegion().isEmpty());
QTest::keyClick(QApplication::focusWidget(), Qt::Key_2, panelModifier);
QTRY_VERIFY(!otherSelected->visibleRegion().isEmpty());
QVERIFY(!panels.last()->visibleRegion().isEmpty());
}
void numberedShortcutsFollowVisibleTabOrder()
{
MainWindow main;
hideBuiltinPanels(main);
QList<IaitoDockWidget *> panels;
for (int i = 0; i < 5; ++i) {
panels.append(addPanel(main, QStringLiteral("tab-%1").arg(i + 1)));
}
auto *standalone = addPanel(main, "standalone");
main.splitDockWidget(panels.first(), standalone, Qt::Horizontal);
for (int i = 1; i < panels.size(); ++i) {
main.tabifyDockWidget(panels.first(), panels.at(i));
}
main.show();
QVERIFY(QTest::qWaitForWindowExposed(&main));
panels.last()->raise();
panels.last()->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), panels.last()->widget());
QTest::qWait(50);
QTabBar *tabs = nullptr;
for (auto *bar : main.findChildren<QTabBar *>()) {
if (!bar->visibleRegion().isEmpty() && bar->count() == 5 && bar->tabText(0) == "tab-1") {
tabs = bar;
break;
}
}
QVERIFY(tabs);
tabs->moveTab(4, 3);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_4, panelModifier);
QTRY_VERIFY(!panels.last()->visibleRegion().isEmpty());
QTest::keyClick(QApplication::focusWidget(), Qt::Key_5, panelModifier);
QTRY_VERIFY(!panels.at(3)->visibleRegion().isEmpty());
panels.at(1)->hide();
QTRY_COMPARE(tabs->count(), 4);
QTest::keyClick(QApplication::focusWidget(), Qt::Key_3, panelModifier);
QTRY_VERIFY(!panels.last()->visibleRegion().isEmpty());
// Missing tab numbers and standalone panels leave the current view alone.
QWidget *focus = QApplication::focusWidget();
QTest::keyClick(focus, Qt::Key_5, panelModifier);
QCOMPARE(QApplication::focusWidget(), focus);
QVERIFY(!panels.last()->visibleRegion().isEmpty());
standalone->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), standalone->widget());
QTest::keyClick(QApplication::focusWidget(), Qt::Key_2, panelModifier);
QCOMPARE(QApplication::focusWidget(), standalone->widget());
QVERIFY(!panels.last()->visibleRegion().isEmpty());
}
void frameShortcutsPreserveSelectedTabs_data()
{
QTest::addColumn<bool>("maximized");
QTest::newRow("tiled") << false;
QTest::newRow("maximized") << true;
}
void frameShortcutsPreserveSelectedTabs()
{
QFETCH(bool, maximized);
MainWindow main;
hideBuiltinPanels(main);
// Registration order deliberately differs from the frame order on screen.
auto *bottomRight = addPanel(main, "bottom-right");
auto *rightFirst = addPanel(main, "right-first");
auto *rightSelected = addPanel(main, "right-selected");
auto *rightLast = addPanel(main, "right-last");
auto *bottomLeft = addPanel(main, "bottom-left");
auto *leftFirst = addPanel(main, "left-first");
auto *leftSelected = addPanel(main, "left-selected");
auto *leftLast = addPanel(main, "left-last");
auto *hidden = addPanel(main, "hidden");
auto *floating = addPanel(main, "floating");
main.splitDockWidget(leftFirst, rightFirst, Qt::Horizontal);
main.splitDockWidget(leftFirst, bottomLeft, Qt::Vertical);
main.splitDockWidget(rightFirst, bottomRight, Qt::Vertical);
main.tabifyDockWidget(leftFirst, leftSelected);
main.tabifyDockWidget(leftFirst, leftLast);
main.tabifyDockWidget(rightFirst, rightSelected);
main.tabifyDockWidget(rightFirst, rightLast);
hidden->hide();
floating->setAttribute(Qt::WA_ShowWithoutActivating);
floating->setFloating(true);
main.show();
main.activateWindow();
QVERIFY(QTest::qWaitForWindowExposed(&main));
QTest::qWait(50);
QApplication::setActiveWindow(&main);
leftSelected->raise();
rightSelected->raise();
const QList<IaitoDockWidget *> frames
= {leftSelected, rightSelected, bottomLeft, bottomRight};
for (auto *frame : frames) {
frame->raiseMemoryWidget();
frame->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), frame->widget());
}
leftSelected->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), leftSelected->widget());
QTest::qWait(50);
QVERIFY(leftSelected->geometry().left() < rightSelected->geometry().left());
QVERIFY(leftSelected->geometry().top() < bottomLeft->geometry().top());
QVERIFY(bottomLeft->geometry().left() < bottomRight->geometry().left());
if (maximized) {
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return, panelModifier);
QTRY_VERIFY(rightSelected->isHidden());
}
int current = 0;
for (Qt::Key key : {Qt::Key_J, Qt::Key_K}) {
// Traverse twice in each direction to verify wraparound and stable selection.
for (int step = 0; step < frames.size() * 2; ++step) {
current = (current + (key == Qt::Key_J ? 1 : frames.size() - 1)) % frames.size();
QTest::keyClick(QApplication::focusWidget(), key, panelModifier);
QTRY_COMPARE(QApplication::focusWidget(), frames.at(current)->widget());
QVERIFY(!frames.at(current)->visibleRegion().isEmpty());
QVERIFY(leftFirst->visibleRegion().isEmpty());
QVERIFY(leftLast->visibleRegion().isEmpty());
QVERIFY(rightFirst->visibleRegion().isEmpty());
QVERIFY(rightLast->visibleRegion().isEmpty());
QVERIFY(hidden->isHidden());
QVERIFY(floating->isFloating());
if (maximized) {
for (int i = 0; i < frames.size(); ++i) {
QCOMPARE(frames.at(i)->isHidden(), i != current);
}
}
}
}
if (maximized) {
QTest::keyClick(QApplication::focusWidget(), Qt::Key_Return, panelModifier);
QTRY_VERIFY(!rightSelected->isHidden());
}
QVERIFY(!leftSelected->visibleRegion().isEmpty());
QVERIFY(!rightSelected->visibleRegion().isEmpty());
}
void frameShortcutsSkipClosedFrames()
{
MainWindow main;
hideBuiltinPanels(main);
auto *left = addPanel(main, "left");
auto *rightFirst = addPanel(main, "right-first");
auto *rightSelected = addPanel(main, "right-selected");
main.splitDockWidget(left, rightFirst, Qt::Horizontal);
main.tabifyDockWidget(rightFirst, rightSelected);
main.show();
QVERIFY(QTest::qWaitForWindowExposed(&main));
rightSelected->raise();
rightSelected->widget()->setFocus();
QTRY_COMPARE(QApplication::focusWidget(), rightSelected->widget());
left->setTransient(true);
left->close();
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
QTest::qWait(50);
for (Qt::Key key : {Qt::Key_J, Qt::Key_K}) {
QTest::keyClick(QApplication::focusWidget(), key, panelModifier);
QCOMPARE(QApplication::focusWidget(), rightSelected->widget());
QVERIFY(!rightSelected->visibleRegion().isEmpty());
QVERIFY(rightFirst->visibleRegion().isEmpty());
}
}
};
QTEST_MAIN(PanelMaximizeTests)
#include "test_panelmaximize.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) ../seekablehistory.pro
$(MAKE) -C build -j8 > /dev/null
check: all
QT_QPA_PLATFORM=offscreen ./build/test_seekablehistory
+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_seekablehistory
DEFINES += IAITO_SOURCE_BUILD
PKGCONFIG += r_core
INCLUDEPATH += ../../src ../../src/core ../../src/common ../../src/widgets ../../build
SOURCES += test_seekablehistory.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,299 @@
#include "common/Configuration.h"
#include "common/IaitoSeekable.h"
#include "core/MainWindow.h"
#include "widgets/CallGraph.h"
#include "widgets/DataAnalysisWidget.h"
#include "widgets/DecompilerWidget.h"
#include "widgets/DisassemblyWidget.h"
#include <QMenu>
#include <QPlainTextEdit>
#include <QSettings>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QTest>
#include <QToolBar>
#include <QToolButton>
class SeekableHistoryTests : public QObject
{
Q_OBJECT
QTemporaryDir settings;
private slots:
void initTestCase()
{
QCoreApplication::setOrganizationName("iaito-tests");
QCoreApplication::setApplicationName("seekablehistory");
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);
Core()->cmd("o malloc://16384");
}
void init()
{
Core()->seek(0x100);
auto core = Core()->core();
r_io_sundo_reset(core->io);
}
void detachAtCurrentAddress()
{
IaitoSeekable view;
Core()->seek(0x200);
view.setSynchronization(false);
QCOMPARE(view.getOffset(), RVA(0x200));
QVERIFY(!view.canSeekPrev());
QVERIFY(!view.canSeekNext());
view.seekPrev();
view.seekNext();
view.seek(RVA_INVALID);
QCOMPARE(view.getOffset(), RVA(0x200));
}
void backForwardAndBranch()
{
IaitoSeekable view;
view.setSynchronization(false);
view.seek(0x200);
view.seek(0x300);
QVERIFY(view.canSeekPrev());
QVERIFY(!view.canSeekNext());
view.seekPrev();
QCOMPARE(view.getOffset(), RVA(0x200));
view.seekPrev();
QCOMPARE(view.getOffset(), RVA(0x100));
QVERIFY(!view.canSeekPrev());
view.seekPrev();
QCOMPARE(view.getOffset(), RVA(0x100));
view.seekNext();
QCOMPARE(view.getOffset(), RVA(0x200));
view.seekNext();
QCOMPARE(view.getOffset(), RVA(0x300));
view.seekNext();
QCOMPARE(view.getOffset(), RVA(0x300));
view.seekPrev();
view.seek(0x400);
QVERIFY(!view.canSeekNext());
view.seekPrev();
QCOMPARE(view.getOffset(), RVA(0x200));
view.seekNext();
QCOMPARE(view.getOffset(), RVA(0x400));
QCOMPARE(Core()->getOffset(), RVA(0x100));
}
void repeatedSeekKeepsForwardHistory()
{
IaitoSeekable view;
view.setSynchronization(false);
view.seek(0x200);
view.seekPrev();
QSignalSpy seeks(&view, &IaitoSeekable::seekableSeekChanged);
view.seek(0x100);
view.seek(RVA_INVALID);
QCOMPARE(seeks.count(), 0);
QVERIFY(view.canSeekNext());
view.seekNext();
QCOMPARE(view.getOffset(), RVA(0x200));
view.seekPrev();
QVERIFY(!view.canSeekPrev());
}
void independentViewsAndGlobalHistory()
{
IaitoSeekable first;
IaitoSeekable second;
first.setSynchronization(false);
second.setSynchronization(false);
first.seek(0x200);
second.seek(0x300);
Core()->seek(0x400);
first.seekPrev();
QCOMPARE(first.getOffset(), RVA(0x100));
QCOMPARE(second.getOffset(), RVA(0x300));
QCOMPARE(Core()->getOffset(), RVA(0x400));
Core()->seekPrev();
QCOMPARE(Core()->getOffset(), RVA(0x100));
QCOMPARE(second.getOffset(), RVA(0x300));
first.seekNext();
QCOMPARE(first.getOffset(), RVA(0x200));
}
void reattachAndDetach()
{
IaitoSeekable view;
view.setSynchronization(false);
view.seek(0x200);
Core()->seek(0x300);
view.setSynchronization(true);
QCOMPARE(view.getOffset(), RVA(0x300));
view.seekPrev();
QCOMPARE(Core()->getOffset(), RVA(0x100));
view.seekNext();
QCOMPARE(Core()->getOffset(), RVA(0x300));
view.setSynchronization(false);
QCOMPARE(view.getOffset(), RVA(0x300));
QVERIFY(!view.canSeekPrev());
view.seek(0x400);
view.setSynchronization(false);
view.seekPrev();
QCOMPARE(view.getOffset(), RVA(0x300));
QVERIFY(!view.canSeekPrev());
}
void historyAvailabilitySignals()
{
IaitoSeekable view;
view.setSynchronization(false);
QSignalSpy changes(&view, &IaitoSeekable::historyChanged);
view.seek(0x200);
QCOMPARE(changes.count(), 1);
view.seekPrev();
QCOMPARE(changes.count(), 2);
view.seekNext();
QCOMPARE(changes.count(), 3);
view.seekNext();
view.seek(0x200);
QCOMPARE(changes.count(), 3);
}
void viewHistoryBars_data()
{
QTest::addColumn<int>("type");
QTest::newRow("disassembly") << int(MemoryWidgetType::Disassembly);
QTest::newRow("graph") << int(MemoryWidgetType::Graph);
QTest::newRow("hex") << int(MemoryWidgetType::Hexdump);
QTest::newRow("decompiler") << int(MemoryWidgetType::Decompiler);
QTest::newRow("data-analysis") << 4;
QTest::newRow("call-graph") << 5;
}
void viewHistoryBars()
{
QFETCH(int, type);
MainWindow main;
AddressableDockWidget *dock;
if (type == 4) {
dock = new DataAnalysisWidget(&main);
} else if (type == 5) {
dock = new CallGraphWidget(&main, false);
} else {
dock = main.addNewMemoryWidget(MemoryWidgetType(type), 0x100);
}
dock->getSeekable()->setSynchronization(false);
dock->setParent(nullptr);
dock->resize(1000, 500);
dock->show();
QVERIFY(QTest::qWaitForWindowExposed(dock));
auto *bar = dock->findChild<QToolBar *>("seekHistoryBar");
QVERIFY(bar && bar->isVisible());
auto *back = bar->findChild<QAction *>("seekBackAction");
auto *forward = bar->findChild<QAction *>("seekForwardAction");
QVERIFY(back && forward);
QVERIFY(!back->isEnabled());
QVERIFY(!forward->isEnabled());
dock->getSeekable()->seek(0x200);
dock->getSeekable()->seek(0x300);
QVERIFY(back->isEnabled());
QTest::mouseClick(bar->widgetForAction(back), Qt::LeftButton);
QCOMPARE(dock->getSeekable()->getOffset(), RVA(0x200));
QVERIFY(forward->isEnabled());
QTest::mouseClick(bar->widgetForAction(forward), Qt::LeftButton);
QCOMPARE(dock->getSeekable()->getOffset(), RVA(0x300));
QCOMPARE(Core()->getOffset(), RVA(0x100));
dock->getSeekable()->setSynchronization(true);
QCOMPARE(bar->isVisible(), type == int(MemoryWidgetType::Decompiler));
main.removeWidget(dock);
delete dock;
}
void decompilerBarAboveCode()
{
MainWindow main;
DecompilerWidget dock(&main);
dock.setParent(nullptr);
dock.resize(1000, 500);
dock.show();
QVERIFY(QTest::qWaitForWindowExposed(&dock));
auto *bar = dock.findChild<QToolBar *>("seekHistoryBar");
auto *edit = dock.findChild<QPlainTextEdit *>("textEdit");
QVERIFY(bar && edit);
QVERIFY(bar->isVisible());
QVERIFY(bar->height() < 50);
QVERIFY(bar->mapTo(&dock, QPoint(0, bar->height())).y() <= edit->mapTo(&dock, QPoint()).y());
auto *back = bar->findChild<QAction *>("seekBackAction");
auto *forward = bar->findChild<QAction *>("seekForwardAction");
QVERIFY(back && forward);
QVERIFY(!back->isEnabled());
Core()->seek(0x200);
QVERIFY(back->isEnabled());
back->trigger();
QCOMPARE(Core()->getOffset(), RVA(0x100));
QVERIFY(forward->isEnabled());
forward->trigger();
QCOMPARE(Core()->getOffset(), RVA(0x200));
}
void localShortcutsAndMainHistory()
{
MainWindow main;
auto *dock = main.addNewMemoryWidget(MemoryWidgetType::Decompiler, 0x100, false);
dock->setParent(nullptr);
dock->resize(1000, 500);
dock->show();
QVERIFY(QTest::qWaitForWindowExposed(dock));
auto *edit = dock->findChild<QPlainTextEdit *>("textEdit");
QVERIFY(edit);
edit->setFocus();
QTRY_VERIFY(edit->hasFocus());
auto *view = dock->getSeekable();
view->seek(0x200);
view->seek(0x300);
QVERIFY(QMetaObject::invokeMethod(&main, "on_actionBackward_triggered"));
QCOMPARE(view->getOffset(), RVA(0x200));
QVERIFY(QMetaObject::invokeMethod(&main, "on_actionForward_triggered"));
QCOMPARE(view->getOffset(), RVA(0x300));
QTest::keyClick(edit, Qt::Key_Escape);
QCOMPARE(view->getOffset(), RVA(0x200));
QTest::mouseClick(edit->viewport(), Qt::ForwardButton);
QCOMPARE(view->getOffset(), RVA(0x300));
QTest::mouseClick(edit->viewport(), Qt::BackButton);
QCOMPARE(view->getOffset(), RVA(0x200));
auto *menu = main.findChild<QMenu *>("historyMenu");
QVERIFY(menu);
QVERIFY(QMetaObject::invokeMethod(menu, "aboutToShow"));
QCOMPARE(menu->actions().size(), 2);
QVERIFY(!menu->actions().first()->isEnabled());
menu->actions().last()->trigger();
QCOMPARE(view->getOffset(), RVA(0x100));
auto *forwardMenu = main.findChild<QMenu *>("forwardHistoryMenu");
QVERIFY(forwardMenu);
QVERIFY(QMetaObject::invokeMethod(forwardMenu, "aboutToShow"));
QCOMPARE(forwardMenu->actions().size(), 3);
forwardMenu->actions().last()->trigger();
QCOMPARE(view->getOffset(), RVA(0x300));
QCOMPARE(Core()->getOffset(), RVA(0x100));
main.removeWidget(dock);
delete dock;
}
void disassemblyEscapeUsesLocalHistory()
{
DisassemblyWidget dock(nullptr);
auto *view = dock.getSeekable();
view->setSynchronization(false);
view->seek(0x200);
view->seek(0x300);
dock.seekPrev();
QCOMPARE(view->getOffset(), RVA(0x200));
QCOMPARE(Core()->getOffset(), RVA(0x100));
}
};
QTEST_MAIN(SeekableHistoryTests)
#include "test_seekablehistory.moc"
+11
View File
@@ -0,0 +1,11 @@
include ../../config.mk
.PHONY: all check
all:
$(MAKE) -C ../..
mkdir -p build
cd build && $(QMAKE) ../structures.pro
$(MAKE) -C build
check: all
QT_QPA_PLATFORM=offscreen ./build/test_structures
+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_structures
DEFINES += IAITO_SOURCE_BUILD
PKGCONFIG += r_core
INCLUDEPATH += ../../src ../../src/core ../../src/common ../../build
SOURCES += test_structures.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
+351
View File
@@ -0,0 +1,351 @@
#include "common/Configuration.h"
#include "common/TempConfig.h"
#include "common/TypeLayout.h"
#include "core/Iaito.h"
#include "widgets/HexdumpWidget.h"
#include <QComboBox>
#include <QFile>
#include <QFileInfo>
#include <QLabel>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QSettings>
#include <QSignalSpy>
#include <QTabWidget>
#include <QTemporaryDir>
#include <QTest>
#include <QTreeWidget>
static const char *definitions = R"TYPES(
Pair=struct
struct.Pair=a,b
struct.Pair.a=uint16_t,0,0
struct.Pair.b=uint16_t,4,0
PairAlias=typedef
typedef.PairAlias=struct Pair
Variant=union
union.Variant=word,wide
union.Variant.word=uint32_t,0,0
union.Variant.wide=uint64_t,0,0
Node=struct
struct.Node=value,next
struct.Node.value=uint32_t,0,0
struct.Node.next=struct Node *,8,0
Choice=enum
enum.Choice=YES,NO
enum.Choice.YES=1
enum.Choice.NO=2
Packet=struct
struct.Packet=magic,pair,list,flags,variant,node,signedValue,choice
struct.Packet.magic=uint32_t,0,0
struct.Packet.pair=PairAlias,4,0
struct.Packet.list=struct Pair,16,2
struct.Packet.flags=uint8_t,32,3
struct.Packet.variant=union Variant,40,0
struct.Packet.node=struct Node *,48,0
struct.Packet.signedValue=int32_t,56,0
struct.Packet.choice=enum Choice,60,0
Bad=struct
struct.Bad=self
struct.Bad.self=struct Bad,0,0
AliasCycle=typedef
typedef.AliasCycle=AliasCycle
)TYPES";
class StructuresTest : public QObject
{
Q_OBJECT
QTemporaryDir temporary;
static void inspect(HexdumpWidget &widget, const QString &type, const QString &address)
{
auto *tabs = widget.findChild<QTabWidget *>("hexSideTab_2");
tabs->setCurrentWidget(widget.findChild<QWidget *>("tabStructures"));
widget.findChild<QComboBox *>("structureTypeComboBox")->setCurrentText(type);
widget.findChild<QLineEdit *>("structureAddressEdit")->setText(address);
widget.findChild<QPushButton *>("structureInspectButton")->click();
}
static void clearPins()
{
TypeLayout layout;
layout.load(Core()->cmdRaw("tk"), 8);
const auto links = layout.links();
for (auto it = links.cbegin(); it != links.cend(); ++it) {
Core()->cmdRaw(QStringLiteral("tl- 0x%1").arg(it.key(), 0, 16));
}
Core()->cmd("Cr-*");
}
private slots:
void initTestCase()
{
QVERIFY(temporary.isValid());
QCoreApplication::setOrganizationName("iaito-tests");
QCoreApplication::setApplicationName("structures");
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, temporary.path());
Core()->initialize(false);
qRegisterMetaType<RVA>("RVA");
Config()->loadInitial();
Config()->setOutputRedirectionEnabled(false);
Core()->cmd("o malloc://1048576 0x1000");
Core()->cmd("wx 78563412 @ 0x1000");
Core()->cmd("wx 0100 @ 0x1004");
Core()->cmd("wx 0200 @ 0x1008");
Core()->cmd("wx 0300 @ 0x1010");
Core()->cmd("wx 0400 @ 0x1014");
Core()->cmd("wx 0500 @ 0x1016");
Core()->cmd("wx 0600 @ 0x101a");
Core()->cmd("wx 070809 @ 0x1020");
Core()->cmd("wx ffffffffffffffff @ 0x1028");
Core()->cmd("wx 0011000000000000 @ 0x1030");
Core()->cmd("wx feffffff01000000 @ 0x1038");
Core()->cmd("wx 07000000 @ 0x1100");
Core()->cmd("wx 0011000000000000 @ 0x1108");
for (const QString &line :
QString::fromLatin1(definitions).split('\n', Qt::SkipEmptyParts)) {
Core()->cmdRaw(QStringLiteral("tk ") + line);
}
Core()->seek(0x1000);
}
void init()
{
Core()->setConfig("asm.bits", 64);
Core()->setConfig("cfg.bigendian", false);
clearPins();
Core()->seek(0x1000);
}
void layoutAndValues()
{
TypeLayout layout;
layout.load(Core()->cmdRaw("tk"), 8);
QCOMPARE(layout.size("PairAlias"), quint64(6));
QCOMPARE(layout.fields("PairAlias").at(1).offset, quint64(4));
QCOMPARE(layout.size("Packet"), quint64(64));
QCOMPARE(layout.size("Variant"), quint64(8));
QCOMPARE(layout.fields("Variant").at(1).offset, quint64(0));
QCOMPARE(layout.size("Node"), quint64(16));
QCOMPARE(layout.size("Node *"), quint64(8));
QCOMPARE(layout.size("uint16_t[2][3]"), quint64(12));
QCOMPARE(layout.size("uint64_t[18446744073709551615]"), quint64(0));
QCOMPARE(layout.size("Bad"), quint64(0));
QCOMPARE(layout.size("AliasCycle"), quint64(0));
QCOMPARE(
layout.value("uint64_t", QByteArray(8, char(0xff)), false),
QString("18446744073709551615 (0xffffffffffffffff)"));
QCOMPARE(
layout.value("int32_t", QByteArray::fromHex("feffffff"), false),
QString("-2 (0xfffffffe)"));
QCOMPARE(layout.value("uint16_t", QByteArray::fromHex("0102"), true), QString("258 (0x102)"));
QCOMPARE(layout.value("float", QByteArray::fromHex("0000803f"), false), QString("1"));
QCOMPARE(
layout.value("Choice", QByteArray::fromHex("01000000"), false),
QString("1 (YES) (0x1)"));
layout.load(Core()->cmdRaw("tk"), 4);
QCOMPARE(layout.size("Node *"), quint64(4));
}
void emptyViewportFetch()
{
MemoryData data;
data.fetch(0x1000, 0);
QVERIFY(data.minIndex() > data.maxIndex());
data.fetch(0x1000, -1);
QVERIFY(data.minIndex() > data.maxIndex());
data.fetch(0x1000, 4);
QByteArray bytes(4, 0);
QVERIFY(data.copy(bytes.data(), 0x1000, 4));
QCOMPARE(bytes.toHex(), QByteArray("78563412"));
}
void nestedFieldsSelectionAndRefresh()
{
HexdumpWidget widget(nullptr);
widget.resize(1200, 700);
widget.show();
inspect(widget, "Packet", "0x1000");
auto *tree = widget.findChild<QTreeWidget *>("structureTree");
QCOMPARE(tree->topLevelItemCount(), 1);
auto *root = tree->topLevelItem(0);
QCOMPARE(root->childCount(), 8);
QCOMPARE(root->text(4), QString("64"));
QCOMPARE(root->child(0)->text(2), QString("305419896 (0x12345678)"));
auto *pair = root->child(1);
QCOMPARE(pair->text(1), QString("PairAlias"));
QCOMPARE(pair->childCount(), 1); // Lazy placeholder.
pair->setExpanded(true);
QCOMPARE(pair->childCount(), 2);
QCOMPARE(pair->child(1)->text(2), QString("2 (0x2)"));
QCOMPARE(pair->child(1)->data(3, Qt::UserRole).toULongLong(), qulonglong(0x1008));
QVERIFY(pair->child(1)->background(0).color().alpha() > 0);
auto *list = root->child(2);
list->setExpanded(true);
QCOMPARE(list->childCount(), 2);
list->child(1)->setExpanded(true);
QCOMPARE(list->child(1)->child(1)->text(2), QString("6 (0x6)"));
QCOMPARE(list->child(1)->child(1)->data(3, Qt::UserRole).toULongLong(), qulonglong(0x101a));
root->child(3)->setExpanded(true);
QCOMPARE(root->child(3)->child(2)->text(2), QString("9 (0x9)"));
root->child(4)->setExpanded(true);
QCOMPARE(
root->child(4)->child(1)->text(2), QString("18446744073709551615 (0xffffffffffffffff)"));
QCOMPARE(root->child(6)->text(2), QString("-2 (0xfffffffe)"));
tree->setCurrentItem(pair->child(1));
auto *hex = widget.findChild<HexWidget *>("hexTextView");
QCOMPARE(hex->getSelection().startAddress, RVA(0x1008));
QCOMPARE(hex->getSelection().endAddress, RVA(0x1009));
QCOMPARE(Core()->getOffset(), RVA(0x1008));
QCOMPARE(tree->topLevelItem(0)->data(3, Qt::UserRole).toULongLong(), qulonglong(0x1000));
const QString screenshot = qEnvironmentVariable("STRUCTURES_SCREENSHOT");
if (!screenshot.isEmpty()) {
QTest::qWait(30);
QVERIFY(widget.grab().save(screenshot));
}
Core()->cmd("wx 0900 @ 0x1008");
widget.findChild<QPushButton *>("structureRefreshButton")->click();
root = tree->topLevelItem(0);
QVERIFY(root->child(1)->isExpanded());
QCOMPARE(root->child(1)->child(1)->text(2), QString("9 (0x9)"));
QCOMPARE(tree->currentItem(), root->child(1)->child(1));
Core()->cmd("wx 0200 @ 0x1008");
Core()->setConfig("cfg.bigendian", true);
widget.findChild<QPushButton *>("structureRefreshButton")->click();
QCOMPARE(tree->topLevelItem(0)->child(0)->text(2), QString("2018915346 (0x78563412)"));
inspect(widget, "Packet", "not_an_address");
QCOMPARE(tree->topLevelItemCount(), 0);
inspect(widget, "Bad", "0x1000");
QCOMPARE(tree->topLevelItemCount(), 0);
}
void pointersAndLargeArrays()
{
HexdumpWidget widget(nullptr);
inspect(widget, "Packet", "0x1000");
auto *tree = widget.findChild<QTreeWidget *>("structureTree");
auto *pointer = tree->topLevelItem(0)->child(5);
pointer->setExpanded(true);
QCOMPARE(pointer->childCount(), 1);
auto *node = pointer->child(0);
QCOMPARE(node->data(3, Qt::UserRole).toULongLong(), qulonglong(0x1100));
node->setExpanded(true);
QCOMPARE(node->child(0)->text(2), QString("7 (0x7)"));
node->child(1)->setExpanded(true);
QCOMPARE(node->child(1)->child(0)->text(2), QString("Recursive reference"));
QCOMPARE(node->child(1)->child(0)->childCount(), 0);
inspect(widget, "uint8_t[100000]", "0x1000");
auto *root = tree->topLevelItem(0);
QVERIFY(root->childCount() <= 256);
auto *last = root;
while (last->childCount()) {
last = last->child(last->childCount() - 1);
last->setExpanded(true);
QVERIFY(last->childCount() <= 256);
}
QCOMPARE(last->data(3, Qt::UserRole).toULongLong(), qulonglong(0x1000 + 99999));
}
void typeAndCommandPins()
{
Core()->cmd("tl Packet = 0x1000");
Core()->cmd("Cr 4 px 4 @ 0x1080");
Core()->setCommandAnnotation(0x1090, 4, "px 4");
Core()->setCommandAnnotation(0x10a0, 4, "px 4 @ 0x1000; ?v 123");
QCOMPARE(Core()->getCommandAnnotation(0x10a0), QString("px 4 @ 0x1000; ?v 123"));
QCOMPARE(Core()->getOffset(), RVA(0x1000)); // Pinning never executes a command or seeks.
HexdumpWidget widget(nullptr);
auto *pins = widget.findChild<QTreeWidget *>("structurePinsTree");
QCOMPARE(pins->topLevelItemCount(), 4);
auto *tree = widget.findChild<QTreeWidget *>("structureTree");
auto *run = widget.findChild<QPushButton *>("structureRunButton");
QVERIFY(!run->isEnabled());
pins->itemActivated(pins->topLevelItem(0), 0);
QCOMPARE(Core()->getOffset(), RVA(0x1000));
QCOMPARE(tree->topLevelItemCount(), 1);
QCOMPARE(tree->topLevelItem(0)->text(0), QString("Packet"));
inspect(widget, "PairAlias", "0x1080");
widget.findChild<QPushButton *>("structurePinButton")->click();
QVERIFY(Core()->cmdRaw("tl").contains("0x00001080 = PairAlias"));
widget.findChild<QPushButton *>("structureRefreshButton")->click();
QTreeWidgetItem *command = nullptr;
for (int i = 0; i < pins->topLevelItemCount(); ++i) {
auto *pin = pins->topLevelItem(i);
if (pin->data(0, Qt::UserRole).toULongLong() == 0x1080
&& pin->data(1, Qt::UserRole).toBool()) {
command = pin;
}
}
QVERIFY(command);
QCOMPARE(command->text(1), QString("px 4"));
pins->setCurrentItem(command);
QVERIFY(run->isEnabled());
run->click();
QVERIFY(
!widget.findChild<QPlainTextEdit *>("structureCommandOutput")->toPlainText().isEmpty());
widget.findChild<QPushButton *>("structureUnpinButton")->click();
QVERIFY(Core()->getCommandAnnotation(0x1080).isEmpty());
QVERIFY(Core()->cmdRaw("tl").contains("0x00001080 = PairAlias"));
TypeLayout layout;
layout.load(QStringLiteral("link.0020000000000001=Packet\n") + definitions, 8);
QCOMPARE(layout.links().firstKey(), quint64(0x20000000000001));
}
void wideAddressesAndProjectPersistence()
{
const RVA address = 0x20000000000001;
Core()->cmd("o malloc://128 0x20000000000001");
Core()->cmd("tl Packet = 0x20000000000001");
Core()->setCommandAnnotation(address + 64, 4, "px 4");
HexdumpWidget widget(nullptr);
auto *pins = widget.findChild<QTreeWidget *>("structurePinsTree");
QCOMPARE(pins->topLevelItemCount(), 2);
QCOMPARE(pins->topLevelItem(0)->data(0, Qt::UserRole).toULongLong(), qulonglong(address));
QCOMPARE(pins->topLevelItem(1)->data(0, Qt::UserRole).toULongLong(), qulonglong(address + 64));
pins->itemActivated(pins->topLevelItem(0), 0);
auto *tree = widget.findChild<QTreeWidget *>("structureTree");
QCOMPARE(tree->topLevelItemCount(), 1);
QCOMPARE(
tree->topLevelItem(0)->child(1)->data(3, Qt::UserRole).toULongLong(),
qulonglong(address + 4));
TempConfig projectConfig;
projectConfig.set("dir.projects", temporary.path()).set("prj.vc", false);
QSignalSpy saved(Core(), &IaitoCore::projectSaved);
Core()->saveProject("pins");
QCOMPARE(saved.count(), 1);
QVERIFY(saved.first().at(0).toBool());
QVERIFY(QFileInfo::exists(temporary.filePath("pins/prj.bin")));
QVERIFY(QFileInfo::exists(temporary.filePath("pins/iaito-memory-pins.json")));
clearPins();
QVERIFY(Core()->getCommandAnnotation(address + 64).isEmpty());
Core()->openProject("pins");
TypeLayout layout;
layout.load(Core()->cmdRaw("tk"), 8);
QCOMPARE(layout.links().value(address), QString("Packet"));
QCOMPARE(layout.size("Packet"), quint64(64));
QCOMPARE(Core()->getCommandAnnotation(address + 64), QString("px 4"));
RVA size = 0;
Core()->getCommandAnnotation(address + 64, &size);
QCOMPARE(size, RVA(4));
clearPins();
// r2's metadata-only binary project load leaves no current core file.
Core()->cmd("o malloc://128 0x20000000000001");
Core()->saveProject("pins");
QVERIFY(saved.last().at(0).toBool());
Core()->openProject("pins");
layout.load(Core()->cmdRaw("tk"), 8);
QVERIFY(layout.links().isEmpty());
QVERIFY(Core()->getCommandAnnotation(address + 64).isEmpty());
}
};
QTEST_MAIN(StructuresTest)
#include "test_structures.moc"