mirror of
https://github.com/radareorg/iaito.git
synced 2026-11-07 14:38:16 -10:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4badc5fd4 | ||
|
|
49b4b19c21 | ||
|
|
068520f923 | ||
|
|
f723ee3dee | ||
|
|
ce1f9181cf | ||
|
|
a043f93e72 | ||
|
|
c0cb4a5123 | ||
|
|
db64c517a4 | ||
|
|
9602d98307 | ||
|
|
d85739b5aa | ||
|
|
1828006c2b | ||
|
|
aff259387b | ||
|
|
fe80cff908 | ||
|
|
ae183f2c43 | ||
|
|
9ae80e4d62 | ||
|
|
e920c6aba8 | ||
|
|
5875f989d8 | ||
|
|
d95daf392f | ||
|
|
6c4997ac29 | ||
|
|
a8d239febd | ||
|
|
6b52aa0be1 | ||
|
|
19c3c07b99 | ||
|
|
ef8aba25e8 | ||
|
|
c2f042643a | ||
|
|
baf1afaca0 | ||
|
|
de8f2b476e | ||
|
|
bc6c362bd5 | ||
|
|
bff22b12bd | ||
|
|
63f2bd57cd | ||
|
|
5518aaa29d | ||
|
|
73856c6c32 | ||
|
|
018c210777 | ||
|
|
59a47845f0 | ||
|
|
11b530a526 | ||
|
|
f6b792020a | ||
|
|
c50dc3313f |
No files matched your search
+116
-56
@@ -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
|
||||
@@ -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,4 +1,4 @@
|
||||
include config.mk
|
||||
-include config.mk
|
||||
|
||||
ifeq ($(shell uname),Darwin)
|
||||
BIN=build/iaito.app/Contents/MacOS/iaito
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
PKGNAME iaito
|
||||
VERSION 6.2.4
|
||||
VERSION 6.2.6
|
||||
CONTACT pancake ; [email protected]
|
||||
|
||||
LANG_CXX!
|
||||
|
||||
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -154,6 +154,9 @@ int main(int argc, char *argv[])
|
||||
#endif
|
||||
|
||||
IaitoApplication a(argc, argv);
|
||||
if (!a.getMainWindow()) {
|
||||
return a.getExitCode();
|
||||
}
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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())));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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">
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" 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<>" name="fromTreeWidget">
|
||||
<widget class="AddressableItemView" name="fromTreeWidget">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
@@ -142,7 +142,7 @@
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AddressableItemList<></class>
|
||||
<class>AddressableItemView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/AddressableItemList.h</header>
|
||||
</customwidget>
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,8 +25,8 @@ spacing=6
|
||||
sliderGroove=4
|
||||
|
||||
[palette]
|
||||
accent=#039be5
|
||||
accentText=#ffffff
|
||||
accent=#c6dff2
|
||||
accentText=#00162e
|
||||
background=#fafcfe
|
||||
border=#bdbdbd
|
||||
mutedText=#7d7d7d
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -233,4 +233,6 @@ private:
|
||||
}
|
||||
};
|
||||
|
||||
using AddressableItemView = AddressableItemList<>;
|
||||
|
||||
#endif // ADDRESSABLE_ITEM_LIST_H
|
||||
@@ -28,7 +28,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" name="breakpointTreeView">
|
||||
<widget class="AddressableItemView" name="breakpointTreeView">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">IaitoTreeView::item
|
||||
{
|
||||
@@ -80,7 +80,7 @@
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AddressableItemList<></class>
|
||||
<class>AddressableItemView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/AddressableItemList.h</header>
|
||||
<container>1</container>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -44,6 +44,7 @@ static const char *refreshAllCommandPrefixes[] = {
|
||||
"C",
|
||||
"aa",
|
||||
"af",
|
||||
"avg",
|
||||
"w",
|
||||
};
|
||||
|
||||
|
||||
@@ -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 ®ion : regions) {
|
||||
if (region.collapsed) {
|
||||
collapsed.insert({region.first, region.last});
|
||||
}
|
||||
}
|
||||
}
|
||||
regions.clear();
|
||||
setPlainText(text);
|
||||
foldingSource = normalizedSource;
|
||||
findFoldRegions(normalizedSource);
|
||||
for (FoldRegion ®ion : 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 ®ion, 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 ®ion : regions) {
|
||||
region.collapsed = true;
|
||||
}
|
||||
applyFolding();
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::expandAll()
|
||||
{
|
||||
for (FoldRegion ®ion : regions) {
|
||||
region.collapsed = false;
|
||||
}
|
||||
applyFolding();
|
||||
}
|
||||
|
||||
void DecompilerTextEdit::applyFolding()
|
||||
{
|
||||
QVector<int> depth(blockCount() + 1, 0);
|
||||
for (const FoldRegion ®ion : 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 ®ion : regions) {
|
||||
if (region.collapsed && region.first < line && region.last > line) {
|
||||
openings.insert(region.first);
|
||||
}
|
||||
}
|
||||
if (!openings.isEmpty()) {
|
||||
for (FoldRegion ®ion : 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 ®ion : 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(" ..."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/*********************
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -70,6 +70,7 @@ public:
|
||||
~FlagsWidget();
|
||||
|
||||
private slots:
|
||||
void selectFlagRange(const QModelIndex &index);
|
||||
void on_flagspaceCombo_currentTextChanged(const QString &arg1);
|
||||
|
||||
void on_actionRename_triggered();
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" 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<></class>
|
||||
<class>AddressableItemView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/AddressableItemList.h</header>
|
||||
<container>1</container>
|
||||
|
||||
@@ -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."));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -986,6 +986,108 @@ li.checked::marker { content: "\2612"; }
|
||||
</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">
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" name="treeView">
|
||||
<widget class="AddressableItemView" name="treeView">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">IaitoTreeView::item
|
||||
{
|
||||
@@ -58,7 +58,7 @@
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AddressableItemList<></class>
|
||||
<class>AddressableItemView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/AddressableItemList.h</header>
|
||||
<container>1</container>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" name="searchTreeView">
|
||||
<widget class="AddressableItemView" name="searchTreeView">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">IaitoTreeView::item
|
||||
{
|
||||
@@ -118,7 +118,7 @@
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>AddressableItemList<></class>
|
||||
<class>AddressableItemView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/AddressableItemList.h</header>
|
||||
<container>1</container>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 §ion) = 0;
|
||||
virtual RVA getAddressOfSection(const SectionDescription §ion) = 0;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AddressableItemList<>" 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<></class>
|
||||
<class>AddressableItemView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/AddressableItemList.h</header>
|
||||
<container>1</container>
|
||||
|
||||
@@ -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")
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user