Compare commits

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

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

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

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

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

Validation: make -C tests/panelmaximize check (11 passed).
2026-10-13 23:27:21 +02:00
pancake bff22b12bd Meta+Enter zooms the panel like in dwm/r2panels 2026-10-13 23:11:34 +02:00
pancake 63f2bd57cd Select small flag ranges in the hexdump on click 2026-10-13 19:31:32 +02:00
pancake 5518aaa29d Keep hexdump types and pins when reopening projects 2026-10-13 19:07:01 +02:00
pancake 73856c6c32 Add a recursive structure tree and pins to the hexdump ## 2026-10-13 19:06:51 +02:00
pancake 018c210777 Fix empty hexdump fetches allocating excessive memory 2026-10-13 19:06:22 +02:00
pancake 59a47845f0 Move Flags from Bin to Data in the View menu 2026-10-13 18:27:01 +02:00
pancake 11b530a526 Add global variable editing and navigation widget ## 2026-10-12 18:37:49 +02:00
pancake f6b792020a Add filesystem browsing and code navigation tests 2026-10-08 19:15:27 +02:00
pancake c50dc3313f Add source file browsing and clickable address previews ## 2026-10-08 19:15:00 +02:00
pancake 0e5a49c331 Fix make run without make install in macos 2026-10-07 15:52:48 +02:00
pancake 74c12685a0 Show options for the decai decompiler 2026-10-07 15:46:13 +02:00
pancake 93af6edfe9 Toggle decompiler with TAB, toggle graph with SPACE 2026-10-07 15:21:55 +02:00
pancake 977578a91e Basic block colors in minigraph and hexdump too 2026-10-07 07:59:44 +02:00
pancake e980b1cb48 Release 6.2.4 2026-10-06 00:08:21 +02:00
pancake b5edef36bc Add hack to workaround r2 6.2.4 r_cmd.h msvc issue 2026-10-05 22:28:23 +02:00
pancake 1208624c77 MakeFmt 2026-10-01 17:20:00 +02:00
pancake eff435fc29 Bounded scrollbar by default 2026-10-01 15:39:46 +02:00
pancake af1f536b0f Hightlight background of the actionable items on hover 2026-10-01 15:26:50 +02:00
pancake 6affec227d Pointing hand cursor for the actionable items in disasm too 2026-10-01 15:11:13 +02:00
pancake e0a120ad08 Implemented the pointing-hand cursor over flags in HexWidget 2026-10-01 14:33:47 +02:00
pancake 3383cc1d92 Fix disasm glitch 2026-09-30 16:35:21 +02:00
pancake 7eade2df0c Pinch to zoom in the data analysis 2026-09-30 09:43:21 +02:00
pancake bd1105f45d Introduce the hexpat widget 2026-09-29 21:40:24 +02:00
pancake a7532b4d80 Introduce the advanced DataAnalysis widget 2026-09-29 21:30:37 +02:00
pancake 11240a1a0f Optimize list filtering and type combobox 2026-09-28 12:55:31 +02:00
pancake ddb7bdb428 Better tree collapsing behaviours in listings 2026-09-28 12:36:32 +02:00
pancake b912778e62 Better demangling support on listings 2026-09-28 12:20:14 +02:00
pancake dc1fde07f5 Tree/list and shared filter for functions, symbols, imports, exports and flags 2026-09-28 11:56:59 +02:00
pancake 1e089d9c37 Introduce the ExceptionBar for disasm and graph navigation 2026-09-28 11:13:48 +02:00
pancake e0e9690fd5 Add support for collapsing sparse gaps like pxs do in r2 2026-09-28 06:21:59 +02:00
pancake bbbea0adcd Release 6.2.2 2026-09-06 21:29:54 +02:00
pancake f8ef719e31 Zoom widget can be shrinked more 2026-09-04 01:01:27 +02:00
pancake e560d5f88c Add scrollbar to the disasm and hexdump widgets 2026-09-04 00:48:54 +02:00
pancake 3f9e2f78f9 Add customizable resolution in the navigation bar 2026-09-04 00:28:08 +02:00
pancake d29c291ff7 Fix recent scrolling bugs 2026-09-04 00:24:00 +02:00
pancake e62e0a9d70 Optimize scrolling in the hexdump widget 2026-09-03 22:08:02 +02:00
pancake 0ccc495308 Optimize scrolling in the disassembly widget 2026-09-03 22:07:57 +02:00
pancake 95ca7a071f Export name after address 2026-09-03 20:21:54 +02:00
pancake 4e100c4fc3 Not zoomable projec lists in the newfile dialog 2026-09-03 20:12:40 +02:00
pancake abf90e6eaa Branch line arrows scale as well 2026-09-03 20:12:03 +02:00
pancake dd28d5b367 Fix build on qt <6.3 2026-09-03 19:05:24 +02:00
pancake d7fabe89f2 Try address before seeking there 2026-09-03 17:52:21 +02:00
pancake 069b64679c Use proper ellipsis in menu labels 2026-09-01 16:38:13 +02:00
pancake 7a8701f030 Proper session closing, with proper async handling 2026-09-01 16:19:30 +02:00
pancake dafb693b59 Fix Load Options close button cancellation 2026-09-01 16:10:50 +02:00
pancake 8df94a0b91 Fix redo for cached write changes 2026-09-01 16:02:25 +02:00
pancake e9c99b6da4 Fix map actions and validate map input 2026-09-01 15:29:39 +02:00
pancake 144dcce52f Fix map edits being silently discarded 2026-09-01 15:20:51 +02:00
pancake f6c5cf0cb8 Add demangler plugins tab 2026-09-01 14:35:19 +02:00
pancake c3ecfea9bb Prompt to copy a readonly theme instead of doing nothing 2026-09-01 14:31:53 +02:00
pancake 5c7dbaca9c Branch lines in disasm widget now dynamically its width 2026-09-01 14:28:38 +02:00
pancake 2982ce12f9 Reorder symbols columns, and focus functions 2026-09-01 14:07:46 +02:00
pancake eaf3b1ae36 Reorder imports columns 2026-09-01 14:06:10 +02:00
pancake c2da3b5c88 Add toggle to list all or just local platform packages 2026-09-01 14:03:16 +02:00
dependabot[bot] 815267962d Bump actions/setup-python from 6 to 7 2026-08-30 02:48:49 +02:00
Claude 60da11e8d9 Fix #311 - Scroll the disassembly by lines instead of instructions 2026-08-29 20:33:39 +02:00
pancake 244596bcb0 Fix cobalt theme colors 2026-08-29 20:20:27 +02:00
pancake 27be4123c3 Add View->FcnBound option to honor asm.lines.fcn in disasm 2026-08-29 01:53:33 +02:00
pancake 158ba57ae5 Show full r2pm error message if anything goes wrong 2026-08-29 00:52:53 +02:00
pancake bb0c8ab6d4 Fix #310 - Persist initial option dialog settings 2026-08-28 15:48:48 +02:00
pancake e35d443079 Fix #306 - bininfo respects mapaddr 2026-08-27 18:14:54 +02:00
pancake 55a401b3dc Fix build warning 2026-08-26 19:57:36 +02:00
pancake 0ecf546b80 Fix build with latest r2 2026-08-16 23:42:04 +02:00
pancake 4a14b8ddfb Add language interface dumper widget and menu entey 2026-08-11 22:42:39 +02:00
pancake 61f47fca35 Release 6.2.0 2026-08-10 13:41:39 +02:00
pancake ab9eb65ac3 Use r2 and r2g 6.2.0 in the ci 2026-08-08 12:23:26 +02:00
pancake 04ca1b2f13 Implement iaito -H to list env common vars 2026-07-28 15:28:26 +01:00
pancake 24e410c41a Fix build for r2git 2026-07-23 14:19:47 +02:00
Pau RE 9c0cf0116c Update macos-sign CI 2026-07-22 13:29:09 +02:00
pancake 3ea7715576 Support pre-110-abi resources 2026-07-15 16:43:38 +02:00
pancake 455617e037 Use the new RBin APIs in the resource widget 2026-07-15 12:40:43 +02:00
dependabot[bot] 0ff5a468e5 Bump actions/checkout from 6 to 7 2026-06-25 16:04:52 +02:00
193 changed files with 17126 additions and 1497 deletions

No files matched your search

+182 -76
View File
@@ -1,12 +1,18 @@
name: iaito CI
env:
R2V: 6.1.8
R2GHIDRA_VERSION: 6.1.8
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,14 +21,57 @@ on:
- master
jobs:
acr-linux-r2git:
runs-on: ubuntu-22.04
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@v6
- 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:
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
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with:
python-version: 3.12.x
- name: apt dependencies
@@ -35,37 +84,34 @@ 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@v6
- uses: actions/setup-python@v6
- uses: actions/checkout@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@v6
- uses: actions/checkout@v7
with:
ref: ${{ needs.source.outputs.sha }}
submodules: false # 'recursive' 'true' or 'false'
persist-credentials: false
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with:
python-version: 3.12.x
- name: apt dependencies
@@ -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,13 +138,14 @@ 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@v6
- uses: actions/checkout@v7
with:
submodules: false # 'recursive' 'true' or 'false'
persist-credentials: false
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with:
python-version: 3.12.x
- name: apt dependencies
@@ -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:
@@ -125,11 +183,12 @@ jobs:
name: acr-macos-${{ matrix.arch }}
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
ref: ${{ needs.source.outputs.sha }}
submodules: false # 'recursive' 'true' or 'false'
persist-credentials: false
- uses: actions/setup-python@v6
- uses: actions/setup-python@v7
with:
python-version: 3.12.x
- name: uname
@@ -145,10 +204,41 @@ jobs:
env:
GH_TOKEN: ${{github.token}}
run: |
curl -Lo radare2.pkg "https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-${{matrix.arch}}-${{env.R2V}}.pkg"
curl -Lo r2ai.tar.gz "$(gh api /repos/radareorg/r2ai/releases/latest --jq .tarball_url)"
curl -Lo r2ghidra.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra-${{env.R2GHIDRA_VERSION}}-macos-${{matrix.arch_alt||matrix.arch}}.zip"
curl -Lo r2ghidra_sleigh.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra_sleigh-${{env.R2GHIDRA_VERSION}}.zip"
validate_download() {
case "$2" in
tar.gz) tar -tzf "$1" >/dev/null 2>&1 ;;
zip) unzip -tq "$1" >/dev/null 2>&1 ;;
*) test -s "$1" ;;
esac
}
download() {
local output="$1"
local url="$2"
local type="$3"
local attempt
for ((attempt = 1; attempt <= 3; attempt++)); do
rm -f "${output}.tmp"
if curl --fail --location --silent --show-error \
--connect-timeout 30 --max-time 300 --output "${output}.tmp" "$url" \
&& validate_download "${output}.tmp" "$type"; then
mv "${output}.tmp" "$output"
return 0
fi
if [ "$attempt" -lt 3 ]; then
echo "Download of ${output} failed (attempt ${attempt}/3); retrying in 10s"
sleep 10
fi
done
echo "::error::Unable to download a valid ${output} after 3 attempts"
return 1
}
r2ai_version="$(gh api /repos/radareorg/r2ai/releases/latest --jq .tag_name)"
download radare2.pkg "https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-${{matrix.arch}}-${{env.R2V}}.pkg" pkg
download r2ai.tar.gz "https://github.com/radareorg/r2ai/archive/refs/tags/${r2ai_version}.tar.gz" tar.gz
download r2ghidra.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra-${{env.R2GHIDRA_VERSION}}-macos-${{matrix.arch_alt||matrix.arch}}.zip" zip
download r2ghidra_sleigh.zip "https://github.com/radareorg/r2ghidra/releases/download/${{env.R2GHIDRA_VERSION}}/r2ghidra_sleigh-${{env.R2GHIDRA_VERSION}}.zip" zip
make unpack-comps
- name: install r2
run: sudo installer -pkg dist/macos/radare2.pkg -target /
@@ -165,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@v6
- 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
- uses: actions/setup-python@v6
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
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: 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@v6
- uses: actions/checkout@v7
with:
ref: ${{ needs.source.outputs.sha }}
submodules: false
persist-credentials: false
- name: Preparing msvc toolchain
@@ -233,6 +308,8 @@ jobs:
curl -L -o r2.zip https://github.com/radareorg/radare2/releases/download/${{env.R2V}}/radare2-${{env.R2V}}-w64.zip
7z x r2.zip
mv radare2-${{env.R2V}}-w64 radare2
# XXX workaround for r2-6.2.4 headers not being msvc c++ friendly (fixed in r2 master)
sed -i 's/(RStrs) { NULL, NULL }/r_strs_new (NULL, NULL)/' radare2/include/libr/r_cmd.h
echo "%CD%\radare2\bin" >> $GITHUB_PATH
- name: configure
shell: cmd
@@ -255,33 +332,46 @@ 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@v6
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
environment: release
# Environment secrets for signing:
# APPLE_DEVELOPER_CERTIFICATE_P12_BASE64:
# Base64 of the p12 export (cert+private key), the certificate must be named
# "Developer ID Application: DEVELOPER_NAME (TEAM_ID)"
# APPLE_DEVELOPER_CERTIFICATE_PASSWORD: Random password used to encrypt the exported P12
# Environment secrets for notarization:
# APPLE_DEVELOPER_ID: email of the developer
# APPLE_TEAM_ID: Team ID (can be extracted from the certificate name too)
# APPLE_APP_SPECIFIC_PASSWORD: https://support.apple.com/en-us/102654
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: refs/tags/${{ needs.check_release.outputs.tag_name }}
- name: Download artifacts
uses: actions/download-artifact@v8
@@ -364,23 +454,34 @@ jobs:
with:
name: iaito-x64.dmg
path: iaito-x64.dmg
- name: Remove keychain
if: always()
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@v6
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:
@@ -396,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:
@@ -445,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
+2 -2
View File
@@ -13,11 +13,11 @@ 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:
image: returntocorp/semgrep
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- run: semgrep ci
+3
View File
@@ -3,6 +3,9 @@ AStyle*
./config.mk
config.mk
# Generated configuration header (qmake / Meson)
IaitoConfig.h
# OSX files
*.DS_Store
.AppleDouble
+5 -1
View File
@@ -1,4 +1,4 @@
include config.mk
-include config.mk
ifeq ($(shell uname),Darwin)
BIN=build/iaito.app/Contents/MacOS/iaito
@@ -128,7 +128,11 @@ endif
user-uninstall:
$(MAKE) uninstall DESTDIR=/ PREFIX=${HOME}/.local MANDIR=${HOME}/.local/share/man
ifneq ($(shell uname),Darwin)
run: install-desktop-user
endif
run:
rarun2 libpath=$(shell r2 -H R2_LIBDIR) program=$(BIN)
install-desktop-user:
+3
View File
@@ -83,6 +83,9 @@ make
make run
```
On macOS, `make run` launches the app from `build/iaito.app` without installing it.
Registering the `iaito://` URI handler is optional; see [deep links](doc/deeplinks.md#macos).
To install the app in your home:
```sh
Vendored
+5 -5
View File
@@ -129,12 +129,12 @@ done
: ${INSTALL_PROGRAM_STRIP:=${INSTALL} -m 755 -s}
: ${INSTALL_MAN:=${INSTALL} -m 444}
: ${INSTALL_LIB:=${INSTALL} -m 755 -c}
PKGNAME='iaito' ; VERSION='6.1.8' ; VERSION_MAJOR=6; VERSION_MINOR=1; VERSION_PATCH=8; VERSION_NUMBER=60108; 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.1.8 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.1.8"
echo "6.2.6"
exit 0
fi
echo "iaito-6.1.8 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.1.8"
echo "VERSION: 6.2.6"
echo "AUTHOR: pancake"
echo "EMAIL: [email protected]"
echo "LANGS: c++"
+1 -1
View File
@@ -1,5 +1,5 @@
PKGNAME iaito
VERSION 6.1.8
VERSION 6.2.6
CONTACT pancake ; [email protected]
LANG_CXX!
+50
View File
@@ -0,0 +1,50 @@
# Data Analysis
Open **View → Data → Analysis**. The dock is hidden in the default layout and when
first added to an older saved layout. Hidden docks do not sample data. **Zoom** is
also available under **View → Data**.
- **Algorithms** enables multiple byte metrics. Hover over a named legend entry
for its definition; click it to edit its color or visibility. Clicking the legend
does not remove the series.
- **Options → Resolution (buckets)** selects 32, 64, 128, 256, 512, 1024, 2048 or
4096 buckets. Lower values give coarser graphs and less work. The default is 1024.
- **Options** also controls lines, filled lines or vertical bars, separate tracks,
greyscale/rainbow/theme colors, custom colors, and structural markers.
- Click the graph to seek. Drag to select bytes. Wheel or `+`/`-` zooms;
Shift+wheel or the arrow keys pans. `Home` fits all data, and `Esc` clears selection.
- The bottom entropy overview shows the complete address span. Drag its window
to pan, drag its edges to resize the range, or double-click to fit all data.
- **Range** fits the current section, segment, map, block or selection.
**Edit range** accepts radare2 expressions and inclusive endpoints.
- **Follow** keeps the current seek address visible. Offset synchronization and
the global address-selection synchronization setting control integration with
disassembly and hexdump widgets.
- **Find marker** searches sections, segments, maps, flags (including search hits)
and comments. The graph context menu provides comments, flags and “Show in”.
Hover details appear after the pointer rests for 650 ms.
- Hexdump selections, sections and segments have direct analysis actions.
Other addressable widgets can use **Show in → Data Analysis**.
The graph uses the current radare2 address space: virtual addresses with `io.va`
enabled, otherwise physical offsets in the active descriptor. Gaps and failed reads
do not contribute invented bytes. Each bucket reads at most 4096 bytes from centered
windows in readable spans. Tooltips, the status line and exported CSV report how
many bytes were actually read. Zoom in for complete coverage of a region. Entropy
is measured per bucket, so very small buckets have a lower attainable entropy.
Multiple overlaid series use a normalized percentage scale. Entropy's native scale
is 0–8 bits/byte, unique bytes 0–256, and mean byte 0–255. A single series or separate
tracks uses its native scale. CSV exports normalized values plus exact bucket bounds
and coverage. Display settings are saved with the layout.
Other widgets can call `MainWindow::showDataAnalysis(start, end)` or
`DataAnalysisWidget::showRange(start, end)` with **inclusive** endpoints. Internally,
`DataAnalysis::Range` and graph signals use half-open ranges.
Run the focused tests with:
```sh
make -C tests/dataanalysis -j8 > /dev/null
QT_QPA_PLATFORM=offscreen tests/dataanalysis/build/test_dataanalysis
```
+7
View File
@@ -210,6 +210,13 @@ The app bundle declares the scheme in `Info.plist` via `CFBundleURLTypes`
the bundle is installed; `scripts/deeplink/register.sh` forces a refresh with
`lsregister -f <iaito.app>`. The handler is dropped when the bundle is removed.
`make run` launches the build without registering the URI handler. To optionally
register the local bundle without installing it, run:
```sh
sh scripts/deeplink/register.sh "$PWD/build/iaito.app"
```
### Windows
`scripts/deeplink/register.bat` writes the `HKCU\Software\Classes\iaito` keys
+30
View File
@@ -0,0 +1,30 @@
# Structures in the hexdump
Open the hexdump sidebar's **Structures** tab, choose an r2 type, enter an address
or expression, and press **Inspect**. **At cursor** uses the first selected byte
or the current cursor. You can also right-click the hexdump and choose **Inspect
structure here**; a type already linked to that address is selected automatically.
The tree shows each field's declared type, value, address, and size. Expand nested
structures, unions, arrays, and pointers to explore them. Selecting a field selects
its bytes in the hexdump. Field colors match the byte highlights. The inspected
structure stays anchored while you navigate, and **Refresh** preserves expanded
fields and the selected row while rereading memory and type definitions.
Types come from r2's type database, including imported C definitions and typedefs.
The tree uses stored member offsets and the current `asm.bits` and `cfg.bigendian`
settings. Large arrays are divided into expandable groups. Unmapped pointers and
recursive references stop expansion; invalid or overflowing layouts are reported.
Array expressions such as `uint16_t[16]` can be inspected directly.
**Pin type** creates an r2 type link (`tl`) at the inspected address. The pinned
address list also includes existing type links and command annotations (`Cr`) from
the current r2 session. Define a named typedef before pinning an array expression.
**Pin command…** stores a command at the address using the selected byte range or
the chosen type's size. Double-click a pin to navigate to its address. Select a
command pin and press **Run command** to display its output. **Unpin** removes only
the selected type link or command annotation. Pins use r2's existing project data
and are saved with the project. iaito also writes `iaito-memory-pins.json` inside the
project directory to preserve types and pins omitted by some r2 binary project versions;
keep that companion file when copying a project.
+37 -5
View File
@@ -3,8 +3,8 @@ TEMPLATE = app
TARGET = iaito
IAITO_VERSION_MAJOR = 6
IAITO_VERSION_MINOR = 1
IAITO_VERSION_PATCH = 8
IAITO_VERSION_MINOR = 2
IAITO_VERSION_PATCH = 6
CONFIG += c++20
QMAKE_CXXFLAGS += -std=c++20
@@ -164,6 +164,8 @@ IAITO_R2GHIDRA_STATIC {
QMAKE_SUBSTITUTES += IaitoConfig.h.in
SOURCES += \
common/ExceptionInfo.cpp \
widgets/ExceptionBar.cpp \
Main.cpp \
core/Iaito.cpp \
dialogs/EditStringDialog.cpp \
@@ -204,6 +206,7 @@ SOURCES += \
widgets/ExportsWidget.cpp \
widgets/FlagsWidget.cpp \
widgets/FunctionsWidget.cpp \
widgets/GlobalVariablesWidget.cpp \
widgets/ImportsWidget.cpp \
widgets/CodeEditor.cpp \
widgets/Omnibar.cpp \
@@ -230,6 +233,7 @@ SOURCES += \
common/SvgIconEngine.cpp \
common/SyntaxHighlighter.cpp \
widgets/DecompilerWidget.cpp \
widgets/DecompilerTextEdit.cpp \
widgets/VisualNavbar.cpp \
widgets/GraphView.cpp \
dialogs/settings/SettingsDialog.cpp \
@@ -243,6 +247,7 @@ SOURCES += \
widgets/VTablesWidget.cpp \
widgets/TypesWidget.cpp \
widgets/HeadersWidget.cpp \
widgets/InterfacesWidget.cpp \
widgets/SearchWidget.cpp \
IaitoApplication.cpp \
common/DeepLink.cpp \
@@ -254,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 \
@@ -313,6 +319,8 @@ SOURCES += \
common/HighDpiPixmap.cpp \
widgets/GraphGridLayout.cpp \
widgets/HexWidget.cpp \
widgets/HexPatWidget.cpp \
widgets/AddressScrollBar.cpp \
widgets/R2AIWidget.cpp \
widgets/R2McpWidget.cpp \
common/SelectionHighlight.cpp \
@@ -331,6 +339,8 @@ SOURCES += \
common/R2retdecDecompiler.cpp \
menus/AddressableItemContextMenu.cpp \
common/AddressableItemModel.cpp \
common/NameListModel.cpp \
widgets/NameListView.cpp \
widgets/ListDockWidget.cpp \
dialogs/MultitypeFileSaveDialog.cpp \
widgets/BoolToggleDelegate.cpp \
@@ -353,12 +363,18 @@ SOURCES += \
dialogs/AssemblerDialog.cpp \
dialogs/ScriptManagerDialog.cpp \
dialogs/ScriptManagerWidget.cpp \
widgets/ZoomWidget.cpp
widgets/ZoomWidget.cpp \
common/DataAnalysis.cpp \
common/TypeLayout.cpp \
widgets/DataAnalysisView.cpp \
widgets/DataAnalysisWidget.cpp
GRAPHVIZ_SOURCES = \
widgets/GraphvizLayout.cpp
HEADERS += \
common/ExceptionInfo.h \
widgets/ExceptionBar.h \
common/R2Shims.h \
core/Iaito.h \
core/IaitoCommon.h \
@@ -406,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 \
@@ -419,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 \
@@ -432,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 \
@@ -448,6 +467,7 @@ HEADERS += \
widgets/VTablesWidget.h \
widgets/TypesWidget.h \
widgets/HeadersWidget.h \
widgets/InterfacesWidget.h \
widgets/SearchWidget.h \
dialogs/R2PluginsDialog.h \
widgets/IaitoDockWidget.h \
@@ -456,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 \
@@ -523,6 +544,9 @@ HEADERS += \
widgets/GraphLayout.h \
widgets/GraphGridLayout.h \
widgets/HexWidget.h \
widgets/HexPatWidget.h \
common/SparseHexLayout.h \
widgets/AddressScrollBar.h \
widgets/R2AIWidget.h \
widgets/R2McpWidget.h \
common/SelectionHighlight.h \
@@ -538,6 +562,8 @@ HEADERS += \
common/R2retdecDecompiler.h \
menus/AddressableItemContextMenu.h \
common/AddressableItemModel.h \
common/NameListModel.h \
widgets/NameListView.h \
widgets/ListDockWidget.h \
widgets/AddressableItemList.h \
dialogs/MultitypeFileSaveDialog.h \
@@ -563,11 +589,17 @@ HEADERS += \
dialogs/AssemblerDialog.h \
dialogs/ScriptManagerDialog.h \
dialogs/ScriptManagerWidget.h \
widgets/ZoomWidget.h
widgets/ZoomWidget.h \
common/DataAnalysis.h \
common/TypeLayout.h \
widgets/DataAnalysisView.h \
widgets/DataAnalysisWidget.h
GRAPHVIZ_HEADERS = widgets/GraphvizLayout.h
FORMS += \
widgets/GlobalVariablesWidget.ui \
widgets/DataAnalysisWidget.ui \
dialogs/AboutDialog.ui \
dialogs/EditStringDialog.ui \
dialogs/Base64EnDecodedWriteDialog.ui \
@@ -591,6 +623,7 @@ FORMS += \
widgets/FlagsWidget.ui \
widgets/StringsWidget.ui \
widgets/HexdumpWidget.ui \
widgets/HexPatWidget.ui \
dialogs/SaveProjectDialog.ui \
dialogs/settings/SettingsDialog.ui \
dialogs/settings/AppearanceOptionsWidget.ui \
@@ -623,7 +656,6 @@ FORMS += \
dialogs/SetToDataDialog.ui \
dialogs/EditVariablesDialog.ui \
widgets/IaitoTreeView.ui \
widgets/ComboQuickFilterView.ui \
dialogs/HexdumpRangeDialog.ui \
dialogs/WelcomeDialog.ui \
dialogs/EditMethodDialog.ui \
+31 -6
View File
@@ -172,8 +172,7 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
#endif
#endif
if (!parseCommandLineOptions()) {
QCoreApplication::exit();
// std::exit(1);
return;
}
if (!versionCheck()) {
@@ -338,9 +337,11 @@ IaitoApplication::IaitoApplication(int &argc, char **argv)
IaitoApplication::~IaitoApplication()
{
SamplesDB::waitForPending();
Plugins()->destroyPlugins();
delete mainWindow;
if (mainWindow) {
SamplesDB::waitForPending();
Plugins()->destroyPlugins();
delete mainWindow;
}
}
void IaitoApplication::launchNewInstance(const QStringList &args)
@@ -465,6 +466,10 @@ bool IaitoApplication::parseCommandLineOptions()
cmd_parser.addVersionOption();
cmd_parser.addPositionalArgument("filename", QObject::tr("Filename to open."));
QCommandLineOption environmentOption(
"H", QObject::tr("List environment variables, or print the value of a selected variable."));
cmd_parser.addOption(environmentOption);
QCommandLineOption analOption(
{"A", "analysis"},
QObject::tr(
@@ -486,6 +491,10 @@ bool IaitoApplication::parseCommandLineOptions()
QObject::tr("base address"));
cmd_parser.addOption(baddrOption);
QCommandLineOption mapaddrOption(
{"m", "map"}, QObject::tr("Map the file at a specific address"), QObject::tr("map address"));
cmd_parser.addOption(mapaddrOption);
QCommandLineOption scriptOption("i", QObject::tr("Run script file"), QObject::tr("file"));
cmd_parser.addOption(scriptOption);
@@ -527,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();
@@ -587,6 +605,13 @@ bool IaitoApplication::parseCommandLineOptions()
opts.fileOpenOptions.binLoadAddr = baddr;
}
}
if (cmd_parser.isSet(mapaddrOption)) {
bool ok;
RVA mapaddr = cmd_parser.value(mapaddrOption).toULongLong(&ok, 0);
if (ok) {
opts.fileOpenOptions.mapAddr = mapaddr;
}
}
switch (opts.analLevel) {
case AutomaticAnalysisLevel::Ask:
break;
+3 -1
View File
@@ -30,6 +30,7 @@ public:
~IaitoApplication();
MainWindow *getMainWindow() { return mainWindow; }
int getExitCode() const { return exitCode; }
void launchNewInstance(const QStringList &args = {});
@@ -51,7 +52,8 @@ private:
private:
bool m_FileAlreadyDropped;
MainWindow *mainWindow;
MainWindow *mainWindow = nullptr;
int exitCode = 1;
IaitoCommandLineOptions clOptions;
};
+73
View File
@@ -5,9 +5,75 @@
#include "core/MainWindow.h"
#include <iostream>
#include <QDir>
#include <QJsonArray>
#include <QJsonObject>
#include <cstdio>
#include <cstring>
#ifdef Q_OS_WIN
static void connectToConsole();
#endif
static bool printEnvironmentVariables(int argc, char *argv[])
{
for (int i = 1; i < argc; i++) {
if (!std::strcmp(argv[i], "--")) {
return false;
}
if (std::strncmp(argv[i], "-H", 2)) {
continue;
}
const char *selectedVariable = nullptr;
if (argv[i][2]) {
selectedVariable = argv[i] + 2;
} else if (i + 1 < argc && argv[i + 1][0] != '-') {
selectedVariable = argv[i + 1];
}
#ifdef Q_OS_WIN
connectToConsole();
#endif
QByteArray extraPluginDirs = IAITO_EXTRA_PLUGIN_DIRS;
const QByteArray environmentPluginDirs = qgetenv("IAITO_EXTRA_PLUGIN_DIRS");
if (!environmentPluginDirs.isEmpty()) {
if (!extraPluginDirs.isEmpty()) {
extraPluginDirs.append(QDir::listSeparator().toLatin1());
}
extraPluginDirs.append(environmentPluginDirs);
}
struct EnvironmentVariable
{
const char *name;
QByteArray value;
};
const EnvironmentVariable variables[] = {
{"IAITO_VERSION", IAITO_VERSION_FULL},
{"IAITO_VERSION_MAJOR", QByteArray::number(IAITO_VERSION_MAJOR)},
{"IAITO_VERSION_MINOR", QByteArray::number(IAITO_VERSION_MINOR)},
{"IAITO_VERSION_PATCH", QByteArray::number(IAITO_VERSION_PATCH)},
{"IAITO_EXTRA_PLUGIN_DIRS", extraPluginDirs},
{"R2_DEBUG", qgetenv("R2_DEBUG")},
{"R2_NOPLUGINS", qgetenv("R2_NOPLUGINS")},
};
for (const auto &variable : variables) {
const char *shortName = std::strchr(variable.name, '_');
shortName = shortName ? shortName + 1 : variable.name;
if (selectedVariable && std::strcmp(selectedVariable, variable.name)
&& std::strcmp(selectedVariable, shortName)) {
continue;
}
if (selectedVariable) {
std::fprintf(stdout, "%s\n", variable.value.constData());
} else {
std::fprintf(stdout, "%s=%s\n", variable.name, variable.value.constData());
}
}
return true;
}
return false;
}
/**
* @brief Attempt to connect to a parent console and configure outputs.
*
@@ -52,6 +118,10 @@ static void connectToConsole()
int main(int argc, char *argv[])
{
if (printEnvironmentVariables(argc, argv)) {
return 0;
}
if (argc >= 3 && QString::fromLocal8Bit(argv[1]) == "--start-crash-handler") {
QApplication app(argc, argv);
QString dumpLocation = QString::fromLocal8Bit(argv[2]);
@@ -84,6 +154,9 @@ int main(int argc, char *argv[])
#endif
IaitoApplication a(argc, argv);
if (!a.getMainWindow()) {
return a.getExitCode();
}
return a.exec();
}
+46 -4
View File
@@ -12,17 +12,20 @@ AddressableFilterProxyModel::AddressableFilterProxyModel(
: AddressableItemModel<QSortFilterProxyModel>(parent)
{
setSourceModel(sourceModel);
addressableSourceModel = sourceModel;
}
RVA AddressableFilterProxyModel::address(const QModelIndex &index) const
{
return addressableSourceModel->address(this->mapToSource(index));
const auto source = mapToSource(index);
return addressableSourceModel && source.isValid() ? addressableSourceModel->address(source)
: RVA_INVALID;
}
QString AddressableFilterProxyModel::name(const QModelIndex &index) const
{
return addressableSourceModel->name(this->mapToSource(index));
const auto source = mapToSource(index);
return addressableSourceModel && source.isValid() ? addressableSourceModel->name(source)
: QString();
}
void AddressableFilterProxyModel::setSourceModel(QAbstractItemModel *)
@@ -32,6 +35,45 @@ void AddressableFilterProxyModel::setSourceModel(QAbstractItemModel *)
void AddressableFilterProxyModel::setSourceModel(AddressableItemModelI *sourceModel)
{
ParentClass::setSourceModel(sourceModel->asItemModel());
addressableSourceModel = sourceModel;
ParentClass::setSourceModel(sourceModel ? sourceModel->asItemModel() : nullptr);
}
void AddressableFilterProxyModel::setTypeFilterColumn(int column)
{
if (typeColumn != column) {
emit filterAboutToChange();
typeColumn = column;
invalidateFilter();
emit filterChanged();
}
}
void AddressableFilterProxyModel::setTypeFilter(const QVariant &type)
{
if (selectedType != type) {
emit filterAboutToChange();
selectedType = type;
invalidateFilter();
emit filterChanged();
}
}
void AddressableFilterProxyModel::setFilterWildcard(const QString &pattern)
{
emit filterAboutToChange();
ParentClass::setFilterWildcard(pattern);
emit filterChanged();
}
bool AddressableFilterProxyModel::filterAcceptsType(int row, const QModelIndex &parent) const
{
return typeColumn < 0 || !selectedType.isValid()
|| sourceModel()->index(row, typeColumn, parent).data().toString()
== selectedType.toString();
}
bool AddressableFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
return filterAcceptsType(row, parent) && ParentClass::filterAcceptsRow(row, parent);
}
+19 -1
View File
@@ -43,6 +43,8 @@ public:
class IAITO_EXPORT AddressableFilterProxyModel : public AddressableItemModel<QSortFilterProxyModel>
{
Q_OBJECT
using ParentClass = AddressableItemModel<QSortFilterProxyModel>;
public:
@@ -51,10 +53,26 @@ public:
RVA address(const QModelIndex &index) const override;
QString name(const QModelIndex &) const override;
void setSourceModel(AddressableItemModelI *sourceModel);
void setTypeFilterColumn(int column);
void setTypeFilter(const QVariant &type);
QVariant typeFilter() const { return selectedType; }
public slots:
void setFilterWildcard(const QString &pattern);
signals:
void filterAboutToChange();
void filterChanged();
protected:
bool filterAcceptsRow(int row, const QModelIndex &parent) const override;
bool filterAcceptsType(int row, const QModelIndex &parent) const;
private:
void setSourceModel(QAbstractItemModel *sourceModel) override; // Don't use this directly
AddressableItemModelI *addressableSourceModel;
AddressableItemModelI *addressableSourceModel = nullptr;
int typeColumn = -1;
QVariant selectedType;
};
#endif // ADDRESSABLEITEMMODEL_H
+80 -3
View File
@@ -47,7 +47,11 @@ static inline int variantTypeId(const QVariant &v)
constexpr int VisualNavbarThicknessDefault = 15;
constexpr int VisualNavbarThicknessMin = 8;
constexpr int VisualNavbarThicknessMax = 128;
constexpr int VisualNavbarResolutionMax = 32;
constexpr const char *AddressRangeSelectionSyncKey = "syncAddressRangeSelection";
constexpr const char *DisassemblyJumpLineAdaptiveSpacingKey = "disassemblyJumpLineAdaptiveSpacing";
constexpr const char *DisassemblyJumpLineSpacingKey = "disassemblyJumpLineSpacing";
constexpr int DisassemblyJumpLineSpacingDefault = 20;
} // namespace
/* Map with names of themes associated with its color palette
@@ -585,13 +589,13 @@ const QFont Configuration::getFont() const
return font;
}
const QFont Configuration::getSmallFont() const
const QFont Configuration::getSmallFont(bool zoomed) const
{
if (IaitoStyle::instance() && IaitoStyle::instance()->theme().fonts.custom) {
return getFont();
return zoomed ? getFont() : getBaseFont();
}
QFont font = getBaseFont();
qreal eff = getZoomFactor() * 0.85;
qreal eff = (zoomed ? getZoomFactor() : 1.0) * 0.85;
if (font.pixelSize() > 0) {
font.setPixelSize(qMax(int(font.pixelSize() * eff), 8));
} else {
@@ -665,6 +669,45 @@ void Configuration::setVisualNavbarThickness(int thickness)
emit visualNavbarThicknessChanged(thickness);
}
int Configuration::getVisualNavbarResolution() const
{
return qBound(1, s.value("visualNavbarResolution", 1).toInt(), VisualNavbarResolutionMax);
}
void Configuration::setVisualNavbarResolution(int divisor)
{
divisor = qBound(1, divisor, VisualNavbarResolutionMax);
if (getVisualNavbarResolution() == divisor) {
return;
}
s.setValue("visualNavbarResolution", divisor);
emit visualNavbarResolutionChanged(divisor);
}
Configuration::MemoryScrollBarMode Configuration::getMemoryScrollBarMode() const
{
const int mode
= s.value("memoryScrollBarMode", static_cast<int>(MemoryScrollBarMode::Bounded)).toInt();
switch (mode) {
case static_cast<int>(MemoryScrollBarMode::Spring):
return MemoryScrollBarMode::Spring;
case static_cast<int>(MemoryScrollBarMode::Hidden):
return MemoryScrollBarMode::Hidden;
case static_cast<int>(MemoryScrollBarMode::Bounded):
default:
return MemoryScrollBarMode::Bounded;
}
}
void Configuration::setMemoryScrollBarMode(MemoryScrollBarMode mode)
{
if (getMemoryScrollBarMode() == mode) {
return;
}
s.setValue("memoryScrollBarMode", static_cast<int>(mode));
emit memoryScrollBarModeChanged(mode);
}
QString Configuration::getLastThemeOf(const IaitoInterfaceTheme &currInterfaceTheme) const
{
return s.value("lastThemeOf." + currInterfaceTheme.name, Config()->getColorTheme()).toString();
@@ -878,6 +921,30 @@ void Configuration::resetToDefaultAsmOptions()
for (auto it = asmOptions.cbegin(); it != asmOptions.cend(); it++) {
setConfig(it.key(), it.value());
}
setDisassemblyJumpLineAdaptiveSpacingEnabled(true);
setDisassemblyJumpLineSpacing(DisassemblyJumpLineSpacingDefault);
}
bool Configuration::getDisassemblyJumpLineAdaptiveSpacingEnabled() const
{
return s.value(DisassemblyJumpLineAdaptiveSpacingKey, true).toBool();
}
void Configuration::setDisassemblyJumpLineAdaptiveSpacingEnabled(bool enabled)
{
s.setValue(DisassemblyJumpLineAdaptiveSpacingKey, enabled);
}
int Configuration::getDisassemblyJumpLineSpacing() const
{
const int spacing
= s.value(DisassemblyJumpLineSpacingKey, DisassemblyJumpLineSpacingDefault).toInt();
return spacing > 0 ? spacing : DisassemblyJumpLineSpacingDefault;
}
void Configuration::setDisassemblyJumpLineSpacing(int spacing)
{
s.setValue(DisassemblyJumpLineSpacingKey, qMax(1, spacing));
}
void Configuration::applySavedAsmOptions()
@@ -1097,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>();
+16 -1
View File
@@ -36,6 +36,8 @@ class IAITO_EXPORT Configuration : public QObject
public:
enum class VisualNavbarLocation { Top = 0, Bottom, Left, Right, SuperTop, SuperBottom };
Q_ENUM(VisualNavbarLocation)
enum class MemoryScrollBarMode { Spring = 0, Bounded, Hidden };
Q_ENUM(MemoryScrollBarMode)
private:
QPalette nativePalette;
@@ -118,8 +120,9 @@ public:
* @brief Gets a smaller variant of the configured font, suitable for
* list/tree views. Uses 85% of the normal font size.
* @return the configured font at reduced size, adjusted by zoom level
* unless zoomed is false
*/
const QFont getSmallFont() const;
const QFont getSmallFont(bool zoomed = true) const;
void setFont(const QFont &font);
qreal getZoomFactor() const;
void setZoomFactor(qreal zoom);
@@ -127,6 +130,10 @@ public:
void setVisualNavbarLocation(VisualNavbarLocation location);
int getVisualNavbarThickness() const;
void setVisualNavbarThickness(int thickness);
int getVisualNavbarResolution() const;
void setVisualNavbarResolution(int divisor);
MemoryScrollBarMode getMemoryScrollBarMode() const;
void setMemoryScrollBarMode(MemoryScrollBarMode mode);
// Colors
bool windowColorIsDark();
@@ -167,6 +174,10 @@ public:
// Asm Options
void resetToDefaultAsmOptions();
bool getDisassemblyJumpLineAdaptiveSpacingEnabled() const;
void setDisassemblyJumpLineAdaptiveSpacingEnabled(bool enabled);
int getDisassemblyJumpLineSpacing() const;
void setDisassemblyJumpLineSpacing(int spacing);
QString getColorTheme() const { return s.value("theme", "iaito").toString(); }
void setColorTheme(const QString &theme);
@@ -225,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);
@@ -286,6 +299,8 @@ signals:
void interfaceThemeChanged();
void visualNavbarLocationChanged(VisualNavbarLocation location);
void visualNavbarThicknessChanged(int thickness);
void visualNavbarResolutionChanged(int divisor);
void memoryScrollBarModeChanged(MemoryScrollBarMode mode);
void addressRangeSelectionSyncEnabledChanged(bool enabled);
#ifdef IAITO_ENABLE_KSYNTAXHIGHLIGHTING
void kSyntaxHighlightingThemeChanged();
+122
View File
@@ -0,0 +1,122 @@
#include "DataAnalysis.h"
#include <algorithm>
#include <cmath>
namespace DataAnalysis {
Range sizedRange(RVA start, RVA size)
{
return {start, start + qMin(size, RVA_MAX - start)};
}
Range intersect(Range a, Range b)
{
const RVA start = qMax(a.start, b.start);
return {start, qMax(start, qMin(a.end, b.end))};
}
Range bucket(Range range, int index, int count)
{
if (!range.valid() || count <= 0 || index < 0 || index >= count) {
return {};
}
// Avoid both floating point address rounding and multiplication overflow.
auto boundary = [range, count](RVA i) {
return range.start + (range.size() / count) * i + (range.size() % count) * i / count;
};
return {boundary(index), boundary(index + 1)};
}
Range centered(Range domain, RVA address, RVA size)
{
if (!domain.valid()) {
return {};
}
size = qBound<RVA>(1, size, domain.size());
address = qBound(domain.start, address, domain.end - 1);
const RVA start = address - qMin(address - domain.start, size / 2);
const RVA clamped = qMin(start, domain.end - size);
return {clamped, clamped + size};
}
QVector<Range> mergeRanges(QVector<Range> ranges)
{
std::sort(ranges.begin(), ranges.end(), [](Range a, Range b) { return a.start < b.start; });
QVector<Range> result;
for (auto range : ranges) {
if (!range.valid()) {
continue;
}
if (result.isEmpty() || result.last().end < range.start) {
result.append(range);
} else {
result.last().end = qMax(result.last().end, range.end);
}
}
return result;
}
Sample sample(
Range range,
const QVector<Range> &readable,
const std::function<QByteArray(RVA, int)> &read,
int budget)
{
Sample result;
result.range = range;
if (!range.valid() || budget <= 0) {
return result;
}
QVector<Range> spans;
for (auto candidate : readable) {
const Range span = intersect(range, candidate);
if (span.valid()) {
spans.append(span);
result.covered += span.size();
}
}
std::array<int, 256> histogram{};
RVA remaining = result.covered;
for (auto span : spans) {
if (budget <= 0) {
break;
}
// Allocate the budget proportionally among disjoint readable spans.
const int share = qMax(1, int((long double) span.size() / remaining * budget));
const int length = int(qMin<RVA>(span.size(), qMin(budget, share)));
const RVA address = span.start + (span.size() - length) / 2;
const QByteArray data = read(address, length).left(length);
for (unsigned char byte : data) {
++histogram[byte];
}
result.bytes += data.size();
budget -= length;
remaining -= span.size();
}
if (!result.bytes) {
return result;
}
auto &v = result.values;
for (int byte = 0; byte < 256; ++byte) {
const int n = histogram[byte];
if (!n) {
continue;
}
const double p = double(n) / result.bytes;
v[Entropy] -= p * std::log2(p) / 8.0;
v[Printable] += (byte >= 32 && byte <= 126) ? p : 0;
v[Zero] += byte == 0 ? p : 0;
v[FF] += byte == 255 ? p : 0;
v[Unique] += 1.0 / 256;
v[Mean] += p * byte / 255;
v[Alpha] += ((byte >= 'a' && byte <= 'z') || (byte >= 'A' && byte <= 'Z')) ? p : 0;
v[Digit] += (byte >= '0' && byte <= '9') ? p : 0;
v[Space] += (byte == ' ' || (byte >= 9 && byte <= 13)) ? p : 0;
v[Control] += (byte < 32 || byte == 127) ? p : 0;
v[High] += byte >= 128 ? p : 0;
}
return result;
}
} // namespace DataAnalysis
+64
View File
@@ -0,0 +1,64 @@
#ifndef DATAANALYSIS_H
#define DATAANALYSIS_H
#include "core/IaitoCommon.h"
#include <array>
#include <functional>
#include <QByteArray>
#include <QVector>
namespace DataAnalysis {
// Half-open ranges, including when the exclusive end is RVA_MAX.
struct Range
{
RVA start = 0;
RVA end = 0;
bool valid() const { return start < end; }
RVA size() const { return end - start; }
bool contains(RVA address) const { return start <= address && address < end; }
bool operator==(const Range &other) const { return start == other.start && end == other.end; }
bool operator!=(const Range &other) const { return !(*this == other); }
};
enum Metric {
Entropy,
Printable,
Zero,
FF,
Unique,
Mean,
Alpha,
Digit,
Space,
Control,
High,
Count
};
struct Sample
{
Range range;
RVA covered = 0;
int bytes = 0;
std::array<double, Count> values{}; // Normalized to [0, 1]; entropy / 8.
};
Range sizedRange(RVA start, RVA size);
Range intersect(Range a, Range b);
Range bucket(Range range, int index, int count);
Range centered(Range domain, RVA address, RVA size);
QVector<Range> mergeRanges(QVector<Range> ranges);
// Bounded, deterministic sampling. Reads only known readable spans; missing bytes
// never contribute fabricated 0xff data. Large buckets use centered windows.
Sample sample(
Range range,
const QVector<Range> &readable,
const std::function<QByteArray(RVA, int)> &read,
int budget = 4096);
} // namespace DataAnalysis
#endif
+1 -1
View File
@@ -337,7 +337,7 @@ QString linkAt(const QString &line, int posInLine)
auto it = re.globalMatch(line);
while (it.hasNext()) {
const QRegularExpressionMatch m = it.next();
if (posInLine >= m.capturedStart() && posInLine <= m.capturedEnd()) {
if (posInLine >= m.capturedStart() && posInLine < m.capturedEnd()) {
return m.captured();
}
}
+124
View File
@@ -0,0 +1,124 @@
#include "ExceptionInfo.h"
#include <cmath>
#include <QCoreApplication>
#include <QJsonObject>
#include <QVariant>
static QString exceptionTr(const char *text)
{
return QCoreApplication::translate("ExceptionInfo", text);
}
static quint64 exceptionAddress(const QJsonValue &value)
{
bool ok = false;
quint64 result = ExceptionRegion::Invalid;
if (value.isString()) {
result = value.toString().toULongLong(&ok, 0);
} else if (
value.isDouble() && value.toDouble() >= 0
&& std::floor(value.toDouble()) == value.toDouble()) {
result = value.toVariant().toULongLong(&ok);
}
return ok ? result : ExceptionRegion::Invalid;
}
bool ExceptionRegion::protects(quint64 address, quint64 size) const
{
// Subtraction avoids overflowing a block's end near the address-space limit.
return size && address < to && (address >= from || from - address < size);
}
bool ExceptionRegion::touches(quint64 address, quint64 size) const
{
return protects(address, size)
|| (handler != Invalid && handler >= address && handler - address < size);
}
QString ExceptionRegion::label() const
{
if (kind == QStringLiteral("cleanup")) {
return exceptionTr("Cleanup / finally");
}
const QString name = kind == QStringLiteral("filter") ? exceptionTr("Filter")
: exceptionTr("Catch");
return QStringLiteral("%1 (%2)").arg(
name,
catchAll ? exceptionTr("all exceptions")
: type.isEmpty() ? exceptionTr("unknown type")
: type);
}
QString ExceptionRegion::description() const
{
QString text = exceptionTr("#%1 · %2\nProtected: [0x%3, 0x%4)\nHandler: 0x%5")
.arg(index)
.arg(label())
.arg(from, 0, 16)
.arg(to, 0, 16)
.arg(handler, 0, 16);
if (source != Invalid) {
text += exceptionTr("\nMetadata source: 0x%1").arg(source, 0, 16);
}
if (filter && filter != Invalid) {
text += exceptionTr("\nFilter value / address: 0x%1").arg(filter, 0, 16);
}
if (typeFilter) {
text += exceptionTr("\nType filter: %1").arg(typeFilter);
}
return text;
}
QColor ExceptionRegion::color() const
{
// Stable across views and independent of metadata order; shared ranges share a color.
const quint64 hash = (from >> 2) ^ (to >> 1) ^ from;
return QColor::fromHsv(int(hash % 12) * 30, 145, 210);
}
QList<ExceptionRegion> ExceptionRegion::parse(const QJsonArray &array)
{
QList<ExceptionRegion> result;
for (int i = 0; i < array.size(); ++i) {
const auto object = array.at(i).toObject();
ExceptionRegion region;
region.from = exceptionAddress(object.value(QStringLiteral("from")));
region.to = exceptionAddress(object.value(QStringLiteral("to")));
region.handler = exceptionAddress(object.value(QStringLiteral("handler")));
if (region.from == Invalid || region.to == Invalid || region.from >= region.to
|| region.handler == Invalid) {
continue;
}
region.index = exceptionAddress(object.value(QStringLiteral("index")));
if (region.index == Invalid) {
region.index = quint64(i);
}
region.source = exceptionAddress(object.value(QStringLiteral("source")));
region.filter = exceptionAddress(object.value(QStringLiteral("filter")));
region.kind = object.value(QStringLiteral("kind")).toString();
region.type = object.value(QStringLiteral("type")).toString();
region.typeFilter = object.value(QStringLiteral("typeFilter")).toVariant().toLongLong();
region.catchAll = object.value(QStringLiteral("catchAll")).toBool();
result.append(region);
}
return result;
}
QList<ExceptionRegion> ExceptionRegion::forBlocks(
const QList<ExceptionRegion> &regions, const QJsonArray &blocks)
{
QList<ExceptionRegion> result;
for (const auto &region : regions) {
for (const auto &value : blocks) {
const auto block = value.toObject();
const auto address = exceptionAddress(block.value(QStringLiteral("addr")));
const auto size = exceptionAddress(block.value(QStringLiteral("size")));
if (address != Invalid && size != Invalid && region.touches(address, size)) {
result.append(region);
break;
}
}
}
return result;
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef EXCEPTIONINFO_H
#define EXCEPTIONINFO_H
#include <limits>
#include <QColor>
#include <QJsonArray>
#include <QList>
#include <QString>
// Addresses in iwj use half-open ranges: [from, to).
struct ExceptionRegion
{
static constexpr quint64 Invalid = std::numeric_limits<quint64>::max();
quint64 index = 0;
quint64 source = Invalid;
quint64 from = Invalid;
quint64 to = Invalid;
quint64 handler = Invalid;
quint64 filter = 0;
QString kind;
QString type;
qint64 typeFilter = 0;
bool catchAll = false;
bool contains(quint64 address) const { return from <= address && address < to; }
bool protects(quint64 address, quint64 size) const;
bool touches(quint64 address, quint64 size) const;
QString label() const;
QString description() const;
QColor color() const;
static QList<ExceptionRegion> parse(const QJsonArray &array);
static QList<ExceptionRegion> forBlocks(
const QList<ExceptionRegion> &regions, const QJsonArray &blocks);
};
#endif
+23
View File
@@ -4,6 +4,7 @@
#include <cmath>
#include <QAbstractButton>
#include <QAbstractItemView>
#include <QApplication>
#include <QComboBox>
#include <QCompleter>
#include <QCryptographicHash>
@@ -151,6 +152,28 @@ void bindFont(QWidget *view, bool smallFont, bool monospace)
QObject::connect(Config(), &Configuration::fontsUpdated, view, apply);
}
// Monospace code font scaled to the size of the regular UI font
QFont logFont()
{
QFont f = Config()->getBaseFont();
const QFont ui = QApplication::font();
if (ui.pixelSize() > 0) {
f.setPixelSize(ui.pixelSize());
} else if (ui.pointSizeF() > 0) {
f.setPointSizeF(ui.pointSizeF());
}
f.setStyleHint(QFont::Monospace);
return f;
}
void bindLogFont(QWidget *view)
{
view->setFont(logFont());
QObject::connect(Config(), &Configuration::fontsUpdated, view, [view]() {
view->setFont(logFont());
});
}
SizePolicyMinMax forceWidth(QWidget *widget, int width)
{
SizePolicyMinMax r;
+2
View File
@@ -67,6 +67,8 @@ IAITO_EXPORT void setVerticalScrollMode(QAbstractItemView *tw);
IAITO_EXPORT void setCheckedWithoutSignals(QAbstractButton *button, bool checked);
IAITO_EXPORT void bindFont(QWidget *view, bool smallFont = true, bool monospace = false);
IAITO_EXPORT QFont logFont();
IAITO_EXPORT void bindLogFont(QWidget *view);
struct IAITO_EXPORT SizePolicyMinMax
{
+74 -13
View File
@@ -1,11 +1,9 @@
#include "IaitoSeekable.h"
#include "core/MainWindow.h"
#include <QPlainTextEdit>
IaitoSeekable::IaitoSeekable(QObject *parent)
: QObject(parent)
{
widgetOffset = Core()->getOffset();
connect(Core(), &IaitoCore::seekChanged, this, &IaitoSeekable::onCoreSeekChanged);
}
@@ -13,35 +11,98 @@ IaitoSeekable::~IaitoSeekable() {}
void IaitoSeekable::setSynchronization(bool sync)
{
if (synchronized == sync) {
return;
}
const RVA address = Core()->getOffset();
synchronized = sync;
onCoreSeekChanged(Core()->getOffset());
seekHistory = {address};
historyIndex = 0;
updateSeek(address);
emit syncChanged();
emit historyChanged();
}
void IaitoSeekable::onCoreSeekChanged(RVA addr)
{
if (synchronized && widgetOffset != addr) {
updateSeek(addr, true);
if (synchronized) {
updateSeek(addr);
emit historyChanged();
}
}
void IaitoSeekable::updateSeek(RVA addr, bool localOnly)
void IaitoSeekable::seek(RVA addr)
{
previousOffset = widgetOffset;
widgetOffset = addr;
if (synchronized && !localOnly) {
Core()->seek(addr);
if (addr == RVA_INVALID) {
return;
}
if (synchronized) {
Core()->seek(addr);
return;
}
if (addr == widgetOffset) {
return;
}
seekHistory.resize(historyIndex + 1);
seekHistory.append(addr);
++historyIndex;
updateSeek(addr);
emit historyChanged();
}
void IaitoSeekable::updateSeek(RVA addr)
{
if (addr == RVA_INVALID || addr == widgetOffset) {
return;
}
widgetOffset = addr;
emit seekableSeekChanged(addr);
}
bool IaitoSeekable::canSeekPrev() const
{
if (synchronized) {
return Core()->core()->io->undo.undos > 0;
}
return historyIndex > 0;
}
bool IaitoSeekable::canSeekNext() const
{
if (synchronized) {
return Core()->core()->io->undo.redos > 0;
}
return historyIndex + 1 < seekHistory.size();
}
QVector<RVA> IaitoSeekable::localHistory(bool forward) const
{
QVector<RVA> result;
if (!synchronized) {
for (int i = historyIndex; i >= 0 && i < seekHistory.size(); i += forward ? 1 : -1) {
result.append(seekHistory.at(i));
}
}
return result;
}
void IaitoSeekable::seekPrev()
{
if (synchronized) {
Core()->seekPrev();
} else {
this->seek(previousOffset);
} else if (canSeekPrev()) {
updateSeek(seekHistory.at(--historyIndex));
emit historyChanged();
}
}
void IaitoSeekable::seekNext()
{
if (synchronized) {
Core()->seekNext();
} else if (canSeekNext()) {
updateSeek(seekHistory.at(++historyIndex));
emit historyChanged();
}
}
+11 -9
View File
@@ -1,6 +1,7 @@
#pragma once
#include "core/Iaito.h"
#include <QVector>
class MainWindow;
@@ -20,7 +21,7 @@ public:
* In any case, IaitoSeekable::seekableSeekChanged is emitted.
* @param addr the location to seek at.
*/
void seek(RVA addr) { updateSeek(addr, false); }
void seek(RVA addr);
/**
* @brief setSynchronization sets
@@ -44,6 +45,10 @@ public:
*/
bool isSynchronized();
bool canSeekPrev() const;
bool canSeekNext() const;
QVector<RVA> localHistory(bool forward) const;
/**
* @brief seekToReference will seek to the function or the object which is
* referenced in a given offset
@@ -56,6 +61,7 @@ public slots:
* @brief seekPrev seeks to last location.
*/
void seekPrev();
void seekNext();
/**
* @brief toggleSyncWithCore toggles
@@ -75,11 +81,8 @@ private:
*/
RVA widgetOffset = RVA_INVALID;
/**
* @brief previousOffset last seek location.
* @todo maybe use an actual history?
*/
RVA previousOffset = RVA_INVALID;
QVector<RVA> seekHistory;
int historyIndex = 0;
/**
* @brief synchronized tells with the seekable's offset is
@@ -89,12 +92,11 @@ private:
/**
* @brief internal method for changing the seek
* @param localOnly whether the seek should be updated globally if
* synchronized
*/
void updateSeek(RVA addr, bool localOnly);
void updateSeek(RVA addr);
signals:
void seekableSeekChanged(RVA addr);
void syncChanged();
void historyChanged();
};
+436
View File
@@ -0,0 +1,436 @@
#include "NameListModel.h"
#include <QStringList>
namespace {
QString compactName(const QString &name, const QString &previous)
{
int prefix = 0;
const int limit = qMin(name.size(), previous.size());
while (prefix < limit && name.at(prefix) == previous.at(prefix)) {
++prefix;
}
if (prefix > 0 && prefix < name.size() && name.at(prefix).isLowSurrogate()) {
--prefix;
}
return prefix > 2 && prefix < name.size() ? QStringLiteral("▶ ") + name.mid(prefix) : name;
}
QStringList nameParts(const QString &name)
{
QStringList parts;
int start = 0;
int templates = 0;
int parentheses = 0;
int brackets = 0;
for (int i = 0; i < name.size(); ++i) {
const QChar c = name.at(i);
// Operators and their signatures belong to the leaf, including operator< and operator().
if (!templates && !parentheses && i == start && name.mid(i, 8) == QLatin1String("operator")
&& (i + 8 == name.size()
|| (!name.at(i + 8).isLetterOrNumber() && name.at(i + 8) != QLatin1Char('_')))) {
break;
}
if (c == QLatin1Char('<')) {
++templates;
} else if (c == QLatin1Char('>') && templates) {
--templates;
} else if (c == QLatin1Char('(')) {
++parentheses;
} else if (c == QLatin1Char(')') && parentheses) {
--parentheses;
} else if (c == QLatin1Char('[')) {
++brackets;
} else if (c == QLatin1Char(']') && brackets) {
--brackets;
}
if (templates || parentheses || brackets) {
continue;
}
int separator = 0;
if (c == QLatin1Char(':') && i + 1 < name.size() && name.at(i + 1) == c) {
separator = 2;
} else if (c == QLatin1Char('-') && i + 1 < name.size() && name.at(i + 1) == QLatin1Char('>')) {
separator = 2;
} else if (c == QLatin1Char('.') || c == QLatin1Char('/') || c == QLatin1Char('$')) {
separator = 1;
}
if (separator) {
i += separator - 1;
if (i + 1 < name.size()) {
parts.append(name.mid(start, i + 1 - start));
start = i + 1;
}
}
}
parts.append(name.mid(start));
return parts;
}
} // namespace
NameListModel::NameListModel(AddressableItemModelI *source, int nameColumn, QObject *parent)
: AddressableItemModel<QAbstractProxyModel>(parent)
, nameColumn(nameColumn)
{
setSourceModel(source->asItemModel());
}
NameListModel::~NameListModel() = default;
void NameListModel::setSourceModel(QAbstractItemModel *source)
{
beginRebuild();
if (sourceModel()) {
disconnect(sourceModel(), nullptr, this, nullptr);
}
QAbstractProxyModel::setSourceModel(source);
sourceProvider = dynamic_cast<AddressableItemModelI *>(source);
if (source) {
if (auto filter = qobject_cast<AddressableFilterProxyModel *>(source)) {
// QSortFilterProxyModel can remove thousands of disjoint row ranges for one query.
// Rebuild the presentation once, after the entire filter operation has finished.
connect(filter, &AddressableFilterProxyModel::filterAboutToChange, this, [this] {
++filterChangeDepth;
beginRebuild();
});
connect(filter, &AddressableFilterProxyModel::filterChanged, this, [this] {
--filterChangeDepth;
endRebuild();
});
}
connect(source, &QAbstractItemModel::modelAboutToBeReset, this, &NameListModel::beginRebuild);
connect(source, &QAbstractItemModel::modelReset, this, &NameListModel::endRebuild);
connect(
source, &QAbstractItemModel::layoutAboutToBeChanged, this, &NameListModel::beginRebuild);
connect(source, &QAbstractItemModel::layoutChanged, this, &NameListModel::endRebuild);
connect(
source, &QAbstractItemModel::rowsAboutToBeInserted, this, &NameListModel::beginRebuild);
connect(source, &QAbstractItemModel::rowsInserted, this, &NameListModel::endRebuild);
connect(source, &QAbstractItemModel::rowsAboutToBeRemoved, this, &NameListModel::beginRebuild);
connect(source, &QAbstractItemModel::rowsRemoved, this, &NameListModel::endRebuild);
connect(source, &QAbstractItemModel::rowsAboutToBeMoved, this, &NameListModel::beginRebuild);
connect(source, &QAbstractItemModel::rowsMoved, this, &NameListModel::endRebuild);
connect(source, &QAbstractItemModel::dataChanged, this, &NameListModel::sourceDataChanged);
connect(
source, &QAbstractItemModel::headerDataChanged, this, &NameListModel::headerDataChanged);
connect(source, &QObject::destroyed, this, [this] {
sourceProvider = nullptr;
beginRebuild();
endRebuild();
});
}
endRebuild();
}
NameListModel::Node *NameListModel::node(const QModelIndex &index) const
{
return index.isValid() ? static_cast<Node *>(index.internalPointer()) : nullptr;
}
QModelIndex NameListModel::nodeIndex(Node *item, int column) const
{
return item && item != &root ? createIndex(item->row, column, item) : QModelIndex();
}
QModelIndex NameListModel::index(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || column < 0 || column >= columnCount() || (parent.isValid() && parent.column())) {
return {};
}
const Node *item = parent.isValid() ? node(parent) : &root;
return row < int(item->children.size()) ? nodeIndex(item->children[row].get(), column)
: QModelIndex();
}
QModelIndex NameListModel::parent(const QModelIndex &index) const
{
auto item = node(index);
return item ? nodeIndex(item->parent) : QModelIndex();
}
int NameListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid() && parent.column()) {
return 0;
}
const Node *item = parent.isValid() ? node(parent) : &root;
return int(item->children.size());
}
int NameListModel::columnCount(const QModelIndex &) const
{
return sourceProvider ? sourceModel()->columnCount() : 0;
}
bool NameListModel::hasChildren(const QModelIndex &parent) const
{
return rowCount(parent) > 0;
}
QModelIndex NameListModel::mapToSource(const QModelIndex &index) const
{
auto item = node(index);
return sourceProvider && item && item->source.isValid()
? item->source.sibling(item->source.row(), index.column())
: QModelIndex();
}
QModelIndex NameListModel::mapFromSource(const QModelIndex &index) const
{
return index.isValid()
? nodeIndex(sourceNodes.value(index.sibling(index.row(), 0)), index.column())
: QModelIndex();
}
QVariant NameListModel::data(const QModelIndex &index, int role) const
{
auto item = node(index);
if (!item) {
return {};
}
if (!item->source.isValid()) {
if (role == GroupPathRole) {
return item->groupPath;
}
if (index.column() == nameColumn) {
if (role == Qt::DisplayRole) {
return item->label;
}
if (role == Qt::ToolTipRole) {
return QStringLiteral("<qt>%1</qt>").arg(item->groupPath.toHtmlEscaped());
}
}
return {};
}
if (index.column() == nameColumn && item->named) {
if (role == Qt::ToolTipRole) {
const auto tooltip = mapToSource(index).data(NameToolTipRole);
if (tooltip.isValid()) {
return tooltip;
}
return QStringLiteral("<qt>%1</qt>").arg(item->fullName.toHtmlEscaped());
}
if (role == Qt::DisplayRole) {
QString label = item->label;
if (compact && item->row > 0) {
auto previous = item->parent->children[item->row - 1].get();
if (previous->named) {
label = compactName(label, previous->label);
}
}
const QString original = mapToSource(index).data().toString();
// Keep decorations represented as text, such as a function's pin emoji.
if (original.endsWith(item->fullName)) {
return original.left(original.size() - item->fullName.size()) + label;
}
return label;
}
}
return mapToSource(index).data(role);
}
Qt::ItemFlags NameListModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
auto source = mapToSource(index);
return source.isValid() ? source.flags() : Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
QVariant NameListModel::headerData(int section, Qt::Orientation orientation, int role) const
{
return sourceProvider ? sourceModel()->headerData(section, orientation, role) : QVariant();
}
void NameListModel::sort(int column, Qt::SortOrder order)
{
if (sourceProvider) {
sourceModel()->sort(column, order);
}
}
RVA NameListModel::address(const QModelIndex &index) const
{
const auto source = mapToSource(index);
return sourceProvider && source.isValid() ? sourceProvider->address(source) : RVA_INVALID;
}
QString NameListModel::name(const QModelIndex &index) const
{
const auto source = mapToSource(index);
return sourceProvider && source.isValid() ? sourceProvider->name(source) : QString();
}
void NameListModel::beginRebuild()
{
if (!resetting) {
resetting = true;
beginResetModel();
}
}
void NameListModel::endRebuild()
{
if (resetting && !filterChangeDepth) {
rebuild();
resetting = false;
endResetModel();
}
}
void NameListModel::appendSource(
Node *parent, const QModelIndex &source, const QString &label, bool named)
{
auto item = std::make_unique<Node>();
item->parent = parent;
item->row = int(parent->children.size());
item->source = source;
item->label = label;
item->named = named;
if (named) {
item->fullName = sourceProvider->name(source);
}
auto ptr = item.get();
sourceNodes.insert(source, ptr);
parent->children.push_back(std::move(item));
// Preserve existing class/member relationships and unnamed function detail rows.
if (sourceModel()->hasChildren(source)) {
for (int row = 0; row < sourceModel()->rowCount(source); ++row) {
auto child = sourceModel()->index(row, 0, source);
QString childName = sourceProvider->name(child);
const bool childNamed = !childName.isEmpty() && childName != ptr->fullName;
if (childNamed) {
for (const QString &separator :
{QStringLiteral("::"),
QStringLiteral("."),
QStringLiteral("/"),
QStringLiteral("$"),
QStringLiteral("->")}) {
const QString prefix = ptr->fullName + separator;
if (childName.startsWith(prefix)) {
childName.remove(0, prefix.size());
break;
}
}
}
appendSource(ptr, child, childName, childNamed);
}
}
}
void NameListModel::rebuild()
{
sourceNodes.clear();
groups.clear();
root.children.clear();
if (!sourceProvider) {
return;
}
for (int row = 0; row < sourceModel()->rowCount(); ++row) {
auto source = sourceModel()->index(row, 0);
const QString fullName = sourceProvider->name(source);
Node *parent = &root;
QString label = fullName;
if (hierarchy) {
const auto parts = nameParts(fullName);
QString path;
for (int part = 0; part + 1 < parts.size(); ++part) {
path += parts[part];
auto group = groups.value(path);
if (!group) {
auto item = std::make_unique<Node>();
item->parent = parent;
item->row = int(parent->children.size());
item->label = parts[part];
item->groupPath = path;
group = item.get();
parent->children.push_back(std::move(item));
groups.insert(path, group);
}
parent = group;
}
label = parts.last();
}
appendSource(parent, source, label, true);
}
collapseSingleGroups(&root);
}
void NameListModel::collapseSingleGroups(Node *parent)
{
for (auto &child : parent->children) {
if (child->source.isValid()) {
continue; // Class/member relationships belong to the source model.
}
collapseSingleGroups(child.get());
if (child->children.size() == 1 && child->children.front()->source.isValid()) {
auto group = std::move(child);
child = std::move(group->children.front());
child->label.prepend(group->label);
child->parent = parent;
child->row = group->row;
groups.remove(group->groupPath);
}
}
}
void NameListModel::sourceDataChanged(const QModelIndex &first, const QModelIndex &last)
{
if (resetting) {
return;
}
for (int row = first.row(); row <= last.row(); ++row) {
auto source = sourceModel()->index(row, 0, first.parent());
auto item = sourceNodes.value(source);
if (item && item->named && item->fullName != sourceProvider->name(source)) {
beginRebuild();
endRebuild();
return;
}
}
for (int row = first.row(); row <= last.row(); ++row) {
auto source = sourceModel()->index(row, 0, first.parent());
auto item = sourceNodes.value(source);
if (item) {
emit dataChanged(nodeIndex(item, first.column()), nodeIndex(item, last.column()));
}
}
}
void NameListModel::namesChanged(Node *parent)
{
if (!parent->children.empty()) {
emit dataChanged(
nodeIndex(parent->children.front().get(), nameColumn),
nodeIndex(parent->children.back().get(), nameColumn),
{Qt::DisplayRole});
for (const auto &child : parent->children) {
namesChanged(child.get());
}
}
}
void NameListModel::setCompact(bool enabled)
{
if (compact != enabled) {
compact = enabled;
namesChanged(&root);
}
}
void NameListModel::setHierarchy(bool enabled)
{
if (hierarchy != enabled) {
beginRebuild();
hierarchy = enabled;
endRebuild();
}
}
QModelIndex NameListModel::groupIndex(const QString &path) const
{
return nodeIndex(groups.value(path));
}
+79
View File
@@ -0,0 +1,79 @@
#ifndef NAMELISTMODEL_H
#define NAMELISTMODEL_H
#include "AddressableItemModel.h"
#include <memory>
#include <vector>
#include <QAbstractProxyModel>
#include <QHash>
// Presentation only: all addresses, descriptions and edits belong to the source model.
class IAITO_EXPORT NameListModel : public AddressableItemModel<QAbstractProxyModel>
{
public:
enum {
GroupPathRole = Qt::UserRole + 0x1000,
// A complete, HTML-escaped name tooltip supplied by models with alternate spellings.
NameToolTipRole
};
NameListModel(AddressableItemModelI *source, int nameColumn, QObject *parent = nullptr);
~NameListModel() override;
void setSourceModel(QAbstractItemModel *source) override;
QModelIndex mapToSource(const QModelIndex &index) const override;
QModelIndex mapFromSource(const QModelIndex &index) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = {}) const override;
int columnCount(const QModelIndex &parent = {}) const override;
bool hasChildren(const QModelIndex &parent = {}) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(
int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
RVA address(const QModelIndex &index) const override;
QString name(const QModelIndex &index) const override;
void setCompact(bool enabled);
void setHierarchy(bool enabled);
bool isHierarchy() const { return hierarchy; }
QModelIndex groupIndex(const QString &path) const;
private:
struct Node
{
Node *parent = nullptr;
int row = 0;
QModelIndex source;
QString label;
QString fullName;
bool named = false;
QString groupPath;
std::vector<std::unique_ptr<Node>> children;
};
AddressableItemModelI *sourceProvider = nullptr;
int nameColumn;
bool compact = false;
bool hierarchy = false;
bool resetting = false;
int filterChangeDepth = 0;
Node root;
QHash<QModelIndex, Node *> sourceNodes;
QHash<QString, Node *> groups;
Node *node(const QModelIndex &index) const;
QModelIndex nodeIndex(Node *node, int column = 0) const;
void beginRebuild();
void endRebuild();
void rebuild();
void collapseSingleGroups(Node *parent);
void appendSource(Node *parent, const QModelIndex &source, const QString &label, bool named);
void sourceDataChanged(const QModelIndex &first, const QModelIndex &last);
void namesChanged(Node *parent);
};
#endif
+15 -1
View File
@@ -2,6 +2,7 @@
#include "R2DecaiDecompiler.h"
#include "Iaito.h"
#include <QByteArray>
#include <QJsonArray>
#include <QJsonObject>
@@ -48,7 +49,20 @@ static QStringList parseDecaiEvalLines(const QString &output)
QStringList R2DecaiDecompiler::listOptions()
{
QString output = Core()->cmd("decai -e");
// r2js console.log writes to stdout instead of the command's output buffer.
const QByteArray script = R"((function() {
var log = console.log;
var lines = [];
console.log = function() { lines.push(Array.prototype.join.call(arguments, " ")); };
try {
var output = r2.cmd("decai -e");
if (output) { lines.push(output); }
} finally {
console.log = log;
}
r2.log(lines.join("\n"));
})())";
QString output = Core()->cmd(("js base64:" + script.toBase64()).constData());
return parseDecaiEvalLines(output);
}
+7
View File
@@ -25,6 +25,13 @@
#error "iaito requires radare2 6.0.0 or newer"
#endif
// The complete generic RBin resource API is available starting with ABI 117.
#if defined(R2_ABIVERSION) && R2_ABIVERSION >= 117
#define IAITO_HAS_R_BIN_RESOURCES 1
#else
#define IAITO_HAS_R_BIN_RESOURCES 0
#endif
#ifdef IAITO_RESTORE_QT_EMIT
#pragma pop_macro("emit")
#undef IAITO_RESTORE_QT_EMIT
+136 -3
View File
@@ -106,6 +106,61 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Close tab"),
{},
QKeySequence::Close});
#ifdef Q_OS_MACOS
// Qt maps the Command key to CTRL on macOS.
const auto panelModifier = Qt::CTRL;
#else
const auto panelModifier = Qt::META;
#endif
t.append(
{"global.maximizePanel",
ShortcutScope::Global,
QT_TR_NOOP("Toggle panel maximization"),
{QKeySequence(panelModifier | Qt::Key_Return),
QKeySequence(panelModifier | Qt::Key_Enter)},
Unknown});
t.append(
{"global.selectPanelTab1",
ShortcutScope::Global,
QT_TR_NOOP("Select panel tab 1"),
{QKeySequence(panelModifier | Qt::Key_1)},
Unknown});
t.append(
{"global.selectPanelTab2",
ShortcutScope::Global,
QT_TR_NOOP("Select panel tab 2"),
{QKeySequence(panelModifier | Qt::Key_2)},
Unknown});
t.append(
{"global.selectPanelTab3",
ShortcutScope::Global,
QT_TR_NOOP("Select panel tab 3"),
{QKeySequence(panelModifier | Qt::Key_3)},
Unknown});
t.append(
{"global.selectPanelTab4",
ShortcutScope::Global,
QT_TR_NOOP("Select panel tab 4"),
{QKeySequence(panelModifier | Qt::Key_4)},
Unknown});
t.append(
{"global.selectPanelTab5",
ShortcutScope::Global,
QT_TR_NOOP("Select panel tab 5"),
{QKeySequence(panelModifier | Qt::Key_5)},
Unknown});
t.append(
{"global.focusNextFrame",
ShortcutScope::Global,
QT_TR_NOOP("Focus next frame"),
{QKeySequence(panelModifier | Qt::Key_J)},
Unknown});
t.append(
{"global.focusPreviousFrame",
ShortcutScope::Global,
QT_TR_NOOP("Focus previous frame"),
{QKeySequence(panelModifier | Qt::Key_K)},
Unknown});
t.append(
{"global.back",
ShortcutScope::Global,
@@ -254,6 +309,12 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Set to data (resize)"),
{QKeySequence(Qt::Key_Asterisk)},
Unknown});
t.append(
{"disasm.flag",
ShortcutScope::Disassembly,
QT_TR_NOOP("Add / edit flag"),
{QKeySequence(Qt::Key_F)},
Unknown});
t.append(
{"disasm.rename",
ShortcutScope::Disassembly,
@@ -276,7 +337,7 @@ const QVector<ShortcutDescriptor> &catalog()
{"disasm.linkType",
ShortcutScope::Disassembly,
QT_TR_NOOP("Link type"),
{QKeySequence(Qt::Key_L)},
{QKeySequence(Qt::SHIFT | Qt::Key_L)},
Unknown});
t.append(
{"disasm.addBreakpoint",
@@ -308,12 +369,30 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Switch to graph view"),
{QKeySequence(Qt::Key_Space)},
Unknown});
t.append(
{"disasm.switchToDecompiler",
ShortcutScope::Disassembly,
QT_TR_NOOP("Switch to decompiler view"),
{QKeySequence(Qt::Key_Tab)},
Unknown});
t.append(
{"disasm.seekPrev",
ShortcutScope::Disassembly,
QT_TR_NOOP("Seek to previous"),
{QKeySequence(Qt::Key_Escape)},
Unknown});
t.append(
{"disasm.cursorLeft",
ShortcutScope::Disassembly,
QT_TR_NOOP("Move cursor left"),
{QKeySequence(Qt::Key_H), QKeySequence(Qt::Key_Left)},
Unknown});
t.append(
{"disasm.cursorRight",
ShortcutScope::Disassembly,
QT_TR_NOOP("Move cursor right"),
{QKeySequence(Qt::Key_L), QKeySequence(Qt::Key_Right)},
Unknown});
t.append(
{"disasm.cursorDown",
ShortcutScope::Disassembly,
@@ -341,8 +420,8 @@ const QVector<ShortcutDescriptor> &catalog()
t.append(
{"disasm.jumpToOffset",
ShortcutScope::Disassembly,
QT_TR_NOOP("Jump to offset under cursor"),
{QKeySequence(Qt::Key_Return)},
QT_TR_NOOP("Follow reference under cursor"),
{QKeySequence(Qt::Key_Return), QKeySequence(Qt::Key_Enter)},
Unknown});
t.append(
@@ -351,6 +430,30 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Copy"),
{},
QKeySequence::Copy});
t.append(
{"decompiler.cursorLeft",
ShortcutScope::Decompiler,
QT_TR_NOOP("Move cursor left"),
{QKeySequence(Qt::Key_H)},
Unknown});
t.append(
{"decompiler.cursorDown",
ShortcutScope::Decompiler,
QT_TR_NOOP("Move cursor down"),
{QKeySequence(Qt::Key_J)},
Unknown});
t.append(
{"decompiler.cursorUp",
ShortcutScope::Decompiler,
QT_TR_NOOP("Move cursor up"),
{QKeySequence(Qt::Key_K)},
Unknown});
t.append(
{"decompiler.cursorRight",
ShortcutScope::Decompiler,
QT_TR_NOOP("Move cursor right"),
{QKeySequence(Qt::Key_L)},
Unknown});
t.append(
{"decompiler.comment",
ShortcutScope::Decompiler,
@@ -381,6 +484,24 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Edit variables"),
{QKeySequence(Qt::Key_Y)},
Unknown});
t.append(
{"decompiler.followReference",
ShortcutScope::Decompiler,
QT_TR_NOOP("Follow reference under cursor"),
{QKeySequence(Qt::Key_Return), QKeySequence(Qt::Key_Enter)},
Unknown});
t.append(
{"decompiler.matchBrace",
ShortcutScope::Decompiler,
QT_TR_NOOP("Jump to matching brace"),
{QKeySequence(Qt::Key_Percent), QKeySequence(Qt::SHIFT | Qt::Key_Percent)},
Unknown});
t.append(
{"decompiler.toggleFold",
ShortcutScope::Decompiler,
QT_TR_NOOP("Collapse/expand block"),
{QKeySequence(Qt::Key_Space)},
Unknown});
t.append(
{"decompiler.toggleBreakpoint",
ShortcutScope::Decompiler,
@@ -399,6 +520,12 @@ const QVector<ShortcutDescriptor> &catalog()
QT_TR_NOOP("Seek to previous"),
{QKeySequence(Qt::Key_Escape)},
Unknown});
t.append(
{"decompiler.switchToDisassembly",
ShortcutScope::Decompiler,
QT_TR_NOOP("Switch to disassembly view"),
{QKeySequence(Qt::Key_Tab)},
Unknown});
t.append(
{"addressable.copyAddress",
@@ -492,6 +619,12 @@ const QVector<ShortcutDescriptor> &catalog()
{QKeySequence(Qt::SHIFT | Qt::Key_L)},
Unknown});
t.append({"hex.copy", ShortcutScope::Hexdump, QT_TR_NOOP("Copy"), {}, QKeySequence::Copy});
t.append(
{"hex.flag",
ShortcutScope::Hexdump,
QT_TR_NOOP("Add / edit flag"),
{QKeySequence(Qt::Key_F)},
Unknown});
t.append(
{"hex.copyAddress",
ShortcutScope::Hexdump,
+30
View File
@@ -0,0 +1,30 @@
#include "SourceLineReference.h"
QList<SourceLineReference> SourceLineReference::parse(const QString &contents)
{
QList<SourceLineReference> result;
for (const QString &row : contents.split(QLatin1Char('\n'))) {
const int firstTab = row.indexOf(QLatin1Char('\t'));
const int secondTab = row.indexOf(QLatin1Char('\t'), firstTab + 1);
if (firstTab <= 0 || secondTab < 0) {
continue;
}
const QString addressText = row.left(firstTab);
if (!addressText.startsWith(QStringLiteral("0x"))) {
continue;
}
bool addressOk = false;
bool lineOk = false;
const quint64 address = addressText.mid(2).toULongLong(&addressOk, 16);
const int line = row.mid(firstTab + 1, secondTab - firstTab - 1).toInt(&lineOk);
if (!addressOk || !lineOk || line < 0) {
continue;
}
QString code = row.mid(secondTab + 1);
if (code.endsWith(QLatin1Char('\r'))) {
code.chop(1);
}
result.append({address, line, code});
}
return result;
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <QList>
#include <QString>
struct SourceLineReference
{
quint64 address;
int line;
QString code;
// fs.r2's /cl files contain address, line number and source text, separated by tabs.
static QList<SourceLineReference> parse(const QString &contents);
};
+88
View File
@@ -0,0 +1,88 @@
#ifndef SPARSEHEXLAYOUT_H
#define SPARSEHEXLAYOUT_H
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <vector>
namespace SparseHexLayout {
// Offsets are relative to a bounded read buffer, never absolute addresses.
struct Row
{
int offset;
int size;
int fill = -1;
bool continues = false;
};
inline int uniformPrefix(const unsigned char *bytes, int size)
{
const uint64_t pattern = bytes[0] ? UINT64_MAX : 0;
int i = 0;
while (size - i >= int(sizeof(pattern))) {
uint64_t word;
std::memcpy(&word, bytes + i, sizeof(word));
if (word != pattern) {
break;
}
i += sizeof(word);
}
while (i < size && bytes[i] == bytes[0]) {
++i;
}
return i;
}
inline std::vector<Row> build(
const unsigned char *bytes, int size, int rowSize, int itemSize, int maxRows)
{
std::vector<Row> rows;
if (size <= 0 || rowSize <= 0 || itemSize <= 0 || maxRows <= 0) {
return rows;
}
rows.reserve(std::min(maxRows, size / rowSize + 2));
int begin = 0;
int pos = 0;
while (pos < size && int(rows.size()) < maxRows) {
int step = itemSize;
if (bytes[pos] == 0 || bytes[pos] == 0xff) {
const int run = uniformPrefix(bytes + pos, size - pos);
if (run > rowSize * 2) {
if (pos > begin) {
rows.push_back({begin, pos - begin});
if (int(rows.size()) == maxRows) {
return rows;
}
}
// Keep multi-byte items intact, including at the end of a read.
const int folded = run - run % itemSize;
rows.push_back({pos, folded, bytes[pos], pos + run == size});
begin = pos = pos + folded;
continue;
}
// Consume short runs once too; rescanning every suffix is quadratic
// when the user selects a large number of columns.
step = std::max(itemSize, run - run % itemSize);
}
step = std::min(step, size - pos);
while (step > 0 && int(rows.size()) < maxRows) {
const int count = std::min(step, rowSize - (pos - begin));
pos += count;
step -= count;
if (pos - begin == rowSize) {
rows.push_back({begin, rowSize});
begin = pos;
}
}
}
if (pos > begin && int(rows.size()) < maxRows) {
rows.push_back({begin, pos - begin});
}
return rows;
}
} // namespace SparseHexLayout
#endif // SPARSEHEXLAYOUT_H
+253
View File
@@ -0,0 +1,253 @@
#include "TypeLayout.h"
#include <algorithm>
#include <cstring>
#include <limits>
#include <QRegularExpression>
void TypeLayout::load(const QString &text, int pointerByteSize)
{
database.clear();
sizes.clear();
pointerSize = qBound(1, pointerByteSize, 8);
for (const QString &line : text.split(QLatin1Char('\n'))) {
const int separator = line.indexOf(QLatin1Char('='));
if (separator > 0) {
database.insert(line.left(separator), line.mid(separator + 1));
}
}
}
QStringList TypeLayout::names() const
{
QStringList result;
for (auto it = database.cbegin(); it != database.cend(); ++it) {
if (it.value() == QLatin1String("struct") || it.value() == QLatin1String("union")
|| it.value() == QLatin1String("typedef") || it.value() == QLatin1String("type")
|| it.value() == QLatin1String("enum")) {
result.append(it.key());
}
}
result.sort(Qt::CaseInsensitive);
return result;
}
QMap<quint64, QString> TypeLayout::links() const
{
QMap<quint64, QString> result;
for (auto it = database.cbegin(); it != database.cend(); ++it) {
if (it.key().startsWith(QLatin1String("link."))) {
bool ok = false;
const quint64 address = it.key().mid(5).toULongLong(&ok, 16);
if (ok) {
result.insert(address, it.value());
}
}
}
return result;
}
QString TypeLayout::resolve(QString type) const
{
static const QRegularExpression qualifiers(QStringLiteral("\\b(const|volatile|restrict)\\b"));
static const QRegularExpression tag(QStringLiteral("^(struct|union|enum)\\s+"));
QStringList visited;
for (int i = 0; i < 32; ++i) {
type.remove(qualifiers);
type = type.simplified();
type.remove(tag);
if (visited.contains(type)) {
return QString();
}
visited.append(type);
if (database.value(type) != QLatin1String("typedef")) {
return type;
}
type = database.value(QStringLiteral("typedef.") + type);
}
return QString();
}
bool TypeLayout::array(const QString &type, QString *element, quint64 *count)
{
static const QRegularExpression expression(QStringLiteral("^(.*?)\\[(\\d+)\\](.*)$"));
const auto match = expression.match(type);
if (!match.hasMatch()) {
return false;
}
bool ok = false;
*count = match.captured(2).toULongLong(&ok);
*element = (match.captured(1) + match.captured(3)).trimmed();
return ok;
}
QString TypeLayout::kind(const QString &type) const
{
const QString resolved = resolve(type);
if (resolved.endsWith(QLatin1Char('*'))) {
return QStringLiteral("pointer");
}
QString element;
quint64 count;
if (array(resolved, &element, &count)) {
return QStringLiteral("array");
}
return database.value(resolved);
}
QVector<TypeLayout::Field> TypeLayout::fields(const QString &type) const
{
return fields(type, {resolve(type)});
}
QVector<TypeLayout::Field> TypeLayout::fields(const QString &type, const QStringList &ancestors) const
{
const QString resolved = resolve(type);
const QString category = kind(resolved);
QVector<Field> result;
if (category != QLatin1String("struct") && category != QLatin1String("union")) {
return result;
}
const QString prefix = category + QLatin1Char('.') + resolved;
const QStringList members = database.value(prefix).split(QLatin1Char(','), Qt::SkipEmptyParts);
bool allZero = true;
for (const QString &name : members) {
const QStringList details
= database.value(prefix + QLatin1Char('.') + name).split(QLatin1Char(','));
if (details.size() < 3) {
continue;
}
bool ok = false;
const quint64 offset = details.at(1).toULongLong(&ok, 0);
if (!ok) {
continue;
}
const quint64 count = details.at(2).toULongLong(&ok, 0);
if (!ok) {
continue;
}
result.append({name, details.first(), offset, qMax<quint64>(1, count)});
allZero &= offset == 0;
}
// Some bundled r2 definitions omit offsets entirely. Their members are packed.
if (allZero && category == QLatin1String("struct") && result.size() > 1) {
quint64 offset = 0;
for (Field &field : result) {
field.offset = offset;
const quint64 bytes = size(field.type, ancestors);
if (bytes && field.count <= (std::numeric_limits<quint64>::max() - offset) / bytes) {
offset += bytes * field.count;
}
}
}
return result;
}
quint64 TypeLayout::size(const QString &type) const
{
const auto cached = sizes.constFind(type);
if (cached != sizes.cend()) {
return cached.value();
}
const quint64 result = size(type, {});
sizes.insert(type, result);
return result;
}
quint64 TypeLayout::size(const QString &type, QStringList ancestors) const
{
const QString resolved = resolve(type);
if (resolved.isEmpty() || ancestors.size() >= 32 || ancestors.contains(resolved)) {
return 0;
}
ancestors.append(resolved);
const QString category = kind(resolved);
if (category == QLatin1String("pointer")) {
return pointerSize;
}
QString element;
quint64 count;
if (array(resolved, &element, &count)) {
const quint64 stride = size(element, ancestors);
return stride && count <= std::numeric_limits<quint64>::max() / stride ? stride * count : 0;
}
if (category == QLatin1String("enum")) {
return 4;
}
if (category == QLatin1String("struct") || category == QLatin1String("union")) {
quint64 end = 0;
for (const Field &field : fields(resolved, ancestors)) {
const quint64 stride = size(field.type, ancestors);
if (!stride
|| field.count > (std::numeric_limits<quint64>::max() - field.offset) / stride) {
return 0;
}
end = qMax(end, field.offset + stride * field.count);
}
return end;
}
bool ok = false;
const qint64 bits = database.value(QStringLiteral("type.") + resolved + QStringLiteral(".size"))
.toLongLong(&ok);
return ok && bits > 0 ? quint64(bits) / 8 : 0;
}
quint64 TypeLayout::unsignedValue(const QByteArray &bytes, bool bigEndian)
{
quint64 result = 0;
for (int i = 0; i < qMin(8, int(bytes.size())); ++i) {
const int shift = (bigEndian ? bytes.size() - i - 1 : i) * 8;
if (shift < 64) {
result |= quint64(static_cast<unsigned char>(bytes.at(i))) << shift;
}
}
return result;
}
QString TypeLayout::value(const QString &type, const QByteArray &bytes, bool bigEndian) const
{
const QString resolved = resolve(type);
const quint64 number = unsignedValue(bytes, bigEndian);
if (kind(resolved) == QLatin1String("pointer")) {
return QStringLiteral("0x%1").arg(number, 0, 16);
}
if (resolved == QLatin1String("float") && bytes.size() == 4) {
const quint32 raw = quint32(number);
float value;
memcpy(&value, &raw, sizeof(value));
return QString::number(value, 'g', 8);
}
if (resolved == QLatin1String("double") && bytes.size() == 8) {
double value;
memcpy(&value, &number, sizeof(value));
return QString::number(value, 'g', 16);
}
if (bytes.size() > 8) {
return QString::fromLatin1(bytes.toHex(' '));
}
const bool isUnsigned = resolved.startsWith(QLatin1String("uint"))
|| resolved.startsWith(QLatin1String("unsigned"));
quint64 extended = number;
if (!isUnsigned && !bytes.isEmpty() && bytes.size() < 8
&& (number & (quint64(1) << (bytes.size() * 8 - 1)))) {
extended |= std::numeric_limits<quint64>::max() << (bytes.size() * 8);
}
qint64 signedNumber;
memcpy(&signedNumber, &extended, sizeof(signedNumber));
QString result = isUnsigned ? QString::number(number) : QString::number(signedNumber);
if (kind(resolved) == QLatin1String("enum")) {
const QString prefix = QStringLiteral("enum.") + resolved + QLatin1Char('.');
for (auto it = database.cbegin(); it != database.cend(); ++it) {
bool ok = false;
const qint64 candidate = it.value().toLongLong(&ok, 0);
if (it.key().startsWith(prefix) && ok && candidate == signedNumber) {
result += QStringLiteral(" (%1)").arg(it.key().mid(prefix.size()));
break;
}
}
}
if (resolved == QLatin1String("char") && number >= 32 && number < 127) {
result += QStringLiteral(" '%1'").arg(QChar(ushort(number)));
}
return result + QStringLiteral(" (0x%1)").arg(number, 0, 16);
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef TYPELAYOUT_H
#define TYPELAYOUT_H
#include <QByteArray>
#include <QHash>
#include <QMap>
#include <QStringList>
#include <QVector>
// A snapshot of r2's type database. Keep stored offsets: pf formats and tsj can
// lose padding, and JSON numbers cannot represent every 64-bit type-link address.
class TypeLayout
{
public:
struct Field
{
QString name;
QString type;
quint64 offset = 0;
quint64 count = 1;
};
void load(const QString &database, int pointerSize);
QStringList names() const;
QMap<quint64, QString> links() const;
QString resolve(QString type) const;
QString kind(const QString &type) const;
QVector<Field> fields(const QString &type) const;
quint64 size(const QString &type) const;
QString value(const QString &type, const QByteArray &bytes, bool bigEndian) const;
static quint64 unsignedValue(const QByteArray &bytes, bool bigEndian);
static bool array(const QString &type, QString *element, quint64 *count);
private:
QHash<QString, QString> database;
int pointerSize = 8;
mutable QHash<QString, quint64> sizes;
QVector<Field> fields(const QString &type, const QStringList &ancestors) const;
quint64 size(const QString &type, QStringList ancestors) const;
};
#endif // TYPELAYOUT_H
+7 -1
View File
@@ -713,13 +713,19 @@ void IaitoStyle::drawControl(
return;
}
const bool accent = m_theme.chromeStyle == ChromeStyle::Accent;
const bool accentTabs = m_theme.chromeStyle == ChromeStyle::AccentTabs;
painter->save();
const bool selected = tab->state & State_Selected;
const bool hovered = (tab->state & State_MouseOver) && !selected
&& (tab->state & State_Enabled);
QColor bg;
QColor fg = m_theme.text.normal;
if (accent) {
if (accentTabs) {
bg = selected ? m_theme.accent.normal
: hovered ? m_theme.surface.hovered
: m_theme.background.normal;
fg = selected ? m_theme.accentText.normal : m_theme.text.normal;
} else if (accent) {
bg = selected ? m_theme.background.normal
: hovered ? m_theme.accent.hovered
: m_theme.accent.normal;
+16 -2
View File
@@ -216,12 +216,26 @@ Skin skinFromString(const QString &s)
QString chromeToString(ChromeStyle c)
{
return c == ChromeStyle::Accent ? QStringLiteral("accent") : QStringLiteral("flat");
switch (c) {
case ChromeStyle::Accent:
return QStringLiteral("accent");
case ChromeStyle::AccentTabs:
return QStringLiteral("accent-tabs");
case ChromeStyle::Flat:
return QStringLiteral("flat");
}
return QStringLiteral("flat");
}
ChromeStyle chromeFromString(const QString &s)
{
return s == QLatin1String("accent") ? ChromeStyle::Accent : ChromeStyle::Flat;
if (s == QLatin1String("accent")) {
return ChromeStyle::Accent;
}
if (s == QLatin1String("accent-tabs")) {
return ChromeStyle::AccentTabs;
}
return ChromeStyle::Flat;
}
QString hintingToString(int h)
+1
View File
@@ -46,6 +46,7 @@ struct ThemeFonts
enum class ChromeStyle {
Flat,
Accent,
AccentTabs,
};
enum class Skin {
+379 -50
View File
@@ -22,6 +22,7 @@
#include <QPoint>
#include <QRect>
#include <QScopedValueRollback>
#include <QSet>
#include <QSizeGrip>
#include <QTabBar>
#include <QTimer>
@@ -43,6 +44,28 @@ static const char *dockHandleResizeGripProperty = "iaitoDockHandleResizeGrip";
static const char *dockTabCloseButtonIndexProperty = "iaitoDockTabCloseButtonIndex";
static const char *dockTabBarConfiguredProperty = "iaitoDockTabBarConfigured";
// Four opaque strips outline the active frame without changing its layout.
// Keeping them narrow avoids translucent child compositing artifacts on macOS.
class DockFocusBorder : public QWidget
{
public:
explicit DockFocusBorder(QWidget *parent)
: QWidget(parent)
{
setObjectName(QStringLiteral("activeFrameBorder"));
setAttribute(Qt::WA_TransparentForMouseEvents);
setAttribute(Qt::WA_OpaquePaintEvent);
setFocusPolicy(Qt::NoFocus);
}
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.fillRect(rect(), palette().highlight());
}
};
// Translucent highlight, in accent color, of the region a dragged panel would
// occupy if dropped now. Transparent to mouse input so it never interferes with
// the drag. Has two modes (see updateDropOverlay):
@@ -132,6 +155,10 @@ QWidget *newDockDragHandleTitleBar(QWidget *parent, bool withControls, bool with
// Tabbed panel: the tab already shows the title and the close-on-hover
// button, so the bar is just a thin grip for dragging the panel around.
handle->setFixedHeight(8);
// Without a size hint the dock layout lays the content out under the grip
auto *gripLayout = new QHBoxLayout(handle);
gripLayout->setContentsMargins(0, 0, 0, 0);
gripLayout->addSpacerItem(new QSpacerItem(0, 8, QSizePolicy::Expanding, QSizePolicy::Fixed));
return handle;
}
@@ -195,15 +222,13 @@ QString panelDisplayName(const IaitoDockWidget *dock)
name.chop(3);
name = name.trimmed();
}
while (name.endsWith(QChar(0x2026))) {
name.chop(1);
name = name.trimmed();
}
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)
@@ -313,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);
@@ -341,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();
@@ -482,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();
}
}
@@ -560,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;
}
@@ -572,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) {
@@ -724,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;
@@ -753,6 +851,7 @@ void DockManager::detachDockTab(IaitoDockWidget *dock, const QPoint &globalPos)
return;
}
restoreMaximizedPanel();
backend->removeDock(dock);
dock->setParent(mainWindow);
backend->setDockFloating(dock, true);
@@ -1324,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;
}
}
@@ -1441,6 +1749,7 @@ void DockManager::startDockWidgetDrag(
return;
}
restoreMaximizedPanel();
dockTabDragActive = true;
dockDragOffset = offset;
updateDropOverlay(globalPos);
@@ -1736,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) {
@@ -1812,6 +2139,7 @@ bool DockManager::handleEvent(QObject *watched, QEvent *event)
clearDockDrag();
dockDragTabBar = tabBar;
dockDragWidget = dockForDockTab(tabBar, index);
setActiveDock(dockDragWidget);
dockDragStartGlobalPos = mouseEventGlobalPos(mouseEvent);
}
break;
@@ -1849,6 +2177,7 @@ bool DockManager::handleEvent(QObject *watched, QEvent *event)
case QEvent::Show:
case QEvent::Resize:
case QEvent::LayoutRequest: {
requestUpdateActiveFrame();
QPointer<QTabBar> guardedTabBar(tabBar);
QTimer::singleShot(0, this, [this, guardedTabBar]() {
if (guardedTabBar) {
+19 -2
View File
@@ -3,13 +3,14 @@
#include <functional>
#include <QByteArray>
#include <QList>
#include <QMap>
#include <QObject>
#include <QPoint>
#include <QPointer>
#include <QRect>
#include <QSet>
#include <QSize>
#include <QString>
#include <QStringList>
@@ -71,6 +72,11 @@ public:
void updateDockTabBars();
void requestUpdateTabBars();
bool closeCurrentTab();
bool selectCurrentPanelTab(int index);
bool focusNextFrame(bool forward);
bool toggleCurrentPanelMaximized();
void restoreMaximizedPanel();
QByteArray saveState() const;
bool handleEvent(QObject *watched, QEvent *event);
@@ -82,11 +88,14 @@ signals:
void layoutMutated();
private:
void setActiveDock(IaitoDockWidget *dock);
void requestUpdateActiveFrame();
void updateActiveFrame();
IaitoDockWidget *dockForWidget(QWidget *widget) const;
void configureDockWidget(QDockWidget *dock);
bool updateEmptyDockTabBar(QTabBar *tabBar);
bool configureDockTabBar(QTabBar *tabBar);
bool isMainDockTabBar(QTabBar *tabBar) const;
void stampTabBar(QTabBar *tabBar, QSet<IaitoDockWidget *> &claimed);
void updateDockTabCloseButtons(QTabBar *tabBar, int hoveredIndex = -1);
QToolButton *dockTabCloseButton(QTabBar *tabBar);
void closeDockTab(QTabBar *tabBar, int index);
@@ -122,6 +131,14 @@ private:
QList<IaitoDockWidget *> pluginDocks;
QMap<QString, std::function<IaitoDockWidget *(MainWindow *)>> widgetTypeToConstructorMap;
QPointer<IaitoDockWidget> activeDock;
QList<QPointer<QWidget>> activeFrameBorders;
bool activeFrameUpdateScheduled = false;
QPointer<IaitoDockWidget> maximizedDock;
QPointer<QWidget> maximizedFocusWidget;
QByteArray maximizedLayoutState;
QSize maximizedDockMaximumSize;
QPointer<QTabBar> dockDragTabBar;
QPointer<QWidget> dockDragHandle;
QPointer<IaitoDockWidget> dockDragWidget;
+557 -32
View File
@@ -4,6 +4,7 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
#include <QSaveFile>
#include <QSet> // for deduplicating entrypoints by address
#include <QStandardPaths>
#include <QStringList>
@@ -20,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"
@@ -176,14 +178,14 @@ static InstructionProbeResult probeInstructionAddress(RCore *core, RVA address,
{
InstructionProbeResult result;
ut8 buf[64];
int readSize = r_io_read_at(core->io, address, buf, probeSize);
if (readSize < 1) {
probeSize = qBound(1, probeSize, (int) sizeof(buf));
if (!r_io_read_at(core->io, address, buf, probeSize)) {
return result;
}
RAnalOp op;
r_anal_op_init(&op);
int ret = r_anal_op(core->anal, &op, address, buf, readSize, R_ARCH_OP_MASK_BASIC);
int ret = r_anal_op(core->anal, &op, address, buf, probeSize, R_ARCH_OP_MASK_BASIC);
result.size = op.size > 0 ? op.size : 1;
result.valid = ret > 0 && op.size > 0 && op.type != R_ANAL_OP_TYPE_ILL;
r_anal_op_fini(&op);
@@ -775,8 +777,20 @@ bool IaitoCore::loadFile(
r_config_set_i(core->config, "io.va", va);
r_config_set_b(core->config, "bin.cache", bincache);
// An empty "map offset" field results in 0, but the callers that don't go
// through the dialog may pass RVA_INVALID. Normalize both to "not given".
const bool haveMapAddr = (mapaddr != 0 && mapaddr != RVA_INVALID);
const ut64 realMapAddr = haveMapAddr ? mapaddr : 0;
// Address where the contents of the file are mapped (bin.laddr).
// Headerless files are mapped there as a whole, binaries with sections
// are mapped by RBin and ignore it. When there's no map offset use the
// base address, so a headerless file loaded at a given base address is
// also mapped there instead of exposing its contents somewhere else.
// (this is what r2 warns about with "Don't use -B on unknown files")
const ut64 loadAddr = haveMapAddr ? realMapAddr : (baddr == RVA_INVALID ? 0 : baddr);
Core()->loadIaitoRC(0);
RIODesc *f = core_file_open_strip_file_uri(core, path.toUtf8().constData(), perms, mapaddr);
RIODesc *f = core_file_open_strip_file_uri(core, path.toUtf8().constData(), perms, loadAddr);
if (!f) {
R_LOG_ERROR("r_core_file_open failed");
return false;
@@ -788,7 +802,17 @@ bool IaitoCore::loadFile(
bool haveEntry = true;
if (loadbin && va) {
if (!r_core_bin_load(core, path.toUtf8().constData(), baddr)) {
// When a map offset is given, RBin must use it as base address too.
// Otherwise all the addresses coming from the bin object (strings,
// symbols, sections, imports, ...) are relative to the base address
// stored in the binary, while its contents are mapped somewhere
// else, so every widget ends up pointing outside of the io maps.
// r2 does something similar with `oba 0 <mapaddr>` when using -m.
ut64 binAddr = baddr;
if (haveMapAddr && baddr == RVA_INVALID) {
binAddr = realMapAddr;
}
if (!r_core_bin_load(core, path.toUtf8().constData(), binAddr)) {
R_LOG_ERROR("Cannot find rbin information");
}
@@ -805,13 +829,10 @@ bool IaitoCore::loadFile(
#endif
} else {
// Loading shellcodes here
ut64 addr = mapaddr != 0 ? mapaddr : baddr;
char *at = strdup(QString::number(addr).toUtf8().constData());
r_core_cmdf(core, "'-e bin.laddr=%s", at);
r_core_cmdf(core, "'om 3 %s", at);
r_core_cmdf(core, "'s %s", at);
r_core_cmdf(core, "'-e bin.laddr=0x%" PFMT64x, loadAddr);
r_core_cmdf(core, "'om %d 0x%" PFMT64x, f->fd, loadAddr);
r_core_cmdf(core, "'s 0x%" PFMT64x, loadAddr);
haveEntry = false;
free(at);
}
r_core_bin_export_info(core, R_MODE_SET);
@@ -822,8 +843,16 @@ bool IaitoCore::loadFile(
*/
auto debug = r_config_get_b(core->config, "cfg.debug");
if (haveEntry && !debug && r_flag_get(core->flags, "entry0")) {
r_core_cmd0(core, "'s entry0");
if (!debug) {
if (haveMapAddr) {
// like `r2 -m`, seek where the file has been mapped
r_core_seek(core, realMapAddr, true);
} else if (haveEntry && r_flag_get(core->flags, "entry0")) {
r_core_cmd0(core, "'s entry0");
} else if (loadAddr) {
// no entrypoint, at least seek where the contents are
r_core_seek(core, loadAddr, true);
}
}
if (perms & R_PERM_W) {
@@ -863,10 +892,14 @@ bool IaitoCore::tryFile(QString path, bool rw)
bool IaitoCore::mapFile(QString path, RVA mapaddr)
{
CORE_LOCK();
RVA addr = mapaddr != RVA_INVALID ? mapaddr : 0;
ut64 baddr = Core()->getFileInfo().object()["bin"].toObject()["baddr"].toVariant().toULongLong();
const bool haveMapAddr = (mapaddr != 0 && mapaddr != RVA_INVALID);
const RVA addr = haveMapAddr ? mapaddr : 0;
if (core_file_open_strip_file_uri(core, path.toUtf8().constData(), R_PERM_RX, addr)) {
r_core_bin_load(core, path.toUtf8().constData(), baddr);
// Load the bin information at the address where the file is mapped,
// otherwise its strings, symbols and sections are shown at addresses
// that don't match the contents of the io maps. Without a map address
// let RBin use the base address defined by the file itself.
r_core_bin_load(core, path.toUtf8().constData(), haveMapAddr ? addr : UT64_MAX);
} else {
return false;
}
@@ -1288,12 +1321,47 @@ 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();
return r_num_math(core ? core->num : NULL, expr.toUtf8().constData());
}
bool IaitoCore::tryMath(const QString &expr, ut64 &value)
{
const QByteArray expression = expr.trimmed().toUtf8();
if (expression.isEmpty()) {
return false;
}
CORE_LOCK();
RNum *number = core ? core->num : nullptr;
if (!number) {
return false;
}
const char *error = nullptr;
value = r_num_math_err(number, expression.constData(), &error);
return !error && !number->dbz && !r_num_failed(number);
}
ut64 IaitoCore::num(const QString &expr)
{
CORE_LOCK();
@@ -1553,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;
}
@@ -3258,6 +3331,26 @@ QList<RMutaPluginDescription> IaitoCore::getRMutaPluginDescriptions()
return ret;
}
QList<ExceptionRegion> IaitoCore::getExceptionRegions(RVA function)
{
if (exceptionJsonSupport < 0) {
// Older iw implementations print flag commands even for iwj. Probe help
// once, without calling unsupported commands or depending on a new ABI.
const QString help = cmd("i?");
const QRegularExpression jsonHelp(QStringLiteral("\\biw\\[[^\\]\\n]*j"));
exceptionJsonSupport = help.contains(jsonHelp) ? 1 : 0;
}
if (!exceptionJsonSupport) {
return {};
}
const auto regions = ExceptionRegion::parse(cmdj("iwj").array());
if (function == RVA_INVALID || regions.isEmpty()) {
return regions;
}
const auto blocks = cmdj("afbj @ " + RAddressString(function)).array();
return ExceptionRegion::forBlocks(regions, blocks);
}
QList<FunctionDescription> IaitoCore::getAllFunctions()
{
CORE_LOCK();
@@ -3279,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);
@@ -3393,13 +3490,22 @@ QList<SymbolDescription> IaitoCore::getAllSymbols()
if (core && core->bin && core->bin->cur && core->bin->cur->BO) {
RVecRBinSymbol *symbols = r_bin_get_symbols_vec(core->bin);
if (symbols) {
const char *language = r_config_get(core->config, "bin.lang");
const bool keepLibrary = r_config_get_b(core->config, "bin.demangle.pfxlib");
RBinSymbol *bs;
R_VEC_FOREACH(symbols, bs)
{
QString type = QString(bs->bind) + " " + QString(bs->type);
SymbolDescription symbol;
symbol.vaddr = bs->vaddr;
symbol.name = QString(r_bin_name_tostring(bs->name));
const char *rawName = r_bin_name_tostring2(bs->name, 'o');
symbol.name = QString::fromUtf8(rawName);
symbol.demangledName = QString::fromUtf8(r_bin_name_tostring2(bs->name, 'd'));
if (symbol.demangledName == symbol.name) {
char *demangled
= r_bin_demangle(core->bin->cur, language, rawName, bs->vaddr, keepLibrary);
symbol.demangledName = QString::fromUtf8(demangled);
free(demangled);
}
symbol.bind = QString(bs->bind);
symbol.type = QString(bs->type);
ret << symbol;
@@ -3509,9 +3615,15 @@ QList<RelocDescription> IaitoCore::getAllRelocs()
if (core && core->bin && core->bin->cur && core->bin->cur->BO) {
RBinReloc *br;
auto relocs = core->bin->cur->BO->relocs;
//// RBIter iter;
if (!relocs) {
return ret;
}
#if R2_ABIVERSION >= 136
R_VEC_FOREACH(relocs, br)
#else
RRBNode *iter;
r_crbtree_foreach(relocs, iter, RBinReloc, br)
#endif
{
RelocDescription reloc;
@@ -3603,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();
@@ -3763,6 +3990,10 @@ QList<BinClassDescription> IaitoCore::getAllClassesFromBin()
BinClassMethodDescription meth;
meth.name = methObject[RJsonKey::name].toString();
meth.mangledName = methObject[QStringLiteral("rawname")].toString();
if (meth.name.isEmpty()) {
meth.name = meth.mangledName;
}
meth.addr = methObject[RJsonKey::addr].toVariant().toULongLong();
cls.methods << meth;
@@ -4064,24 +4295,198 @@ QList<ResourcesDescription> IaitoCore::getAllResources()
CORE_LOCK();
QList<ResourcesDescription> resources;
QJsonArray resourcesArray = cmdj("iRj").array();
for (const QJsonValue value : resourcesArray) {
QJsonObject resourceObject = value.toObject();
#if IAITO_HAS_R_BIN_RESOURCES
RBinFile *binFile = core && core->bin ? r_bin_cur(core->bin) : nullptr;
RVecRBinResource *binResources = binFile ? r_bin_file_get_resources(binFile) : nullptr;
if (binResources) {
RBinResource *resource;
R_VEC_FOREACH(binResources, resource)
{
ResourcesDescription res;
res.name = QString::fromUtf8(resource->name ? resource->name : "");
res.type = QString::fromUtf8(resource->type ? resource->type : "");
res.language = QString::fromUtf8(resource->language ? resource->language : "");
res.timestamp = QString::fromUtf8(resource->timestamp ? resource->timestamp : "");
res.origin = QString::fromUtf8(resource->origin ? resource->origin : "");
res.vaddr = resource->vaddr;
res.paddr = resource->paddr;
res.size = resource->size;
res.id = resource->id;
res.index = resource->index;
res.typeId = resource->type_id;
res.languageId = resource->language_id;
res.codepage = resource->codepage;
res.named = resource->named;
res.hasExtendedMetadata = true;
resources.append(res);
}
}
#else
const QJsonArray resourcesArray = cmdj(QStringLiteral("iRj")).array();
for (const QJsonValue &value : resourcesArray) {
const QJsonObject resourceObject = value.toObject();
ResourcesDescription res;
res.name = resourceObject[RJsonKey::name].toString();
res.vaddr = resourceObject[RJsonKey::vaddr].toVariant().toULongLong();
res.index = resourceObject[RJsonKey::index].toVariant().toULongLong();
res.index = resourceObject[RJsonKey::index].toVariant().toUInt();
res.type = resourceObject[RJsonKey::type].toString();
res.size = resourceObject[RJsonKey::size].toVariant().toULongLong();
res.lang = resourceObject[RJsonKey::lang].toString();
resources << res;
res.language = resourceObject[RJsonKey::lang].toString();
res.timestamp = resourceObject[QStringLiteral("timestamp")].toString();
resources.append(res);
}
#endif
return resources;
}
bool IaitoCore::dumpResource(
const ResourcesDescription &resource, const QString &fileName, QString *errorMessage)
{
#if IAITO_HAS_R_BIN_RESOURCES
CORE_LOCK();
RBinFile *binFile = core && core->bin ? r_bin_cur(core->bin) : nullptr;
RVecRBinResource *binResources = binFile ? r_bin_file_get_resources(binFile) : nullptr;
if (!binResources) {
if (errorMessage) {
*errorMessage = tr("No binary resources are available.");
}
return false;
}
RBinResource *binResource = nullptr;
RBinResource *candidate;
R_VEC_FOREACH(binResources, candidate)
{
if (candidate->index == resource.index && candidate->paddr == resource.paddr
&& candidate->size == resource.size) {
binResource = candidate;
break;
}
}
if (!binResource) {
if (errorMessage) {
*errorMessage = tr("The selected resource is no longer available.");
}
return false;
}
#if R2_ABIVERSION >= 125
RBuffer *buffer = r_bin_file_get_resource_data(binFile, binResource, false);
#else
RBuffer *buffer = r_bin_file_get_resource_data(binFile, binResource);
#endif
if (!buffer) {
if (errorMessage) {
*errorMessage = tr("Unable to read the selected resource.");
}
return false;
}
QSaveFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
if (errorMessage) {
*errorMessage = file.errorString();
}
r_unref(buffer);
return false;
}
constexpr int ChunkSize = 64 * 1024;
QByteArray chunk(ChunkSize, '\0');
const ut64 bufferSize = r_buf_size(buffer);
ut64 offset = 0;
while (offset < bufferSize) {
const int requested = static_cast<int>(qMin<ut64>(ChunkSize, bufferSize - offset));
const st64 readSize
= r_buf_read_at(buffer, offset, reinterpret_cast<ut8 *>(chunk.data()), requested);
if (readSize <= 0) {
file.cancelWriting();
if (errorMessage) {
*errorMessage = tr("Unable to read the selected resource.");
}
r_unref(buffer);
return false;
}
qint64 written = 0;
while (written < readSize) {
const qint64 writeSize = file.write(chunk.constData() + written, readSize - written);
if (writeSize <= 0) {
file.cancelWriting();
if (errorMessage) {
*errorMessage = file.errorString();
}
r_unref(buffer);
return false;
}
written += writeSize;
}
offset += static_cast<ut64>(readSize);
}
const bool committed = file.commit();
if (!committed && errorMessage) {
*errorMessage = file.errorString();
}
r_unref(buffer);
return committed;
#else
CORE_LOCK();
if (resource.vaddr == RVA_INVALID) {
if (errorMessage) {
*errorMessage = tr("The selected resource is not readable.");
}
return false;
}
QSaveFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
if (errorMessage) {
*errorMessage = file.errorString();
}
return false;
}
constexpr int ChunkSize = 64 * 1024;
QByteArray chunk(ChunkSize, '\0');
ut64 offset = 0;
while (offset < resource.size) {
const int requested = static_cast<int>(qMin<ut64>(ChunkSize, resource.size - offset));
if (!r_io_read_at(
core->io,
resource.vaddr + offset,
reinterpret_cast<ut8 *>(chunk.data()),
requested)) {
file.cancelWriting();
if (errorMessage) {
*errorMessage = tr("Unable to read the selected resource.");
}
return false;
}
qint64 written = 0;
while (written < requested) {
const qint64 writeSize = file.write(chunk.constData() + written, requested - written);
if (writeSize <= 0) {
file.cancelWriting();
if (errorMessage) {
*errorMessage = file.errorString();
}
return false;
}
written += writeSize;
}
offset += static_cast<ut64>(requested);
}
const bool committed = file.commit();
if (!committed && errorMessage) {
*errorMessage = file.errorString();
}
return committed;
#endif
}
QList<VTableDescription> IaitoCore::getAllVTables()
{
CORE_LOCK();
@@ -4515,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());
}
@@ -4613,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)) {
@@ -4620,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"));
@@ -4641,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())));
@@ -4660,6 +5173,18 @@ void IaitoCore::deleteProject(const QString &name)
cmdRaw0("'P-" + name);
}
bool IaitoCore::isProjectDirty()
{
CORE_LOCK();
return R_DIRTY_CHECK(core->config) || R_DIRTY_CHECK(core->anal) || R_DIRTY_CHECK(core->flags);
}
void IaitoCore::markProjectClean()
{
CORE_LOCK();
r_core_project_undirty(core);
}
bool IaitoCore::isProjectNameValid(const QString &name)
{
// see is_valid_project_name() in libr/core/project.
+20
View File
@@ -4,6 +4,7 @@
#define MONOTHREAD 1
#include "common/BasicInstructionHighlighter.h"
#include "common/ExceptionInfo.h"
#include "core/IaitoCommon.h"
#include "core/IaitoDescriptions.h"
@@ -357,6 +358,7 @@ public:
/* Math functions */
ut64 math(const QString &expr);
bool tryMath(const QString &expr, ut64 &value);
ut64 num(const QString &expr);
QString itoa(ut64 num, int rdx = 16);
@@ -463,6 +465,7 @@ public:
void suspendDebug();
private:
int exceptionJsonSupport = -1;
void finishStopDebug();
public:
@@ -552,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();
@@ -564,6 +569,8 @@ public:
void openProject(const QString &name);
void saveProject(const QString &name);
void deleteProject(const QString &name);
bool isProjectDirty();
void markProjectClean();
static bool isProjectNameValid(const QString &name);
/* Widgets */
@@ -577,6 +584,7 @@ public:
QList<RDebugPluginDescription> getRDebugPluginDescriptions();
QList<RMutaPluginDescription> getRMutaPluginDescriptions();
QList<FunctionDescription> getAllFunctions();
QList<ExceptionRegion> getExceptionRegions(RVA function = RVA_INVALID);
QList<ImportDescription> getAllImports();
QList<ExportDescription> getAllExports();
QList<SymbolDescription> getAllSymbols();
@@ -587,12 +595,24 @@ 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();
QList<BinClassDescription> getAllClassesFromBin();
QList<BinClassDescription> getAllClassesFromFlags();
QList<ResourcesDescription> getAllResources();
bool dumpResource(
const ResourcesDescription &resource,
const QString &fileName,
QString *errorMessage = nullptr);
QList<VTableDescription> getAllVTables();
/**
+23 -3
View File
@@ -98,6 +98,7 @@ struct SymbolDescription
QString bind;
QString type;
QString name;
QString demangledName;
};
struct CommentDescription
@@ -137,6 +138,14 @@ struct FlagDescription
QString realname;
};
struct GlobalVariableDescription
{
RVA address = RVA_INVALID;
RVA size = 0;
QString name;
QString type;
};
struct SectionDescription
{
RVA vaddr = 0;
@@ -264,6 +273,7 @@ struct BinClassBaseClassDescription
struct BinClassMethodDescription
{
QString name;
QString mangledName;
RVA addr = RVA_INVALID;
st64 vtableOffset = -1;
};
@@ -310,10 +320,19 @@ struct ResourcesDescription
{
QString name;
RVA vaddr = 0;
ut64 index = 0;
QString type;
RVA paddr = RVA_INVALID;
ut64 size = 0;
QString lang;
ut64 id = UT64_MAX;
ut32 index = 0;
ut32 typeId = UT32_MAX;
ut32 languageId = 0;
ut32 codepage = 0;
bool named = false;
bool hasExtendedMetadata = false;
QString type;
QString language;
QString timestamp;
QString origin;
};
struct VTableDescription
@@ -416,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)
+513 -80
View File
@@ -48,6 +48,7 @@
#include "widgets/ConsoleWidget.h"
#include "widgets/CustomCommandWidget.h"
#include "widgets/Dashboard.h"
#include "widgets/DataAnalysisWidget.h"
#include "widgets/DebugActions.h"
#include "widgets/DecompilerWidget.h"
#include "widgets/DisassemblerGraphView.h"
@@ -58,11 +59,14 @@
#include "widgets/FilesystemWidget.h"
#include "widgets/FlagsWidget.h"
#include "widgets/FunctionsWidget.h"
#include "widgets/GlobalVariablesWidget.h"
#include "widgets/GraphView.h"
#include "widgets/GraphWidget.h"
#include "widgets/HeadersWidget.h"
#include "widgets/HexPatWidget.h"
#include "widgets/HexdumpWidget.h"
#include "widgets/ImportsWidget.h"
#include "widgets/InterfacesWidget.h"
#include "widgets/MapsWidget.h"
#include "widgets/MemoryMapWidget.h"
#include "widgets/Omnibar.h"
@@ -98,6 +102,7 @@
#include <QApplication>
#include <QComboBox>
#include <QCompleter>
#include <QCryptographicHash>
#include <QCursor>
#include <QDebug>
#include <QDesktopServices>
@@ -160,6 +165,21 @@
#include <QGraphicsScene>
#include <QGraphicsView>
static void addHashData(QCryptographicHash &hash, const void *data, int size)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
hash.addData(QByteArrayView(reinterpret_cast<const char *>(data), size));
#else
hash.addData(reinterpret_cast<const char *>(data), size);
#endif
}
template<typename T>
static void addHashValue(QCryptographicHash &hash, const T &value)
{
addHashData(hash, &value, static_cast<int>(sizeof(value)));
}
template<class T>
T *getNewInstance(MainWindow *m)
{
@@ -684,6 +704,49 @@ QString r2QuotedFileArg(const QString &path)
return QStringLiteral("\"%1\"").arg(result);
}
QString classdumpFileExtension(const QString &language)
{
if (language == QLatin1String("java")) {
return QStringLiteral("java");
}
if (language == QLatin1String("kotlin")) {
return QStringLiteral("kt");
}
if (language == QLatin1String("swift")) {
return QStringLiteral("swift");
}
if (language == QLatin1String("cxx") || language == QLatin1String("c++")
|| language == QLatin1String("objc") || language == QLatin1String("objective-c")
|| language == QLatin1String("c")) {
return QStringLiteral("h");
}
return QStringLiteral("txt");
}
QStringList classdumpLanguages()
{
QStringList languages;
const QString output = Core()->cmdRaw("iccl").trimmed();
if (!output.contains(QStringLiteral("invalid"), Qt::CaseInsensitive)
&& !output.contains(QStringLiteral("unknown"), Qt::CaseInsensitive)
&& !output.contains(QStringLiteral("0x"), Qt::CaseInsensitive)
&& !output.contains(QLatin1Char('{')) && !output.contains(QLatin1Char(';'))) {
static const QRegularExpression languagePattern(QStringLiteral("^[A-Za-z0-9_+.-]+$"));
for (const QString &line :
output.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts)) {
const QString language = line.trimmed();
if (languagePattern.match(language).hasMatch() && !language.startsWith(QLatin1Char('-'))
&& !languages.contains(language)) {
languages << language;
}
}
}
if (languages.isEmpty()) {
languages << QStringLiteral("cxx") << QStringLiteral("java") << QStringLiteral("swift");
}
return languages;
}
QString buildAnalyzePluginStatusText(
const QString &name, const QString &args, const QString &description)
{
@@ -916,7 +979,7 @@ void MainWindow::initUI()
widget->setFloating(true);
widget->raiseMemoryWidget();
});
QAction *assemblerAction = new QAction(tr("Assembler..."), this);
QAction *assemblerAction = new QAction(tr("Assembler"), this);
setAppMenuIcon(this, assemblerAction, AppMenuIcon::Assembler, QColor(216, 88, 64));
assemblerAction->setToolTip(tr("Assemble and disassemble bytes at an address"));
assemblerAction->setStatusTip(
@@ -937,7 +1000,7 @@ void MainWindow::initUI()
dialog->activateWindow();
});
if (!Core()->cmd("Lc~^r2mcp").trimmed().isEmpty()) {
QAction *r2mcpAction = new QAction(tr("r2mcp..."), this);
QAction *r2mcpAction = new QAction(tr("r2mcp"), this);
setAppMenuIcon(this, r2mcpAction, AppMenuIcon::Web, QColor(67, 160, 71));
r2mcpAction->setToolTip(tr("Manage the r2mcp HTTP MCP server"));
r2mcpAction->setStatusTip(tr("Start, stop, and configure the r2mcp HTTP MCP server"));
@@ -961,16 +1024,8 @@ void MainWindow::initUI()
});
ui->actionCommitChanges->setEnabled(false);
connect(Core(), &IaitoCore::ioCacheChanged, ui->actionCommitChanges, &QAction::setEnabled);
connect(ui->actionUndoWrite, &QAction::triggered, this, [this]() {
core->cmdRaw("wcu");
emit core->refreshCodeViews();
updateWriteUndoRedoActions();
});
connect(ui->actionRedoWrite, &QAction::triggered, this, [this]() {
core->cmdRaw("wcU");
emit core->refreshCodeViews();
updateWriteUndoRedoActions();
});
connect(ui->actionUndoWrite, &QAction::triggered, this, &MainWindow::undoWriteCache);
connect(ui->actionRedoWrite, &QAction::triggered, this, &MainWindow::redoWriteCache);
updateWriteUndoRedoActions();
m_dockManager->constructorMap().insert(GraphWidget::getWidgetType(), getNewInstance<GraphWidget>);
@@ -1020,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);
@@ -1034,6 +1117,7 @@ void MainWindow::initUI()
connect(core, &IaitoCore::projectSaved, this, &MainWindow::projectSaved);
connect(core, &IaitoCore::toggleDebugView, this, &MainWindow::toggleDebugView);
connect(core, &IaitoCore::debugTaskStateChanged, this, &MainWindow::resumePendingClose);
connect(core, &IaitoCore::newMessage, this->consoleDock, &ConsoleWidget::addOutput);
connect(core, &IaitoCore::newDebugMessage, this->consoleDock, &ConsoleWidget::addDebugOutput);
connect(
@@ -1098,6 +1182,14 @@ void MainWindow::initUI()
addDockWidget(Qt::DockWidgetArea::TopDockWidgetArea, r2aiDock);
m_dockManager->addPluginDock(r2aiDock);
}
{
auto *hexPatDock = new HexPatWidget(this);
addWidget(hexPatDock);
ui->menuTools->insertAction(ui->actionStart_Web_Server, hexPatDock->toggleViewAction());
addDockWidget(Qt::DockWidgetArea::TopDockWidgetArea, hexPatDock);
m_dockManager->addPluginDock(hexPatDock);
connect(ui->menuTools, &QMenu::aboutToShow, hexPatDock, &HexPatWidget::updateAvailability);
}
// Check if plugins are loaded and display tooltips accordingly
ui->menuWindows->setToolTipsVisible(true);
@@ -1155,6 +1247,7 @@ void MainWindow::initUI()
connectMenuStatusTips(ui->menuEdit);
connectMenuStatusTips(ui->menuCode);
connectMenuStatusTips(ui->menuAnalysis);
connectMenuStatusTips(ui->menuData);
connectMenuStatusTips(ui->menuTools);
connectMenuStatusTips(ui->menuPlugins);
@@ -1180,7 +1273,7 @@ void MainWindow::initToolBar()
refreshEditIcon();
connect(Config(), &Configuration::interfaceThemeChanged, editBtn, refreshEditIcon);
QMenu *editMenu = new QMenu(editBtn);
editMenu->addAction(tr("Rename function..."), this, [this]() {
editMenu->addAction(tr("Rename function…"), this, [this]() {
const RVA off = Core()->getOffset();
RAnalFunction *fcn = Core()->functionIn(off);
if (!fcn) {
@@ -1201,7 +1294,7 @@ void MainWindow::initToolBar()
Core()->renameFunction(fcn->addr, newName);
}
});
editMenu->addAction(tr("Rename / set flag..."), this, [this]() {
editMenu->addAction(tr("Rename / set flag…"), this, [this]() {
FlagDialog dialog(Core()->getOffset(), 1, this);
dialog.exec();
});
@@ -1218,7 +1311,7 @@ void MainWindow::initToolBar()
});
editMenu->addAction(tr("Delete flag"), this, []() { Core()->delFlag(Core()->getOffset()); });
editMenu->addSeparator();
editMenu->addAction(tr("Add / edit comment..."), this, [this]() {
editMenu->addAction(tr("Add / edit comment…"), this, [this]() {
CommentsDialog::addOrEditComment(Core()->getOffset(), this);
});
editMenu->addSeparator();
@@ -1293,7 +1386,7 @@ void MainWindow::initToolBar()
DebugActions *debugActions = new DebugActions(ui->mainToolBar, this);
// Debug menu
auto debugViewAction = ui->menuDebug->addAction(tr("View"));
auto debugViewAction = ui->menuDebug->addAction(tr("Debug Panels"));
setAppMenuIcon(this, debugViewAction, AppMenuIcon::View, QColor(126, 87, 194));
debugViewAction->setMenu(ui->menuAddDebugWidgets);
ui->menuDebug->addSeparator();
@@ -1490,14 +1583,15 @@ 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),
relocsDock = new RelocsWidget(this),
resourcesDock = new ResourcesWidget(this),
sdbDock = new SdbWidget(this),
@@ -1524,8 +1618,20 @@ void MainWindow::initDocks()
r2GraphDock = new R2GraphWidget(this),
callGraphDock = new CallGraphWidget(this, false),
globalCallGraphDock = new CallGraphWidget(this, true),
};
QList<IaitoDockWidget *> dataDocks = {
flagsDock = new FlagsWidget(this),
#if R2_ABIVERSION >= 154
// avgj and C type metadata require post-6.2.4 radare2.
globalVariablesDock = new GlobalVariablesWidget(this),
#endif
dataAnalysisDock = new DataAnalysisWidget(this),
zoomDock = new ZoomWidget(this),
};
dataAnalysisDock->toggleViewAction()->setText(tr("Analysis"));
connect(dataAnalysisDock, &QWidget::windowTitleChanged, this, [this]() {
dataAnalysisDock->toggleViewAction()->setText(tr("Analysis"));
});
auto makeActionList = [this](QList<IaitoDockWidget *> docks) {
QList<QAction *> result;
@@ -1541,10 +1647,11 @@ void MainWindow::initDocks()
return result;
};
searchDock->toggleViewAction()->setText(tr("Search..."));
searchDock->toggleViewAction()->setText(tr("Search"));
ui->menuCode->addSeparator();
ui->menuCode->addActions(makeActionList(codeDocks));
ui->menuAnalysis->addActions(makeActionList(analysisDocks));
ui->menuData->addActions(makeActionList(dataDocks));
QList<IaitoDockWidget *> windowDocks2 = {
consoleDock,
};
@@ -1558,19 +1665,17 @@ void MainWindow::initDocks()
ui->menuView->insertSeparator(ui->menuZoom->menuAction());
ui->menuAddInfoWidgets->addActions(makeActionList(infoDocks));
ui->menuAddIoWidgets->addActions(makeActionList(ioDocks));
QAction *actionFilesystem = new QAction("Filesystem", this);
connect(actionFilesystem, &QAction::triggered, this, [this]() {
if (!filesystemDock) {
filesystemDock = new FilesystemWidget(this);
addExtraWidget(filesystemDock);
}
QAction *actionSourceFiles = new QAction(tr("Source Files"), this);
connect(actionSourceFiles, &QAction::triggered, this, [this]() {
filesystemDock->show();
filesystemDock->raise();
filesystemDock->showSourceFiles();
});
ui->menuAddIoWidgets->addAction(actionFilesystem);
ui->menuCode->addAction(actionSourceFiles);
ui->menuAddDebugWidgets->addActions(makeActionList(debugDocks));
auto uniqueDocks = mainViewDocks + windowDocks2 + infoDocks + analysisDocks;
uniqueDocks += codeDocks + ioDocks + debugDocks;
uniqueDocks += codeDocks + ioDocks + debugDocks + dataDocks;
uniqueDocks.append(overviewDock);
for (auto dock : uniqueDocks) {
if (dock) { // ignore nullptr used as separators
@@ -1624,6 +1729,7 @@ void MainWindow::applyTopLevelMenuIcons()
setAppMenuIcon(this, ui->menuAddInfoWidgets, AppMenuIcon::Info, viewColor);
setAppMenuIcon(this, ui->menuAddIoWidgets, AppMenuIcon::Storage, viewColor);
setAppMenuIcon(this, ui->menuAnalysis, AppMenuIcon::Analysis, analysisColor);
setAppMenuIcon(this, ui->menuData, AppMenuIcon::Storage, viewColor);
setAppMenuIcon(this, ui->menuAddAnother, AppMenuIcon::Extra, viewColor);
setAppMenuIcon(this, ui->menuZoom, AppMenuIcon::ZoomIn, viewColor);
setAppMenuIcon(this, ui->actionZoomIn, AppMenuIcon::ZoomIn, viewColor);
@@ -2056,11 +2162,15 @@ void MainWindow::finalizeOpen()
Config()->adjustColorThemeDarkness();
setViewLayout(getViewLayout(core->currentlyDebugging ? LAYOUT_DEBUG : LAYOUT_DEFAULT));
// Raise the preferred memory view now and again after Qt's deferred layout
// pass: restoreState() re-applies the saved active tab on the next event
// loop turn, so a synchronous raise alone gets overridden.
// Raise the preferred left and memory views now and again after Qt's deferred layout
// pass: restoreState() re-applies the saved active tabs on the next event loop turn,
// so a synchronous raise alone gets overridden.
functionsDock->raise();
raiseDefaultMemoryWidget();
QTimer::singleShot(0, this, [this]() { raiseDefaultMemoryWidget(); });
QTimer::singleShot(0, this, [this]() {
functionsDock->raise();
raiseDefaultMemoryWidget();
});
consoleDock->hide();
// Wire up status bar updates for all seekable views
// Disassembly widgets
@@ -2101,6 +2211,10 @@ void MainWindow::finalizeOpen()
}
}
// Initial loading and analysis establish the session baseline. Only changes made after the
// file is ready should require a project-save decision when closing.
core->markProjectClean();
// Signal that the UI is ready for background tasks to run safely.
// Use a single-shot queued invocation so that uiReady is emitted after the
// current initialization events are processed (avoids re-entrancy into
@@ -2160,46 +2274,131 @@ void MainWindow::closeEvent(QCloseEvent *event)
return;
}
if (this->filename == "") {
QMainWindow::closeEvent(event);
return;
}
// Check if there are uncommitted changes
if (!ioModesController.askCommitUnsavedChanges()) {
// if false, Cancel was chosen
event->ignore();
return;
}
QMessageBox::StandardButton ret = QMessageBox::question(
this,
APPNAME,
tr("Do you really want to exit?\nSave your project before closing!"),
(QMessageBox::StandardButtons) (QMessageBox::Save | QMessageBox::Discard
| QMessageBox::Cancel));
if (ret == QMessageBox::Cancel) {
event->ignore();
return;
}
if (ret == QMessageBox::Discard) {
event->ignore();
QMainWindow::closeEvent(event);
return;
}
if (ret == QMessageBox::Save && !saveProject(true)) {
event->ignore();
return;
}
// discard (do not save)
if (core->currentlyDebugging) {
core->stopDebug();
} else {
saveSettings();
QMainWindow::closeEvent(event);
return;
}
if (pendingCloseAction != PendingCloseAction::None) {
if (core->currentlyDebugging) {
event->ignore();
return;
}
if (!completePendingClose()) {
event->ignore();
return;
}
QMainWindow::closeEvent(event);
return;
}
const bool projectDirty = core->isProjectDirty();
const bool cachedWrites = !ioModesController.allChangesComitted();
const bool debugging = core->currentlyDebugging;
const bool hasSavableChanges = projectDirty || cachedWrites;
if (!hasSavableChanges && !debugging) {
saveSettings();
QMainWindow::closeEvent(event);
return;
}
QStringList sessionState;
if (projectDirty) {
sessionState << tr("- Project changes have not been saved.");
}
if (cachedWrites) {
sessionState << tr("- Cached writes have not been committed to the file.");
}
if (debugging) {
sessionState << tr("- A debugging session is active and will be stopped.");
}
QMessageBox messageBox(this);
messageBox.setIcon(QMessageBox::Question);
messageBox.setWindowTitle(APPNAME);
messageBox.setText(
hasSavableChanges ? tr("Save session changes before closing?")
: tr("Stop debugging and close?"));
messageBox.setInformativeText(
tr("The current session contains:\n%1").arg(sessionState.join(QLatin1Char('\n'))));
if (hasSavableChanges) {
messageBox.setStandardButtons(
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
messageBox.setDefaultButton(QMessageBox::Save);
} else {
messageBox.setStandardButtons(QMessageBox::Close | QMessageBox::Cancel);
messageBox.setDefaultButton(QMessageBox::Close);
}
const auto choice = static_cast<QMessageBox::StandardButton>(messageBox.exec());
if (choice == QMessageBox::Cancel) {
event->ignore();
return;
}
if (choice == QMessageBox::Save && cachedWrites) {
core->commitWriteCache();
if (!ioModesController.allChangesComitted()) {
QMessageBox::warning(
this,
tr("Commit failed"),
tr("The cached writes could not be committed. The session will remain open."));
event->ignore();
return;
}
} else if (choice == QMessageBox::Discard && cachedWrites) {
core->cmdRaw("wcr");
emit core->refreshCodeViews();
}
pendingCloseAction = choice == QMessageBox::Save ? PendingCloseAction::Save
: PendingCloseAction::Discard;
pendingProjectSave = choice == QMessageBox::Save && projectDirty;
if (debugging) {
core->stopDebug();
event->ignore();
resumePendingClose();
return;
}
if (!completePendingClose()) {
event->ignore();
return;
}
QMainWindow::closeEvent(event);
}
bool MainWindow::completePendingClose()
{
const PendingCloseAction action = pendingCloseAction;
const bool saveProjectChanges = pendingProjectSave;
pendingCloseAction = PendingCloseAction::None;
pendingProjectSave = false;
if (action == PendingCloseAction::Save && saveProjectChanges && !saveProject()) {
return false;
}
saveSettings();
return true;
}
void MainWindow::resumePendingClose()
{
if (pendingCloseAction == PendingCloseAction::None || core->currentlyDebugging
|| pendingCloseScheduled) {
return;
}
pendingCloseScheduled = true;
QTimer::singleShot(0, this, [this]() {
pendingCloseScheduled = false;
close();
});
}
void MainWindow::paintEvent(QPaintEvent *event)
{
QMainWindow::paintEvent(event);
@@ -2286,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);
@@ -2313,6 +2516,8 @@ void MainWindow::restoreDocks()
filesDock->hide();
binariesDock->hide();
tabifyDockWidget(dashboardDock, zoomDock);
tabifyDockWidget(dashboardDock, dataAnalysisDock);
dataAnalysisDock->hide();
for (const auto &it : m_dockManager->docks()) {
// Check whether or not current widgets is graph, hexdump or disasm
if (isExtraMemoryWidget(it)) {
@@ -2391,7 +2596,8 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type)
for (auto &dock : m_dockManager->docks()) {
if (auto memoryWidget = qobject_cast<MemoryDockWidget *>(dock)) {
if (memoryWidget->getType() == type && memoryWidget->getSeekable()->isSynchronized()) {
memoryWidget->tryRaiseMemoryWidget();
// Explicit view requests should also open an empty graph.
memoryWidget->raiseMemoryWidget();
return;
}
}
@@ -2400,17 +2606,23 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type)
memoryDockWidget->raiseMemoryWidget();
}
void MainWindow::showDataAnalysis(RVA start, RVA end)
{
dataAnalysisDock->showRange(start, end);
}
void MainWindow::gotoOffset(const QString &offset)
{
if (offset.isEmpty()) {
ut64 address;
if (!core->tryMath(offset, address)) {
return;
}
if (auto memoryWidget = getLastMemoryWidget()) {
memoryWidget->getSeekable()->seek(Core()->math(offset));
memoryWidget->getSeekable()->seek(address);
memoryWidget->raiseMemoryWidget();
return;
}
Core()->seekAndShow(offset);
Core()->seekAndShow(address);
}
QMenu *MainWindow::createShowInMenu(QWidget *parent, RVA address, AddressTypeHint addressType)
@@ -2442,6 +2654,16 @@ QMenu *MainWindow::createShowInMenu(QWidget *parent, RVA address, AddressTypeHin
}
QAction *action = new QAction(memoryWidget->windowTitle(), menu);
connect(action, &QAction::triggered, this, [memoryWidget, address]() {
if (auto *analysis = qobject_cast<DataAnalysisWidget *>(memoryWidget)) {
if (Core()->hasAddressRangeSelection()
&& Core()->getAddressRangeSelectionStart() <= address
&& address <= Core()->getAddressRangeSelectionEnd()) {
analysis->showRange(
Core()->getAddressRangeSelectionStart(),
Core()->getAddressRangeSelectionEnd());
return;
}
}
memoryWidget->getSeekable()->seek(address);
memoryWidget->raiseMemoryWidget();
});
@@ -2569,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()) {
@@ -2604,6 +2851,7 @@ void MainWindow::updateHistoryMenu(QMenu *menu, bool redo)
QFont font;
font.setBold(true);
action->setFont(font);
action->setEnabled(false);
}
}
}
@@ -2612,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]() {
@@ -2837,9 +3085,116 @@ void MainWindow::updateSaveProjectAction()
void MainWindow::updateWriteUndoRedoActions()
{
const bool cacheEnabled = core->isIOCacheEnabled();
const bool hasUndo = cacheEnabled && !core->cmdj("wcj").array().isEmpty();
ui->actionUndoWrite->setEnabled(hasUndo);
ui->actionRedoWrite->setEnabled(false);
bool hasUndo = false;
const QByteArray fingerprint = currentWriteCacheFingerprint(&hasUndo);
if (!writeRedoStack.isEmpty() && (!cacheEnabled || fingerprint != writeRedoCacheFingerprint)) {
writeRedoStack.clear();
writeRedoCacheFingerprint.clear();
}
ui->actionUndoWrite->setEnabled(cacheEnabled && hasUndo);
ui->actionRedoWrite->setEnabled(cacheEnabled && !writeRedoStack.isEmpty());
}
QByteArray MainWindow::currentWriteCacheFingerprint(bool *hasUndo)
{
QCryptographicHash hash(QCryptographicHash::Sha256);
RCoreLocked locked = core->core();
RIO *io = locked->io;
quintptr descriptor = reinterpret_cast<quintptr>(io->desc);
addHashValue(hash, descriptor);
addHashValue(hash, io->cache.mode);
int layerCount = r_list_length(io->cache.layers);
addHashValue(hash, layerCount);
RIOCacheLayer *activeLayer = static_cast<RIOCacheLayer *>(r_list_last(io->cache.layers));
if (hasUndo) {
*hasUndo = activeLayer && RVecRIOCacheItemPtr_length(activeLayer->vec) > 0;
}
RListIter *layerIterator;
void *layerData;
r_list_foreach(io->cache.layers, layerIterator, layerData)
{
RIOCacheLayer *layer = static_cast<RIOCacheLayer *>(layerData);
size_t itemCount = RVecRIOCacheItemPtr_length(layer->vec);
addHashValue(hash, itemCount);
RIOCacheItem **itemIterator;
R_VEC_FOREACH(layer->vec, itemIterator)
{
RIOCacheItem *item = *itemIterator;
quint64 address = r_itv_begin(item->itv);
quint64 size = r_itv_size(item->itv);
addHashValue(hash, address);
addHashValue(hash, size);
addHashValue(hash, item->written);
addHashData(hash, item->data, static_cast<int>(size));
addHashData(hash, item->odata, static_cast<int>(size));
}
}
return hash.result();
}
void MainWindow::undoWriteCache()
{
updateWriteUndoRedoActions();
if (!ui->actionUndoWrite->isEnabled()) {
return;
}
WriteRedoEntry entry;
bool undone = false;
{
RCoreLocked locked = core->core();
RIOCacheLayer *layer = static_cast<RIOCacheLayer *>(r_list_last(locked->io->cache.layers));
if (layer && R_VEC_START_ITER(layer->vec) != R_VEC_END_ITER(layer->vec)) {
RIOCacheItem *item = *(R_VEC_END_ITER(layer->vec) - 1);
const int size = static_cast<int>(r_itv_size(item->itv));
entry.address = r_itv_begin(item->itv);
entry.data = QByteArray(reinterpret_cast<const char *>(item->data), size);
undone = r_io_cache_undo(locked->io);
if (undone) {
r_core_block_read(locked);
}
}
}
if (!undone) {
return;
}
writeRedoStack.append(entry);
writeRedoCacheFingerprint = currentWriteCacheFingerprint();
emit core->refreshCodeViews();
updateWriteUndoRedoActions();
}
void MainWindow::redoWriteCache()
{
updateWriteUndoRedoActions();
if (!ui->actionRedoWrite->isEnabled()) {
return;
}
const WriteRedoEntry &entry = writeRedoStack.constLast();
bool redone = false;
{
RCoreLocked locked = core->core();
redone = r_io_write_at(
locked->io,
entry.address,
reinterpret_cast<const ut8 *>(entry.data.constData()),
entry.data.size());
if (redone) {
r_core_block_read(locked);
}
}
if (!redone) {
QMessageBox::warning(this, tr("Redo Write"), tr("The cached write could not be restored."));
return;
}
writeRedoStack.removeLast();
writeRedoCacheFingerprint = currentWriteCacheFingerprint();
emit core->refreshCodeViews();
updateWriteUndoRedoActions();
}
void MainWindow::enableDebugWidgetsMenu(bool enable)
@@ -2856,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;
@@ -2879,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();
@@ -2946,6 +3302,10 @@ void MainWindow::setViewLayout(const IaitoLayout &layout)
// other
}
for (auto dock : newDocks) {
// Data analysis scans are opt-in, including when upgrading a saved layout.
if (dock == dataAnalysisDock) {
continue;
}
dockOnMainArea(dock);
// Show any new docks by default.
// Showing new builtin docks helps discovering features added in
@@ -3250,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()
@@ -3712,6 +4091,43 @@ void MainWindow::on_actionDump_triggered()
.arg(QDir::toNativeSeparators(filePath)));
}
void MainWindow::on_actionExportClassdump_triggered()
{
QStringList filters;
QMap<QString, QString> languageByFilter;
for (const QString &language : classdumpLanguages()) {
const QString extension = classdumpFileExtension(language);
const QString filter = tr("%1 class dump (*.%2)").arg(language, extension);
filters << filter;
languageByFilter[filter] = language;
}
filters << tr("All files (*)");
QFileDialog dialog(this, tr("Export class dump"));
dialog.setAcceptMode(QFileDialog::AcceptSave);
dialog.setFileMode(QFileDialog::AnyFile);
dialog.setNameFilters(filters);
dialog.selectFile(QStringLiteral("classdump.%1")
.arg(classdumpFileExtension(languageByFilter.value(filters.first()))));
dialog.setDefaultSuffix(classdumpFileExtension(languageByFilter.value(filters.first())));
if (!dialog.exec()) {
return;
}
const QString selectedFilter = dialog.selectedNameFilter();
const QString language
= languageByFilter.value(selectedFilter, languageByFilter.value(filters.first()));
QFile file(dialog.selectedFiles().first());
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, tr("Error"), tr("Cannot open file for writing."));
return;
}
QTextStream fileOut(&file);
fileOut << Core()->cmd(QStringLiteral("icc %1").arg(language));
}
void MainWindow::on_actionExportDWARF_triggered()
{
QFileDialog dialog(this, tr("Export DWARF information"));
@@ -3817,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;
@@ -3850,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;
}
@@ -3870,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;
}
}
+28
View File
@@ -28,12 +28,14 @@ class AsciiHighlighter;
class VisualNavbar;
class FunctionsWidget;
class ImportsWidget;
class InterfacesWidget;
class ExportsWidget;
class SymbolsWidget;
class RelocsWidget;
class CommentsWidget;
class StringsWidget;
class FlagsWidget;
class GlobalVariablesWidget;
class Dashboard;
class QLineEdit;
class QRubberBand;
@@ -65,6 +67,7 @@ class FilesWidget;
class FilesystemWidget;
class BinariesWidget;
class ZoomWidget;
class DataAnalysisWidget;
class XrefsWidget;
class RefsWidget;
class CodeInfoWidget;
@@ -150,6 +153,7 @@ public:
QString getUniqueObjectName(const QString &widgetType) const;
void showMemoryWidget();
void showMemoryWidget(MemoryWidgetType type);
void showDataAnalysis(RVA start, RVA end);
void gotoOffset(const QString &offset);
enum class AddressTypeHint { Function, Data, Unknown };
QMenu *createShowInMenu(
@@ -260,6 +264,7 @@ private slots:
void on_actionExport_as_code_triggered();
void on_actionDump_triggered();
void on_actionExportClassdump_triggered();
void on_actionExportDWARF_triggered();
void on_actionExportProjectArchive_triggered();
@@ -284,9 +289,22 @@ private slots:
void repositionSuperBottomNavbar();
private:
enum class PendingCloseAction { None, Save, Discard };
struct WriteRedoEntry
{
RVA address;
QByteArray data;
};
IaitoCore *core;
void runCommandShortcut(const QString &command, bool needsInput);
void undoWriteCache();
void redoWriteCache();
QByteArray currentWriteCacheFingerprint(bool *hasUndo = nullptr);
bool completePendingClose();
void resumePendingClose();
bool tabsOnTop;
ut64 hexdumpTopOffset;
@@ -300,7 +318,12 @@ private:
QToolButton *webserverButton = nullptr;
bool webserverRunning = false;
QByteArray emptyState;
QList<WriteRedoEntry> writeRedoStack;
QByteArray writeRedoCacheFingerprint;
IOModesController ioModesController;
PendingCloseAction pendingCloseAction = PendingCloseAction::None;
bool pendingProjectSave = false;
bool pendingCloseScheduled = false;
Configuration *configuration;
@@ -310,6 +333,7 @@ private:
EntrypointWidget *entrypointDock = nullptr;
FunctionsWidget *functionsDock = nullptr;
ImportsWidget *importsDock = nullptr;
InterfacesWidget *interfacesDock = nullptr;
ExportsWidget *exportsDock = nullptr;
HeadersWidget *headersDock = nullptr;
TypesWidget *typesDock = nullptr;
@@ -319,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;
@@ -345,6 +370,7 @@ private:
CallGraphWidget *callGraphDock = nullptr;
CallGraphWidget *globalCallGraphDock = nullptr;
ZoomWidget *zoomDock = nullptr;
DataAnalysisWidget *dataAnalysisDock = nullptr;
XrefsWidget *xrefsDock = nullptr;
RefsWidget *refsDock = nullptr;
CodeInfoWidget *codeInfoDock = nullptr;
@@ -394,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();
@@ -424,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
+43 -28
View File
@@ -30,7 +30,7 @@
</property>
<widget class="QMenu" name="addExtraWidgets">
<property name="title">
<string>Add extra...</string>
<string>New Panel</string>
</property>
</widget>
<widget class="QMenuBar" name="menuBar">
@@ -59,7 +59,7 @@
</property>
<widget class="QMenu" name="menuImport">
<property name="title">
<string>Import...</string>
<string>Import</string>
</property>
<addaction name="actionMap"/>
<addaction name="actionImportPDB"/>
@@ -71,10 +71,11 @@
</widget>
<widget class="QMenu" name="menuExport">
<property name="title">
<string>Export...</string>
<string>Export</string>
</property>
<addaction name="actionExport_as_code"/>
<addaction name="actionDump"/>
<addaction name="actionExportClassdump"/>
<addaction name="actionExportDWARF"/>
<addaction name="separator"/>
<addaction name="actionExportProjectArchive"/>
@@ -129,9 +130,15 @@
<string>Analysis</string>
</property>
</widget>
<widget class="QMenu" name="menuData">
<property name="title">
<string>Data</string>
</property>
</widget>
<addaction name="actionHighlight"/>
<addaction name="separator"/>
<addaction name="menuAnalysis"/>
<addaction name="menuData"/>
<addaction name="menuAddInfoWidgets"/>
<addaction name="menuAddIoWidgets"/>
<addaction name="separator"/>
@@ -225,12 +232,12 @@
</widget>
<widget class="QMenu" name="menuAddInfoWidgets">
<property name="title">
<string>Info...</string>
<string>Bin headers</string>
</property>
</widget>
<widget class="QMenu" name="menuAddIoWidgets">
<property name="title">
<string>IO...</string>
<string>I/O Panels</string>
</property>
<!--
<addaction name="actionBinaries"/>
@@ -241,12 +248,12 @@
</widget>
<widget class="QMenu" name="menuAddDebugWidgets">
<property name="title">
<string>Debug...</string>
<string>Debug Panels</string>
</property>
</widget>
<widget class="QMenu" name="menuAddAnother">
<property name="title">
<string>Add another</string>
<string>New Panel</string>
</property>
<addaction name="actionExtraDecompiler"/>
<addaction name="actionExtraDisassembly"/>
@@ -342,7 +349,7 @@
</action>
<action name="actionNew">
<property name="text">
<string>Open...</string>
<string>Open…</string>
</property>
</action>
<action name="actionClose">
@@ -380,12 +387,12 @@
</action>
<action name="actionMap">
<property name="text">
<string>Map file at...</string>
<string>Map file at…</string>
</property>
</action>
<action name="actionManageAddressSpace">
<property name="text">
<string>Manage files...</string>
<string>Manage files</string>
</property>
<property name="toolTip">
<string>Manage files, maps, binaries, filesystems, and address space options</string>
@@ -738,7 +745,7 @@
</action>
<action name="actionRun_Script">
<property name="text">
<string>Radare Script...</string>
<string>Radare Script…</string>
</property>
<property name="toolTip">
<string>Run a radare2 script file once</string>
@@ -749,7 +756,7 @@
</action>
<action name="actionRunR2jsScript">
<property name="text">
<string>r2js script...</string>
<string>r2js script…</string>
</property>
<property name="toolTip">
<string>Run an r2js script file once</string>
@@ -760,7 +767,7 @@
</action>
<action name="actionScripts">
<property name="text">
<string>Script Editor...</string>
<string>Script Editor</string>
</property>
<property name="toolTip">
<string>Open the script editor</string>
@@ -838,7 +845,7 @@
</action>
<action name="actionSettings">
<property name="text">
<string>Settings...</string>
<string>Settings</string>
</property>
</action>
<action name="actionPackageManagerPrefs">
@@ -848,7 +855,7 @@
</action>
<action name="actionSaveAs">
<property name="text">
<string>Save Project...</string>
<string>Save Project…</string>
</property>
</action>
<action name="actionGraph">
@@ -861,12 +868,12 @@
</action>
<action name="actionImportPDB">
<property name="text">
<string>PDB file...</string>
<string>PDB file…</string>
</property>
</action>
<action name="actionImportSymbols">
<property name="text">
<string>Binary symbols...</string>
<string>Binary symbols…</string>
</property>
<property name="toolTip">
<string>Import symbols from another binary object file</string>
@@ -912,7 +919,7 @@
</action>
<action name="actionSwitchJumpTable">
<property name="text">
<string>SwitchJumpTable...</string>
<string>SwitchJumpTable…</string>
</property>
<property name="toolTip">
<string>Create or edit a switch / jump table at the current dispatcher</string>
@@ -923,20 +930,28 @@
</action>
<action name="actionExport_as_code">
<property name="text">
<string>Disassembly/code...</string>
<string>Disassembly/code…</string>
</property>
</action>
<action name="actionDump">
<property name="text">
<string>Binary range...</string>
<string>Binary range…</string>
</property>
<property name="toolTip">
<string>Dump a range of bytes to a file</string>
</property>
</action>
<action name="actionExportClassdump">
<property name="text">
<string>Class dump…</string>
</property>
<property name="toolTip">
<string>Export class information using icc</string>
</property>
</action>
<action name="actionImportDWARF">
<property name="text">
<string>DWARF information...</string>
<string>DWARF information…</string>
</property>
<property name="toolTip">
<string>Import DWARF information from another binary object file</string>
@@ -944,7 +959,7 @@
</action>
<action name="actionImportFLIRT">
<property name="text">
<string>FLIRT signatures...</string>
<string>FLIRT signatures…</string>
</property>
<property name="toolTip">
<string>Load and scan FLIRT signature files</string>
@@ -952,12 +967,12 @@
</action>
<action name="actionImportProjectArchive">
<property name="text">
<string>Project archive...</string>
<string>Project archive…</string>
</property>
</action>
<action name="actionExportDWARF">
<property name="text">
<string>DWARF information...</string>
<string>DWARF information…</string>
</property>
<property name="toolTip">
<string>Export DWARF source line information</string>
@@ -965,7 +980,7 @@
</action>
<action name="actionExportProjectArchive">
<property name="text">
<string>Project archive...</string>
<string>Project archive…</string>
</property>
</action>
<action name="actionExtraHexdump">
@@ -1051,7 +1066,7 @@
</action>
<action name="actionPatchInstruction">
<property name="text">
<string>Patch Instruction...</string>
<string>Patch Instruction…</string>
</property>
<property name="toolTip">
<string>Patch the instruction at the current address</string>
@@ -1059,7 +1074,7 @@
</action>
<action name="actionPatchString">
<property name="text">
<string>Patch String...</string>
<string>Patch String…</string>
</property>
<property name="toolTip">
<string>Patch a string at the current address</string>
@@ -1067,7 +1082,7 @@
</action>
<action name="actionPatchBytes">
<property name="text">
<string>Patch Bytes...</string>
<string>Patch Bytes…</string>
</property>
<property name="toolTip">
<string>Patch bytes at the current address</string>
+1 -1
View File
@@ -21,7 +21,7 @@ AsyncTaskDialog::AsyncTaskDialog(AsyncTask::Ptr task, QWidget *parent)
setWindowTitle(title);
}
qhelpers::bindFont(ui->logTextEdit, false, true);
qhelpers::bindLogFont(ui->logTextEdit);
ui->logTextEdit->setStyleSheet(
QStringLiteral("QPlainTextEdit { background-color: %1; color: %2; }")
.arg(ConfigColor("gui.background").name(), ConfigColor("btext").name()));
+44 -39
View File
@@ -3,13 +3,12 @@
#include <QAction>
#include <QColor>
#include <QColorDialog>
#include <QEvent>
#include <QMenu>
#include "core/Iaito.h"
#include <QAbstractButton>
#include <QIntValidator>
#include <QPushButton>
#include <QRegularExpressionValidator>
// Forwarding constructor: default flag size when only offset & parent provided
FlagDialog::FlagDialog(RVA offset, QWidget *parent)
@@ -38,15 +37,14 @@ FlagDialog::FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent)
connect(ui->colorEdit, &QLineEdit::textChanged, this, [this](const QString &color) {
ui->colorPreview->setStyleSheet(QStringLiteral("background-color: %1;").arg(color));
});
RFlagItem *flag = r_flag_get_i(Core()->core()->flags, offset);
RFlagItem *flag = r_flag_get_in(Core()->core()->flags, offset);
if (flag) {
flagName = QString(flag->name);
flagOffset = flag->addr;
}
auto size_validator = new QIntValidator(ui->sizeEdit);
size_validator->setBottom(1);
ui->sizeEdit->setValidator(size_validator);
ui->sizeEdit->setValidator(new QRegularExpressionValidator(
QRegularExpression(QStringLiteral("[0-9]{1,20}")), ui->sizeEdit));
// Pre-fill size field: use defaultSize for new flags, override with existing for edits
ui->sizeEdit->setText(QString::number(defaultSize));
if (flag) {
@@ -61,15 +59,31 @@ FlagDialog::FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent)
}
}
}
// Enable color picker on color field
ui->colorEdit->installEventFilter(this);
connect(ui->colorButton, &QPushButton::clicked, this, [this]() {
QColor color
= QColorDialog::getColor(QColor(ui->colorEdit->text()), this, tr("Select flag color"));
if (color.isValid()) {
ui->colorEdit->setText(color.name());
}
});
if (flag) {
setWindowTitle(tr("Edit Flag"));
ui->nameEdit->setText(flag->name);
ui->labelAction->setText(tr("Edit flag at %1").arg(RAddressString(offset)));
} else {
ui->labelAction->setText(tr("Add flag at %1").arg(RAddressString(offset)));
}
auto updateAcceptButton = [this]() {
bool validSize = false;
const ut64 size = ui->sizeEdit->text().toULongLong(&validSize);
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(!ui->nameEdit->text().trimmed().isEmpty() && validSize && size > 0);
};
connect(ui->nameEdit, &QLineEdit::textChanged, this, updateAcceptButton);
connect(ui->sizeEdit, &QLineEdit::textChanged, this, updateAcceptButton);
updateAcceptButton();
// Connect slots for OK/Cancel
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &FlagDialog::buttonBoxAccepted);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &FlagDialog::buttonBoxRejected);
@@ -88,34 +102,27 @@ FlagDialog::~FlagDialog() {}
void FlagDialog::buttonBoxAccepted()
{
RVA size = ui->sizeEdit->text().toULongLong();
QString name = ui->nameEdit->text();
bool validSize = false;
RVA size = ui->sizeEdit->text().toULongLong(&validSize);
QString name = ui->nameEdit->text().trimmed();
if (name.isEmpty() || !validSize || size == 0) {
return;
}
QString color = ui->colorEdit->text();
QString comment = ui->commentEdit->text();
if (flagOffset != RVA_INVALID) {
// Existing flag: remove old flag by name
Core()->delFlag(flagName);
// If a name is provided, recreate/update the flag with new attributes
if (!name.isEmpty()) {
Core()->addFlag(offset, name, size, color, comment);
}
} else {
// New flag: only add if name is provided
if (!name.isEmpty()) {
Core()->addFlag(offset, name, size, color, comment);
}
}
close();
setResult(QDialog::Accepted);
Core()->addFlag(offset, name, size, color, comment);
accept();
}
void FlagDialog::buttonBoxRejected()
{
close();
this->setResult(QDialog::Rejected);
reject();
}
std::pair<QAction *, QAction *> FlagDialog::addFlagMenuActions(
std::tuple<QAction *, QAction *, QAction *> FlagDialog::addFlagMenuActions(
QMenu *menu,
QWidget *dialogParent,
RVA addr,
@@ -145,19 +152,17 @@ std::pair<QAction *, QAction *> FlagDialog::addFlagMenuActions(
}
});
return {addAction, editAction};
}
bool FlagDialog::eventFilter(QObject *watched, QEvent *event)
{
// Open color picker when clicking on color field
if (watched == ui->colorEdit && event->type() == QEvent::MouseButtonPress) {
QColor initial = QColor(ui->colorEdit->text());
QColor c = QColorDialog::getColor(initial, this, tr("Select flag color"));
if (c.isValid()) {
ui->colorEdit->setText(c.name());
QAction *deleteAction = menu->addAction(tr("Delete flag"));
deleteAction->setEnabled(existing != nullptr);
QObject::connect(deleteAction, &QAction::triggered, dialogParent, [addr, onChanged]() {
RFlagItem *flag = r_flag_get_in(Core()->core()->flags, addr);
if (flag) {
Core()->delFlag(QString::fromUtf8(flag->name));
if (onChanged) {
onChanged();
}
}
return true;
}
return QDialog::eventFilter(watched, event);
});
return {addAction, editAction, deleteAction};
}
+5 -12
View File
@@ -4,7 +4,7 @@
#include "core/IaitoCommon.h"
#include <functional>
#include <memory>
#include <utility>
#include <tuple>
#include <QDialog>
class QAction;
@@ -19,18 +19,14 @@ class FlagDialog : public QDialog
Q_OBJECT
public:
// offset: address of flag; defaultSize: initial size to pre-fill (number of bytes)
// Constructor: offset is address; defaultSize optional size for new flags
// defaultSize pre-fills the byte count for new flags.
explicit FlagDialog(RVA offset, QWidget *parent = nullptr);
explicit FlagDialog(RVA offset, ut64 defaultSize, QWidget *parent = nullptr);
~FlagDialog();
// Append an "Add flag..." / "Edit flag..." action pair to menu, wired to open
// FlagDialog at addr. Whichever action is inapplicable (add when a flag already
// exists, edit when none does) is created disabled. onChanged is invoked when
// the dialog is accepted (typically a refresh callback). Returns the two
// actions so callers can attach icons, etc.
static std::pair<QAction *, QAction *> addFlagMenuActions(
// Append add, edit and delete actions at addr, disabling inapplicable actions.
// onChanged is called after accepting the dialog or deleting the flag.
static std::tuple<QAction *, QAction *, QAction *> addFlagMenuActions(
QMenu *menu,
QWidget *dialogParent,
RVA addr,
@@ -48,9 +44,6 @@ private:
RVA offset;
QString flagName;
ut64 flagOffset;
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
};
#endif // FLAGDIALOG_H
+7
View File
@@ -107,6 +107,13 @@
<item>
<widget class="QLineEdit" name="colorEdit"/>
</item>
<item>
<widget class="QPushButton" name="colorButton">
<property name="text">
<string>Choose color...</string>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="colorPreview">
<property name="minimumSize">
+53 -8
View File
@@ -2,13 +2,11 @@
#include "common/AsyncTask.h"
#include "ui_InitialOptionsDialog.h"
#include "common/Helpers.h"
#include "core/MainWindow.h"
#include "dialogs/AsyncTaskDialog.h"
#include "dialogs/NewFileDialog.h"
#include <QApplication>
#include <QCloseEvent>
#include <QDateTime>
#include <QDialogButtonBox>
#include <QElapsedTimer>
@@ -27,6 +25,7 @@
#include <QVBoxLayout>
#include "common/AnalTask.h"
#include "common/Helpers.h"
#include "core/Iaito.h"
static QPlainTextEdit *s_analysisLogWidget = nullptr;
@@ -189,6 +188,52 @@ void InitialOptionsDialog::updateCPUComboBox()
ui->cpuComboBox->lineEdit()->setText(currentText);
}
void InitialOptionsDialog::savePersistentOptions()
{
QSettings settings;
settings.beginGroup("InitialOptions");
settings.setValue("analLevel", ui->analSlider->value());
AnalysisCommands item;
foreach (item, analysisCommands) {
settings.setValue("anal/" + item.commandDesc.command, item.checkbox->isChecked());
}
settings.setValue("writeEnabled", ui->writeCheckBox->isChecked());
settings.setValue("loadBinInfo", ui->binCheckBox->isChecked());
settings.setValue("loadBinCache", ui->binCacheCheckBox->isChecked());
settings.setValue("demangle", ui->demangleCheckBox->isChecked());
settings.setValue("analVars", ui->varCheckBox->isChecked());
settings.endGroup();
}
void InitialOptionsDialog::restorePersistentOptions()
{
QSettings settings;
settings.beginGroup("InitialOptions");
if (!settings.contains("analLevel")) {
settings.endGroup();
return;
}
int level = settings.value("analLevel").toInt();
if (level == 6) {
AnalysisCommands item;
foreach (item, analysisCommands) {
item.checkbox->setChecked(
settings.value("anal/" + item.commandDesc.command, item.checked).toBool());
}
}
ui->analSlider->setValue(level);
ui->writeCheckBox->setChecked(
settings.value("writeEnabled", ui->writeCheckBox->isChecked()).toBool());
ui->binCheckBox->setChecked(
settings.value("loadBinInfo", ui->binCheckBox->isChecked()).toBool());
ui->binCacheCheckBox->setChecked(
settings.value("loadBinCache", ui->binCacheCheckBox->isChecked()).toBool());
ui->demangleCheckBox->setChecked(
settings.value("demangle", ui->demangleCheckBox->isChecked()).toBool());
ui->varCheckBox->setChecked(settings.value("analVars", ui->varCheckBox->isChecked()).toBool());
settings.endGroup();
}
QList<QString> InitialOptionsDialog::getAnalysisCommands(const InitialOptions &options)
{
QList<QString> commands;
@@ -287,6 +332,10 @@ void InitialOptionsDialog::loadOptions(const InitialOptions &options)
ui->binCacheCheckBox->setChecked(options.loadBinCache);
ui->demangleCheckBox->setChecked(options.demangle);
ui->varCheckBox->setChecked(options.analVars);
if (!debugMode && options.script.isEmpty()) {
restorePersistentOptions();
}
}
void InitialOptionsDialog::setTooltipWithConfigHelp(QWidget *w, const char *config)
@@ -451,7 +500,7 @@ void InitialOptionsDialog::setupAndStartAnalysis()
QPlainTextEdit logText;
logText.setReadOnly(true);
qhelpers::bindFont(&logText, false, true);
logText.setFont(qhelpers::logFont());
layout.addWidget(&logText);
QPushButton interruptBtn(tr("Interrupt"));
@@ -684,14 +733,10 @@ void InitialOptionsDialog::setupAndStartAnalysis()
void InitialOptionsDialog::on_okButton_clicked()
{
ui->okButton->setEnabled(false);
savePersistentOptions();
setupAndStartAnalysis();
}
void InitialOptionsDialog::closeEvent(QCloseEvent *event)
{
event->accept();
}
QString InitialOptionsDialog::analysisDescription(int level)
{
// TODO: replace this with meaningful descriptions
+2 -3
View File
@@ -35,9 +35,6 @@ private slots:
void updatePDBLayout();
void updateScriptLayout();
protected:
void closeEvent(QCloseEvent *event) override;
private:
std::unique_ptr<Ui::InitialOptionsDialog> ui;
@@ -52,6 +49,8 @@ private:
QList<RAsmPluginDescription> asmPlugins;
void updateCPUComboBox();
void savePersistentOptions();
void restorePersistentOptions();
struct AnalysisCommands
{
CommandDescription commandDesc;
+4 -34
View File
@@ -197,8 +197,6 @@ NewFileDialog::NewFileDialog(MainWindow *main)
setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint));
setAcceptDrops(true);
applyListFonts();
ui->recentsListWidget->viewport()->installEventFilter(this);
ui->projectsListWidget->viewport()->installEventFilter(this);
ui->recentsListWidget->addAction(ui->actionRemove_item);
ui->recentsListWidget->addAction(ui->actionClear_all);
ui->projectsListWidget->addAction(ui->actionRemove_project);
@@ -507,39 +505,11 @@ void NewFileDialog::on_exportProjectButton_clicked()
void NewFileDialog::applyListFonts()
{
ui->recentsListWidget->setFont(Config()->getSmallFont());
ui->projectsListWidget->setFont(Config()->getSmallFont());
}
void NewFileDialog::zoomFonts(int delta)
{
if (delta == 0) {
return;
const QFont font = QApplication::font();
const auto views = findChildren<QAbstractItemView *>();
for (QAbstractItemView *view : views) {
view->setFont(font);
}
Config()->setZoomFactor(Config()->getZoomFactor() + (delta > 0 ? 0.1 : -0.1));
applyListFonts();
}
void NewFileDialog::wheelEvent(QWheelEvent *event)
{
if (event->modifiers() & (Qt::ControlModifier | Qt::MetaModifier)) {
zoomFonts(event->angleDelta().y());
event->accept();
return;
}
QDialog::wheelEvent(event);
}
bool NewFileDialog::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::Wheel) {
auto *wheelEvent = static_cast<QWheelEvent *>(event);
if (wheelEvent->modifiers() & (Qt::ControlModifier | Qt::MetaModifier)) {
zoomFonts(wheelEvent->angleDelta().y());
return true;
}
}
return QDialog::eventFilter(obj, event);
}
void NewFileDialog::dragEnterEvent(QDragEnterEvent *event)
+2 -5
View File
@@ -74,10 +74,8 @@ private slots:
#endif
protected:
void dragEnterEvent(QDragEnterEvent *event);
void dropEvent(QDropEvent *event);
void wheelEvent(QWheelEvent *event) override;
bool eventFilter(QObject *obj, QEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dropEvent(QDropEvent *event) override;
private:
std::unique_ptr<Ui::NewFileDialog> ui;
@@ -125,7 +123,6 @@ private:
void setDisableAndHideWidget(QWidget *w, bool disable_and_hide = true);
void setSpacerEnabled(QSpacerItem *s, bool enabled, int w = 10, int h = 10);
void zoomFonts(int delta);
void applyListFonts();
static const int MaxRecentFiles = 5;
+81 -16
View File
@@ -1,6 +1,7 @@
// PackageManagerDialog.cpp
#include "PackageManagerDialog.h"
#include <QApplication>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QJsonArray>
@@ -9,6 +10,7 @@
#include <QLineEdit>
#include <QMessageBox>
#include <QPainter>
#include <QProcessEnvironment>
#include <QPushButton>
#include <QRegularExpression>
#include <QStringList>
@@ -19,6 +21,14 @@
#include <QTextEdit>
#include <QVBoxLayout>
static QString processError(QProcess &process, const QString &message)
{
const QString details = QString::fromLocal8Bit(
process.readAllStandardOutput() + process.readAllStandardError())
.trimmed();
return details.isEmpty() ? message : message + "\n\n" + details;
}
void CheckBoxDelegate::paint(
QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
@@ -38,20 +48,30 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
: QDialog(parent)
, m_process(new QProcess(this))
{
setWindowTitle(tr("Package Manager"));
setWindowTitle(tr("Radare2 Package Manager (r2pm)"));
resize(900, 600);
auto *layout = new QVBoxLayout(this);
auto *filterLayout = new QHBoxLayout();
m_filterLineEdit = new QLineEdit(this);
m_filterLineEdit->setPlaceholderText(tr("Filter packages..."));
m_filterLineEdit->setMinimumHeight(32);
layout->addWidget(m_filterLineEdit);
m_binaryPackagesCheckBox = new QCheckBox(tr("Binary packages"), this);
m_binaryPackagesCheckBox->setToolTip(
tr("Install ready-to-use packages, including scripts, without building from source."));
m_showAllPlatformsCheckBox = new QCheckBox(tr("Show all platforms"), this);
filterLayout->addWidget(m_filterLineEdit);
filterLayout->addWidget(m_binaryPackagesCheckBox);
filterLayout->addWidget(m_showAllPlatformsCheckBox);
layout->addLayout(filterLayout);
m_tableWidget = new QTableWidget(this);
m_tableWidget->setColumnCount(3);
m_tableWidget->setHorizontalHeaderLabels({tr("Installed"), tr("Package"), tr("Description")});
m_tableWidget->setColumnCount(4);
m_tableWidget->setHorizontalHeaderLabels(
{tr("Installed"), tr("Package"), tr("Platforms"), tr("Description")});
m_tableWidget->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
m_tableWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
m_tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
m_tableWidget->horizontalHeader()->setSectionResizeMode(3, QHeaderView::Stretch);
m_tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
m_tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
m_tableWidget->setSortingEnabled(true);
@@ -61,11 +81,15 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
auto *buttonLayout = new QHBoxLayout();
m_refreshButton = new QPushButton(tr("Refresh"), this);
m_installButton = new QPushButton(tr("Install"), this);
m_uninstallButton = new QPushButton(tr("Uninstall"), this);
m_installButton = new QPushButton(tr("Install"), this);
for (auto *button : {m_refreshButton, m_uninstallButton, m_installButton}) {
button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
}
buttonLayout->addWidget(m_refreshButton);
buttonLayout->addWidget(m_installButton);
buttonLayout->addStretch();
buttonLayout->addWidget(m_uninstallButton);
buttonLayout->addWidget(m_installButton);
layout->addLayout(buttonLayout);
m_logTextEdit = new QTextEdit(this);
@@ -74,6 +98,12 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
layout->addWidget(m_logTextEdit);
connect(m_filterLineEdit, &QLineEdit::textChanged, this, &PackageManagerDialog::filterPackages);
connect(m_binaryPackagesCheckBox, &QCheckBox::toggled, this, [this]() {
filterPackages(m_filterLineEdit->text());
});
connect(m_showAllPlatformsCheckBox, &QCheckBox::toggled, this, [this]() {
filterPackages(m_filterLineEdit->text());
});
connect(m_refreshButton, &QPushButton::clicked, this, &PackageManagerDialog::refreshPackages);
connect(m_installButton, &QPushButton::clicked, this, &PackageManagerDialog::installPackage);
connect(m_uninstallButton, &QPushButton::clicked, this, &PackageManagerDialog::uninstallPackage);
@@ -104,23 +134,30 @@ void PackageManagerDialog::refreshPackages()
QProcess updateProc;
updateProc.start("r2pm", QStringList() << "-U");
if (!updateProc.waitForFinished(30000)) {
QMessageBox::warning(this, tr("Error"), tr("Failed to run r2pm -U"));
QMessageBox::warning(
this, tr("Error"), processError(updateProc, tr("Failed to run r2pm -U.")));
return;
}
// Then list packages
QProcess listProc;
listProc.start("r2pm", QStringList() << "-sj");
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
environment.remove("R2PM_PLATFORM");
listProc.setProcessEnvironment(environment);
listProc.start("r2pm", QStringList() << "-Asj");
if (!listProc.waitForFinished(30000)) {
QMessageBox::warning(this, tr("Error"), tr("Failed to run r2pm -sj"));
QMessageBox::warning(
this, tr("Error"), processError(listProc, tr("Failed to run r2pm -Asj.")));
return;
}
QByteArray out = listProc.readAllStandardOutput();
QJsonDocument doc = QJsonDocument::fromJson(out);
if (!doc.isArray()) {
QMessageBox::warning(this, tr("Error"), tr("Invalid JSON output from r2pm -sj"));
QMessageBox::warning(this, tr("Error"), tr("Invalid JSON output from r2pm -Asj"));
return;
}
QJsonArray array = doc.array();
const bool sortingEnabled = m_tableWidget->isSortingEnabled();
m_tableWidget->setSortingEnabled(false);
m_tableWidget->setRowCount(0);
for (const QJsonValue &value : array) {
if (!value.isObject())
@@ -131,6 +168,13 @@ void PackageManagerDialog::refreshPackages()
if (desc == "") {
desc = obj["desc"].toString();
}
QStringList platforms;
for (const QJsonValue &platformValue : obj["platforms"].toArray()) {
const QString platform = platformValue.toString();
if (!platform.isEmpty()) {
platforms << platform;
}
}
int row = m_tableWidget->rowCount();
m_tableWidget->insertRow(row);
QTableWidgetItem *itemInstalled = new QTableWidgetItem();
@@ -138,12 +182,20 @@ void PackageManagerDialog::refreshPackages()
itemInstalled->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
m_tableWidget->setItem(row, 0, itemInstalled);
QTableWidgetItem *itemName = new QTableWidgetItem(name);
itemName->setData(Qt::UserRole, obj["binary"].toBool());
itemName->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
m_tableWidget->setItem(row, 1, itemName);
QTableWidgetItem *itemPlatforms = new QTableWidgetItem(platforms.join(' '));
itemPlatforms->setData(Qt::UserRole, obj["supported"].toBool(true));
itemPlatforms->setToolTip(platforms.join(", "));
itemPlatforms->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
itemPlatforms->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
m_tableWidget->setItem(row, 2, itemPlatforms);
QTableWidgetItem *itemDesc = new QTableWidgetItem(desc);
itemDesc->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
m_tableWidget->setItem(row, 2, itemDesc);
m_tableWidget->setItem(row, 3, itemDesc);
}
m_tableWidget->setSortingEnabled(sortingEnabled);
filterPackages(m_filterLineEdit->text());
}
@@ -166,30 +218,43 @@ void PackageManagerDialog::filterPackages(const QString &text)
for (int row = 0; row < m_tableWidget->rowCount(); ++row) {
bool visible = text.isEmpty()
|| m_tableWidget->item(row, 1)->text().contains(text, Qt::CaseInsensitive)
|| m_tableWidget->item(row, 2)->text().contains(text, Qt::CaseInsensitive);
|| m_tableWidget->item(row, 2)->text().contains(text, Qt::CaseInsensitive)
|| m_tableWidget->item(row, 3)->text().contains(text, Qt::CaseInsensitive);
if (m_binaryPackagesCheckBox->isChecked()) {
visible = visible && m_tableWidget->item(row, 1)->data(Qt::UserRole).toBool();
}
if (!m_showAllPlatformsCheckBox->isChecked()) {
visible = visible && m_tableWidget->item(row, 2)->data(Qt::UserRole).toBool();
}
m_tableWidget->setRowHidden(row, !visible);
}
if (m_tableWidget->currentRow() >= 0
&& m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
m_tableWidget->setCurrentItem(nullptr);
m_tableWidget->clearSelection();
}
}
void PackageManagerDialog::installPackage()
{
if (m_process->state() != QProcess::NotRunning)
return;
if (m_tableWidget->currentRow() < 0) {
if (m_tableWidget->currentRow() < 0 || m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
QMessageBox::information(this, tr("Install"), tr("Please select a package to install."));
return;
}
QString pkg = m_tableWidget->item(m_tableWidget->currentRow(), 1)->text();
m_logTextEdit->clear();
m_logTextEdit->setVisible(true);
m_process->start("r2pm", QStringList() << "-ci" << pkg);
const QString flags = m_binaryPackagesCheckBox->isChecked() ? "-bi" : "-ci";
m_process->start("r2pm", QStringList() << flags << pkg);
}
void PackageManagerDialog::uninstallPackage()
{
if (m_process->state() != QProcess::NotRunning)
return;
if (m_tableWidget->currentRow() < 0) {
if (m_tableWidget->currentRow() < 0 || m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
QMessageBox::information(this, tr("Uninstall"), tr("Please select a package to uninstall."));
return;
}
+4 -1
View File
@@ -9,6 +9,7 @@
#include <QStyledItemDelegate>
class QLineEdit;
class QCheckBox;
class QTableWidget;
class QPushButton;
class QTextEdit;
@@ -42,6 +43,8 @@ private slots:
private:
QLineEdit *m_filterLineEdit;
QCheckBox *m_binaryPackagesCheckBox;
QCheckBox *m_showAllPlatformsCheckBox;
QTableWidget *m_tableWidget;
QPushButton *m_refreshButton;
QPushButton *m_installButton;
@@ -52,4 +55,4 @@ private:
void populateInstalledPackages();
};
#endif // PACKAGEMANAGERDIALOG_H
#endif // PACKAGEMANAGERDIALOG_H
+9 -2
View File
@@ -78,16 +78,23 @@ void R2PluginsDialog::loadPlugin()
void R2PluginsDialog::refreshPluginDescriptions()
{
ui->RBinTreeWidget->clear();
ui->RDemangleTreeWidget->clear();
for (const auto &plugin : Core()->getRBinPluginDescriptions()) {
QTreeWidgetItem *item = new QTreeWidgetItem();
item->setText(0, plugin.name);
item->setText(1, plugin.description);
item->setText(2, plugin.license);
item->setText(3, plugin.type);
ui->RBinTreeWidget->addTopLevelItem(item);
if (plugin.type == QStringLiteral("dem")) {
ui->RDemangleTreeWidget->addTopLevelItem(item);
} else {
item->setText(3, plugin.type);
ui->RBinTreeWidget->addTopLevelItem(item);
}
}
ui->RBinTreeWidget->sortByColumn(0, Qt::AscendingOrder);
qhelpers::adjustColumns(ui->RBinTreeWidget, 0);
ui->RDemangleTreeWidget->sortByColumn(0, Qt::AscendingOrder);
qhelpers::adjustColumns(ui->RDemangleTreeWidget, 0);
ui->RIOTreeWidget->clear();
for (const auto &plugin : Core()->getRIOPluginDescriptions()) {
+37 -1
View File
@@ -17,7 +17,7 @@
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>4</number>
<number>5</number>
</property>
<widget class="QWidget" name="RBinTab">
<attribute name="title">
@@ -60,6 +60,42 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="RDemangleTab">
<attribute name="title">
<string>Demangle</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_11">
<item>
<widget class="QLabel" name="RDemangleLabel">
<property name="text">
<string>Demangle symbol names for supported languages and compilers</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="RDemangleTreeWidget">
<property name="sortingEnabled">
<bool>true</bool>
</property>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Description</string>
</property>
</column>
<column>
<property name="text">
<string>License</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="RIOTab">
<attribute name="title">
<string>RIO</string>
+3 -3
View File
@@ -50,7 +50,7 @@
</widget>
</item>
<item>
<widget class="AddressableItemList&lt;&gt;" name="toTreeWidget">
<widget class="AddressableItemView" name="toTreeWidget">
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
@@ -73,7 +73,7 @@
</widget>
</item>
<item>
<widget class="AddressableItemList&lt;&gt;" name="fromTreeWidget">
<widget class="AddressableItemView" name="fromTreeWidget">
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
@@ -142,7 +142,7 @@
</widget>
<customwidgets>
<customwidget>
<class>AddressableItemList&lt;&gt;</class>
<class>AddressableItemView</class>
<extends>QTreeView</extends>
<header>widgets/AddressableItemList.h</header>
</customwidget>
@@ -4,6 +4,7 @@
#include <QFontDialog>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
#include <QPainter>
#include <QSettings>
#include <QSignalBlocker>
@@ -82,6 +83,17 @@ AppearanceOptionsWidget::AppearanceOptionsWidget(SettingsDialog *dialog)
->addItem(tr("Bottom"), static_cast<int>(Configuration::VisualNavbarLocation::Bottom));
ui->visualNavbarLocationComboBox->addItem(
tr("SuperBottom"), static_cast<int>(Configuration::VisualNavbarLocation::SuperBottom));
{
// Filling the list must not write the first entry into the settings
QSignalBlocker blocker(ui->memoryScrollBarComboBox);
ui->memoryScrollBarComboBox
->addItem(tr("Hidden"), static_cast<int>(Configuration::MemoryScrollBarMode::Hidden));
ui->memoryScrollBarComboBox->addItem(
tr("Spring (unbounded)"), static_cast<int>(Configuration::MemoryScrollBarMode::Spring));
ui->memoryScrollBarComboBox->addItem(
tr("Bounded (navigation bar range)"),
static_cast<int>(Configuration::MemoryScrollBarMode::Bounded));
}
updateFromConfig();
QStringList langs = Config()->getAvailableTranslations();
@@ -133,6 +145,11 @@ AppearanceOptionsWidget::AppearanceOptionsWidget(SettingsDialog *dialog)
connect(Config(), &Configuration::visualNavbarThicknessChanged, this, [this](int) {
updateVisualNavbarFromConfig();
});
connect(
Config(),
&Configuration::memoryScrollBarModeChanged,
this,
[this](Configuration::MemoryScrollBarMode) { updateVisualNavbarFromConfig(); });
connect(Config(), &Configuration::colorsUpdated, this, [this]() {
updateVisualNavbarFromConfig();
});
@@ -235,6 +252,7 @@ void AppearanceOptionsWidget::updateVisualNavbarFromConfig()
{
QSignalBlocker locationBlocker(ui->visualNavbarLocationComboBox);
QSignalBlocker thicknessBlocker(ui->visualNavbarThicknessSpinBox);
QSignalBlocker scrollBarBlocker(ui->memoryScrollBarComboBox);
auto location = static_cast<int>(Config()->getVisualNavbarLocation());
int index = ui->visualNavbarLocationComboBox->findData(location);
@@ -243,6 +261,11 @@ void AppearanceOptionsWidget::updateVisualNavbarFromConfig()
}
ui->visualNavbarThicknessSpinBox->setValue(Config()->getVisualNavbarThickness());
const int scrollBarMode = static_cast<int>(Config()->getMemoryScrollBarMode());
const int scrollBarIndex = ui->memoryScrollBarComboBox->findData(scrollBarMode);
if (scrollBarIndex >= 0) {
ui->memoryScrollBarComboBox->setCurrentIndex(scrollBarIndex);
}
}
void AppearanceOptionsWidget::onFontZoomBoxValueChanged(int zoom)
@@ -311,10 +334,25 @@ void AppearanceOptionsWidget::on_visualNavbarThicknessSpinBox_valueChanged(int v
Config()->setVisualNavbarThickness(value);
}
void AppearanceOptionsWidget::on_memoryScrollBarComboBox_currentIndexChanged(int index)
{
Config()->setMemoryScrollBarMode(
static_cast<Configuration::MemoryScrollBarMode>(
ui->memoryScrollBarComboBox->itemData(index).toInt()));
}
void AppearanceOptionsWidget::on_editButton_clicked()
{
QString theme = ui->colorComboBox->currentText();
if (!ThemeWorker().isCustomTheme(theme)) {
if (!confirmSystemThemeCopy(theme) || !copyColorTheme(theme)) {
return;
}
theme = ui->colorComboBox->currentText();
}
ColorThemeEditDialog dial;
dial.setWindowTitle(tr("Theme Editor - <%1>").arg(ui->colorComboBox->currentText()));
dial.setWindowTitle(tr("Theme Editor - <%1>").arg(theme));
dial.exec();
ui->colorComboBox->updateFromConfig(false);
}
@@ -333,7 +371,15 @@ void AppearanceOptionsWidget::selectInterfaceThemeByName(const QString &name)
void AppearanceOptionsWidget::on_ifaceEditButton_clicked()
{
InterfaceThemeEditDialog dialog(ui->themeComboBox->currentText(), this);
QString theme = ui->themeComboBox->currentText();
if (!Configuration::isCustomInterfaceTheme(theme)) {
if (!confirmSystemThemeCopy(theme) || !copyInterfaceTheme(theme)) {
return;
}
theme = ui->themeComboBox->currentText();
}
InterfaceThemeEditDialog dialog(theme, this);
if (dialog.exec() == QDialog::Accepted) {
selectInterfaceThemeByName(dialog.savedName());
}
@@ -341,8 +387,11 @@ void AppearanceOptionsWidget::on_ifaceEditButton_clicked()
void AppearanceOptionsWidget::on_ifaceCopyButton_clicked()
{
const QString current = ui->themeComboBox->currentText();
copyInterfaceTheme(ui->themeComboBox->currentText());
}
bool AppearanceOptionsWidget::copyInterfaceTheme(const QString &theme)
{
QString newName;
do {
newName = QInputDialog::getText(
@@ -350,10 +399,10 @@ void AppearanceOptionsWidget::on_ifaceCopyButton_clicked()
tr("Enter theme name"),
tr("Name:"),
QLineEdit::Normal,
current + tr(" - copy"))
theme + tr(" - copy"))
.trimmed();
if (newName.isEmpty()) {
return;
return false;
}
if (!Configuration::isValidThemeName(newName)) {
QMessageBox::warning(this, tr("Theme Copy"), tr("Invalid theme name."));
@@ -367,16 +416,23 @@ void AppearanceOptionsWidget::on_ifaceCopyButton_clicked()
const QString dst
= QDir(Configuration::userThemesDir()).filePath(newName + QStringLiteral(".theme"));
if (Configuration::isCustomInterfaceTheme(current)) {
QFile::copy(
QDir(Configuration::userThemesDir()).filePath(current + QStringLiteral(".theme")), dst);
bool copied = false;
if (Configuration::isCustomInterfaceTheme(theme)) {
copied = QFile::copy(
QDir(Configuration::userThemesDir()).filePath(theme + QStringLiteral(".theme")), dst);
} else {
Theme t = current == QStringLiteral("Native") ? IaitoStyle::instance()->theme()
: Theme::builtin(current);
Theme t = theme == QStringLiteral("Native") ? IaitoStyle::instance()->theme()
: Theme::builtin(theme);
t.name = newName;
t.save(dst);
copied = QFileInfo::exists(dst);
}
if (!copied) {
QMessageBox::critical(this, tr("Error"), tr("Cannot copy theme."));
return false;
}
selectInterfaceThemeByName(newName);
return true;
}
void AppearanceOptionsWidget::on_ifaceRenameButton_clicked()
@@ -478,15 +534,18 @@ void AppearanceOptionsWidget::on_ifaceImportButton_clicked()
void AppearanceOptionsWidget::updateInterfaceModificationButtons(const QString &theme)
{
bool editable = Configuration::isCustomInterfaceTheme(theme);
ui->ifaceEditButton->setEnabled(editable);
ui->ifaceEditButton->setEnabled(!theme.isEmpty());
ui->ifaceRenameButton->setEnabled(editable);
ui->ifaceDeleteButton->setEnabled(editable);
}
void AppearanceOptionsWidget::on_copyButton_clicked()
{
QString currColorTheme = ui->colorComboBox->currentText();
copyColorTheme(ui->colorComboBox->currentText());
}
bool AppearanceOptionsWidget::copyColorTheme(const QString &theme)
{
QString newThemeName;
do {
newThemeName = QInputDialog::getText(
@@ -494,10 +553,10 @@ void AppearanceOptionsWidget::on_copyButton_clicked()
tr("Enter theme name"),
tr("Name:"),
QLineEdit::Normal,
currColorTheme + tr(" - copy"))
theme + tr(" - copy"))
.trimmed();
if (newThemeName.isEmpty()) {
return;
return false;
}
if (ThemeWorker().isThemeExist(newThemeName)) {
QMessageBox::information(
@@ -507,9 +566,14 @@ void AppearanceOptionsWidget::on_copyButton_clicked()
}
} while (true);
ThemeWorker().copy(currColorTheme, newThemeName);
QString err = ThemeWorker().copy(theme, newThemeName);
if (!err.isEmpty()) {
QMessageBox::critical(this, tr("Error"), err);
return false;
}
Config()->setColorTheme(newThemeName);
updateThemeFromConfig(false);
return true;
}
void AppearanceOptionsWidget::on_deleteButton_clicked()
@@ -616,11 +680,26 @@ void AppearanceOptionsWidget::onLanguageComboBoxCurrentIndexChanged(int index)
void AppearanceOptionsWidget::updateModificationButtons(const QString &theme)
{
bool editable = ThemeWorker().isCustomTheme(theme);
ui->editButton->setEnabled(editable);
ui->editButton->setEnabled(!theme.isEmpty());
ui->deleteButton->setEnabled(editable);
ui->renameButton->setEnabled(editable);
}
bool AppearanceOptionsWidget::confirmSystemThemeCopy(const QString &theme)
{
QMessageBox prompt(this);
prompt.setIcon(QMessageBox::Question);
prompt.setWindowTitle(tr("Edit Theme"));
prompt.setText(
tr("The system theme <b>%1</b> cannot be edited directly.").arg(theme.toHtmlEscaped()));
prompt.setInformativeText(tr("Would you like to make a copy and start editing it?"));
QPushButton *copyButton = prompt.addButton(tr("Make a Copy"), QMessageBox::AcceptRole);
prompt.addButton(QMessageBox::Cancel);
prompt.setDefaultButton(copyButton);
prompt.exec();
return prompt.clickedButton() == copyButton;
}
void AppearanceOptionsWidget::updateFromConfig()
{
updateFontFromConfig();
@@ -38,6 +38,7 @@ private slots:
void on_themeComboBox_currentIndexChanged(int index);
void on_visualNavbarLocationComboBox_currentIndexChanged(int index);
void on_visualNavbarThicknessSpinBox_valueChanged(int value);
void on_memoryScrollBarComboBox_currentIndexChanged(int index);
void on_copyButton_clicked();
void on_deleteButton_clicked();
@@ -69,6 +70,9 @@ private slots:
void onLanguageComboBoxCurrentIndexChanged(int index);
private:
bool confirmSystemThemeCopy(const QString &theme);
bool copyColorTheme(const QString &theme);
bool copyInterfaceTheme(const QString &theme);
void updateModificationButtons(const QString &theme);
void updateInterfaceModificationButtons(const QString &theme);
void selectInterfaceThemeByName(const QString &name);
@@ -324,6 +324,30 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="memoryScrollBarLayout">
<item>
<widget class="QLabel" name="memoryScrollBarLabel">
<property name="text">
<string>Hexdump and disassembly scrollbar</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="memoryScrollBarComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Hidden: no scrollbar. Spring: the handle rests in the middle and scrolls faster the further and longer it is dragged. Bounded: a regular scrollbar over the navigation bar range.</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="4" column="0">
+42
View File
@@ -17,6 +17,12 @@ AsmOptionsWidget::AsmOptionsWidget(SettingsDialog *dialog)
, ui(new Ui::AsmOptionsWidget)
{
ui->setupUi(this);
ui->jumpLineSpacingComboBox->blockSignals(true);
for (int spacing : {2, 4, 6, 8, 10, 12, 16, 20, 24, 32}) {
ui->jumpLineSpacingComboBox->addItem(tr("%1 px").arg(spacing), spacing);
}
ui->jumpLineSpacingComboBox->blockSignals(false);
// Initialize "Relative to" combobox for asm.addr.relto values
ui->relToComboBox->setEditable(true);
ui->relToComboBox->blockSignals(true);
@@ -116,6 +122,13 @@ AsmOptionsWidget::AsmOptionsWidget(SettingsDialog *dialog)
connect(ui->offsetCheckBox, &QCheckBox::toggled, this, &AsmOptionsWidget::offsetCheckBoxToggled);
connect(
ui->relOffsetCheckBox, &QCheckBox::toggled, this, &AsmOptionsWidget::relOffCheckBoxToggled);
connect(ui->linesCheckBox, &QCheckBox::toggled, ui->jumpLineSpacingLabel, &QWidget::setEnabled);
connect(ui->linesCheckBox, &QCheckBox::toggled, ui->jumpLineSpacingComboBox, &QWidget::setEnabled);
connect(
ui->linesCheckBox,
&QCheckBox::toggled,
ui->adaptiveJumpLineSpacingCheckBox,
&QWidget::setEnabled);
connect(Core(), &IaitoCore::asmOptionsChanged, this, &AsmOptionsWidget::updateAsmOptionsFromVars);
updateAsmOptionsFromVars();
}
@@ -183,6 +196,19 @@ void AsmOptionsWidget::updateAsmOptionsFromVars()
qhelpers::setCheckedWithoutSignals(
confCheckbox->checkBox, Config()->getConfigBool(confCheckbox->config));
}
const bool adaptiveSpacing = Config()->getDisassemblyJumpLineAdaptiveSpacingEnabled();
qhelpers::setCheckedWithoutSignals(ui->adaptiveJumpLineSpacingCheckBox, adaptiveSpacing);
ui->jumpLineSpacingLabel->setText(
adaptiveSpacing ? tr("Maximum spacing:") : tr("Fixed spacing:"));
ui->jumpLineSpacingComboBox->blockSignals(true);
const int spacingIndex = ui->jumpLineSpacingComboBox->findData(
Config()->getDisassemblyJumpLineSpacing());
ui->jumpLineSpacingComboBox->setCurrentIndex(spacingIndex < 0 ? 0 : spacingIndex);
ui->jumpLineSpacingComboBox->blockSignals(false);
const bool jumpLinesEnabled = Config()->getConfigBool("asm.lines");
ui->adaptiveJumpLineSpacingCheckBox->setEnabled(jumpLinesEnabled);
ui->jumpLineSpacingLabel->setEnabled(jumpLinesEnabled);
ui->jumpLineSpacingComboBox->setEnabled(jumpLinesEnabled);
// Update "Relative to" combobox based on asm.reloff
bool reloffEnabled = Config()->getConfigBool("asm.reloff");
ui->relToComboBox->blockSignals(true);
@@ -242,6 +268,22 @@ void AsmOptionsWidget::on_nbytesSpinBox_valueChanged(int value)
triggerAsmOptionsChanged();
}
void AsmOptionsWidget::on_jumpLineSpacingComboBox_currentIndexChanged(int index)
{
if (index < 0) {
return;
}
Config()->setDisassemblyJumpLineSpacing(ui->jumpLineSpacingComboBox->itemData(index).toInt());
triggerAsmOptionsChanged();
}
void AsmOptionsWidget::on_adaptiveJumpLineSpacingCheckBox_toggled(bool checked)
{
Config()->setDisassemblyJumpLineAdaptiveSpacingEnabled(checked);
ui->jumpLineSpacingLabel->setText(checked ? tr("Maximum spacing:") : tr("Fixed spacing:"));
triggerAsmOptionsChanged();
}
void AsmOptionsWidget::on_syntaxComboBox_currentIndexChanged(int index)
{
Config()->setConfig(
+2
View File
@@ -45,6 +45,8 @@ private slots:
void on_asmTabsSpinBox_valueChanged(int value);
void on_asmTabsOffSpinBox_valueChanged(int value);
void on_nbytesSpinBox_valueChanged(int value);
void on_jumpLineSpacingComboBox_currentIndexChanged(int index);
void on_adaptiveJumpLineSpacingCheckBox_toggled(bool checked);
void on_bytesCheckBox_toggled(bool checked);
void on_varsubCheckBox_toggled(bool checked);
+28
View File
@@ -434,6 +434,34 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="adaptiveJumpLineSpacingCheckBox">
<property name="text">
<string>Fit jump lines to gutter</string>
</property>
<property name="toolTip">
<string>Shrink jump line spacing as needed to keep every lane visible</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="jumpLineSpacingLayout">
<item>
<widget class="QLabel" name="jumpLineSpacingLabel">
<property name="text">
<string>Maximum spacing:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="jumpLineSpacingComboBox">
<property name="toolTip">
<string>Maximum spacing when fitting to the gutter, or exact spacing otherwise</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="fcnlinesCheckBox">
<property name="text">
@@ -125,7 +125,8 @@ InterfaceThemeEditDialog::InterfaceThemeEditDialog(const QString &themeName, QWi
chromeCombo = new QComboBox(this);
chromeCombo->addItem(tr("Flat"));
chromeCombo->addItem(tr("Accent"));
chromeCombo->setCurrentIndex(m_base.chromeStyle == ChromeStyle::Accent ? 1 : 0);
chromeCombo->addItem(tr("Accent tabs"));
chromeCombo->setCurrentIndex(static_cast<int>(m_base.chromeStyle));
connect(skinCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int) {
previewTimer->start();
});
@@ -202,7 +203,7 @@ Theme InterfaceThemeEditDialog::buildTheme() const
{
Theme t = Theme::fromChrome(m_vars, m_dark);
t.skin = skinCombo->currentIndex() == 1 ? Skin::Bevel : Skin::Modern;
t.chromeStyle = chromeCombo->currentIndex() == 1 ? ChromeStyle::Accent : ChromeStyle::Flat;
t.chromeStyle = static_cast<ChromeStyle>(chromeCombo->currentIndex());
for (int i = 0; i < metricSpins.size(); ++i) {
t.metrics.*(kEditorMetrics[i].field) = metricSpins[i]->value();
}
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" stroke="#888" stroke-width="1.8" stroke-linecap="round">
<path d="M9 5h11M9 12h11M9 19h11"/>
<path d="M4 5h.5M4 12h.5M4 19h.5" stroke-width="3"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 263 B

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

After

Width:  |  Height:  |  Size: 319 B

+7 -1
View File
@@ -210,6 +210,9 @@ void AddressableItemContextMenu::onActionAddComment()
void AddressableItemContextMenu::aboutToShowSlot()
{
if (!hasTarget) {
return;
}
if (actionShowInMenu->menu()) {
actionShowInMenu->menu()->deleteLater();
}
@@ -225,6 +228,9 @@ void AddressableItemContextMenu::setHasTarget(bool hasTarget)
{
this->hasTarget = hasTarget;
for (const auto &action : this->actions()) {
action->setEnabled(hasTarget);
// View options also apply to group rows and empty lists.
if (!action->property("addressIndependent").toBool()) {
action->setEnabled(hasTarget);
}
}
}
+131
View File
@@ -0,0 +1,131 @@
#ifndef CONTEXTMENUICONS_H
#define CONTEXTMENUICONS_H
#include <QAction>
#include <QColor>
#include <QIcon>
#include <QPainter>
#include <QPixmap>
#include <QPolygonF>
#include <QVector>
#include <QWidget>
namespace ContextMenuIcons {
enum class Icon {
Copy,
Navigation,
XRefs,
Comment,
Rename,
Delete,
Edit,
View,
Breakpoint,
Debug,
Fold,
Collapse,
Expand,
Sync
};
inline void setIcon(const QWidget *widget, QAction *action, Icon icon, const QColor &color)
{
const qreal ratio = widget->devicePixelRatioF();
const int size = 16;
QPixmap pixmap(size * ratio, size * ratio);
pixmap.setDevicePixelRatio(ratio);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
QColor accent = color;
accent.setAlpha(205);
painter.setPen(QPen(accent, 1.8, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
painter.setBrush(Qt::NoBrush);
switch (icon) {
case Icon::Copy:
painter.drawRect(QRectF(5, 3.5, 7, 9));
painter.drawRect(QRectF(3, 5.5, 7, 7));
break;
case Icon::Navigation:
painter.drawLine(QPointF(4, 8), QPointF(11, 8));
painter.drawPolyline(
QPolygonF(QVector<QPointF>{QPointF(8, 5), QPointF(11, 8), QPointF(8, 11)}));
break;
case Icon::XRefs:
painter.drawEllipse(QPointF(5, 8), 2.0, 2.0);
painter.drawEllipse(QPointF(11, 5), 2.0, 2.0);
painter.drawEllipse(QPointF(11, 11), 2.0, 2.0);
painter.drawLine(QPointF(6.7, 7), QPointF(9.3, 5.8));
painter.drawLine(QPointF(6.7, 9), QPointF(9.3, 10.2));
break;
case Icon::Comment:
painter.drawRoundedRect(QRectF(3.5, 4, 9, 7), 1.5, 1.5);
painter.drawLine(QPointF(6, 11), QPointF(4.5, 13));
break;
case Icon::Rename:
painter.drawLine(QPointF(4, 12), QPointF(12, 4));
painter.drawLine(QPointF(10, 4), QPointF(12, 6));
painter.drawLine(QPointF(4, 12), QPointF(7, 11));
break;
case Icon::Delete:
painter.drawLine(QPointF(4, 5), QPointF(12, 5));
painter.drawLine(QPointF(6, 3.5), QPointF(10, 3.5));
painter.drawRoundedRect(QRectF(5, 6, 6, 7), 1.0, 1.0);
painter.drawLine(QPointF(7, 7.5), QPointF(7, 11.5));
painter.drawLine(QPointF(9, 7.5), QPointF(9, 11.5));
break;
case Icon::Edit:
painter.drawRect(QRectF(4, 4, 8, 8));
painter.drawLine(QPointF(6, 7), QPointF(10, 7));
painter.drawLine(QPointF(6, 9), QPointF(9, 9));
break;
case Icon::View:
painter.drawEllipse(QRectF(3, 5, 10, 6));
painter.drawEllipse(QPointF(8, 8), 1.8, 1.8);
break;
case Icon::Breakpoint:
painter.setBrush(accent);
painter.drawEllipse(QPointF(8, 8), 4.0, 4.0);
break;
case Icon::Debug:
painter.drawLine(QPointF(8, 3), QPointF(8, 13));
painter.drawLine(QPointF(4, 5), QPointF(12, 5));
painter.drawLine(QPointF(4, 11), QPointF(12, 11));
painter.drawRoundedRect(QRectF(5, 4, 6, 8), 1.5, 1.5);
break;
case Icon::Fold:
painter.drawPolyline(
QPolygonF(QVector<QPointF>{QPointF(4, 3), QPointF(8, 7), QPointF(12, 3)}));
painter.drawPolyline(
QPolygonF(QVector<QPointF>{QPointF(4, 13), QPointF(8, 9), QPointF(12, 13)}));
break;
case Icon::Collapse:
case Icon::Expand:
painter.drawRoundedRect(QRectF(3, 3, 10, 10), 1.5, 1.5);
painter.drawLine(QPointF(5.5, 8), QPointF(10.5, 8));
if (icon == Icon::Expand) {
painter.drawLine(QPointF(8, 5.5), QPointF(8, 10.5));
}
break;
case Icon::Sync:
painter.drawLine(QPointF(3, 5), QPointF(12, 5));
painter.drawPolyline(
QPolygonF(QVector<QPointF>{QPointF(10, 3), QPointF(12, 5), QPointF(10, 7)}));
painter.drawLine(QPointF(13, 11), QPointF(4, 11));
painter.drawPolyline(
QPolygonF(QVector<QPointF>{QPointF(6, 9), QPointF(4, 11), QPointF(6, 13)}));
break;
}
painter.end();
action->setIcon(QIcon(pixmap));
action->setIconVisibleInMenu(true);
}
} // namespace ContextMenuIcons
#endif // CONTEXTMENUICONS_H
+93 -14
View File
@@ -1,4 +1,5 @@
#include "DecompilerContextMenu.h"
#include "ContextMenuIcons.h"
#include "MainWindow.h"
#include "common/DeepLink.h"
#include "common/ShortcutManager.h"
@@ -19,6 +20,7 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
, curHighlightedWord(QString())
, offset(0)
, decompiledFunctionAddress(RVA_INVALID)
, firstOffsetInLine(RVA_INVALID)
, isTogglingBreakpoints(false)
, annotationHere(nullptr)
, actionCopy(tr("Copy"), this)
@@ -33,7 +35,8 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
, actionDeleteName(tr("Delete <name>"), this)
, actionEditFunctionVariables(tr("Edit variable <name of variable>"), this)
, actionXRefs(tr("Show X-Refs"), this)
, actionShowAddress(tr("Show address"), this)
, actionShowAddress(tr("Show addresses"), this)
, actionShowDisassembly(tr("Show disassembly"), this)
, actionToggleBreakpoint(tr("Add/remove breakpoint"), this)
, actionAdvancedBreakpoint(tr("Advanced breakpoint"), this)
, breakpointsInLineMenu(new QMenu(this))
@@ -54,6 +57,7 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
setActionXRefs();
setActionShowAddress();
setActionShowDisassembly();
setActionEditFunctionVariables();
@@ -64,6 +68,36 @@ DecompilerContextMenu::DecompilerContextMenu(QWidget *parent, MainWindow *mainWi
// actionEditAnnotation.setVisible(true);
// actionEditAnnotation.setEnabled(true);
using ContextMenuIcons::Icon;
using ContextMenuIcons::setIcon;
const QColor copyColor(56, 142, 60);
const QColor navigationColor(103, 80, 164);
const QColor annotationColor(67, 160, 71);
const QColor analysisColor(0, 137, 190);
const QColor viewColor(191, 128, 32);
const QColor debugColor(216, 88, 64);
setIcon(this, &actionCopy, Icon::Copy, copyColor);
setIcon(this, &actionCopyInstructionAddress, Icon::Copy, copyColor);
setIcon(this, &actionCopyReferenceAddress, Icon::Copy, copyColor);
setIcon(this, &actionCopyDeepLink, Icon::Copy, copyColor);
setIcon(this, &actionShowInSubmenu, Icon::Navigation, navigationColor);
setIcon(this, &actionAddComment, Icon::Comment, annotationColor);
setIcon(this, &actionDeleteComment, Icon::Delete, debugColor);
setIcon(this, &actionRenameThingHere, Icon::Rename, annotationColor);
setIcon(this, &actionDeleteName, Icon::Delete, debugColor);
setIcon(this, &actionEditFunctionVariables, Icon::Edit, analysisColor);
setIcon(this, &actionXRefs, Icon::XRefs, analysisColor);
setIcon(this, &actionShowAddress, Icon::View, viewColor);
setIcon(this, &actionShowDisassembly, Icon::View, viewColor);
setIcon(this, breakpointMenu->menuAction(), Icon::Breakpoint, debugColor);
setIcon(this, &actionToggleBreakpoint, Icon::Breakpoint, debugColor);
setIcon(this, &actionAdvancedBreakpoint, Icon::Edit, debugColor);
setIcon(this, debugMenu->menuAction(), Icon::Debug, debugColor);
setIcon(this, &actionContinueUntil, Icon::Navigation, debugColor);
setIcon(this, &actionSetPC, Icon::Debug, debugColor);
setIcon(this, &actionEditAnnotation, Icon::Edit, annotationColor);
setShortcutContextInActions(this);
connect(
@@ -100,6 +134,8 @@ void DecompilerContextMenu::setDecompiledFunctionAddress(RVA functionAddr)
void DecompilerContextMenu::setFirstOffsetInLine(RVA firstOffset)
{
firstOffsetInLine = firstOffset;
actionAddComment.setEnabled(firstOffset != RVA_INVALID);
actionDeleteComment.setEnabled(firstOffset != RVA_INVALID);
}
RVA DecompilerContextMenu::getFirstOffsetInLine()
@@ -117,6 +153,8 @@ void DecompilerContextMenu::setupBreakpointsInLineMenu()
breakpointsInLineMenu->clear();
for (auto curOffset : this->availableBreakpoints) {
QAction *action = breakpointsInLineMenu->addAction(RAddressString(curOffset));
action->setIcon(actionToggleBreakpoint.icon());
action->setIconVisibleInMenu(true);
connect(action, &QAction::triggered, this, [this, curOffset] {
BreakpointsDialog::editBreakpoint(Core()->getBreakpointAt(curOffset), this);
});
@@ -163,14 +201,15 @@ void DecompilerContextMenu::aboutToHideSlot()
void DecompilerContextMenu::aboutToShowSlot()
{
DecompilerWidget *decompilerWidget = qobject_cast<DecompilerWidget *>(parentWidget());
actionShowAddress.setText(
decompilerWidget && decompilerWidget->isAddressColumnVisible() ? tr("Hide address")
: tr("Show address"));
actionShowAddress.setChecked(decompilerWidget && decompilerWidget->isAddressColumnVisible());
actionShowDisassembly.setChecked(
decompilerWidget && decompilerWidget->isDisassemblyColumnVisible());
actionAddComment.setVisible(true);
actionAddComment.setEnabled(firstOffsetInLine != RVA_INVALID);
if (this->firstOffsetInLine != RVA_MAX) {
actionShowInSubmenu.setVisible(true);
QString comment = Core()->cmdRawAt("CC.", this->firstOffsetInLine);
actionAddComment.setVisible(true);
if (comment.isEmpty()) {
actionDeleteComment.setVisible(false);
actionAddComment.setText(tr("Add Comment"));
@@ -180,7 +219,7 @@ void DecompilerContextMenu::aboutToShowSlot()
}
} else {
actionShowInSubmenu.setVisible(false);
actionAddComment.setVisible(false);
actionAddComment.setText(tr("Add Comment"));
actionDeleteComment.setVisible(false);
}
@@ -310,6 +349,8 @@ void DecompilerContextMenu::setActionShowInSubmenu()
void DecompilerContextMenu::setActionAddComment()
{
actionAddComment.setObjectName(QStringLiteral("actionAddComment"));
actionAddComment.setEnabled(firstOffsetInLine != RVA_INVALID);
connect(
&actionAddComment,
&QAction::triggered,
@@ -321,6 +362,7 @@ void DecompilerContextMenu::setActionAddComment()
void DecompilerContextMenu::setActionDeleteComment()
{
actionDeleteComment.setObjectName(QStringLiteral("actionDeleteComment"));
connect(
&actionDeleteComment,
&QAction::triggered,
@@ -338,15 +380,32 @@ void DecompilerContextMenu::setActionXRefs()
void DecompilerContextMenu::setActionShowAddress()
{
connect(&actionShowAddress, &QAction::triggered, this, [this]() {
actionShowAddress.setObjectName(QStringLiteral("actionShowAddresses"));
actionShowAddress.setCheckable(true);
actionShowAddress.setToolTip(tr("Show the address associated with each decompiled line."));
connect(&actionShowAddress, &QAction::triggered, this, [this](bool checked) {
DecompilerWidget *decompilerWidget = qobject_cast<DecompilerWidget *>(parentWidget());
if (decompilerWidget) {
decompilerWidget->setAddressColumnVisible(!decompilerWidget->isAddressColumnVisible());
decompilerWidget->setAddressColumnVisible(checked);
}
});
addAction(&actionShowAddress);
}
void DecompilerContextMenu::setActionShowDisassembly()
{
actionShowDisassembly.setObjectName(QStringLiteral("actionShowDisassembly"));
actionShowDisassembly.setCheckable(true);
actionShowDisassembly.setToolTip(tr("Show disassembly beside the decompiled code, like pdca."));
connect(&actionShowDisassembly, &QAction::triggered, this, [this](bool checked) {
DecompilerWidget *decompilerWidget = qobject_cast<DecompilerWidget *>(parentWidget());
if (decompilerWidget) {
decompilerWidget->setDisassemblyColumnVisible(checked);
}
});
addAction(&actionShowDisassembly);
}
void DecompilerContextMenu::setActionRenameThingHere()
{
ShortcutMgr()->bindAction("decompiler.rename", &actionRenameThingHere);
@@ -435,12 +494,21 @@ void DecompilerContextMenu::actionCopyInstructionAddressTriggered()
void DecompilerContextMenu::actionEditAnnotationTriggered()
{
QString os = Core()->cmdRaw("anos");
QString *s = openTextEditDialog(os, this);
if (s != nullptr) {
Core()->cmdRaw(QStringLiteral("ano=base64:%1").arg(QString(s->toLocal8Bit().toBase64())));
this->mainWindow->refreshAll();
const RVA functionAddress = decompiledFunctionAddress;
if (functionAddress == RVA_INVALID) {
return;
}
TextEditDialog dialog(Core()->cmdRawAt("anos", functionAddress), this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
const QString text = dialog.getEditedText();
const QString command
= text.isEmpty()
? QStringLiteral("ano-")
: QStringLiteral("ano=base64:%1").arg(QString::fromLatin1(text.toUtf8().toBase64()));
Core()->cmdRawAt(command, functionAddress);
this->mainWindow->refreshAll();
}
void DecompilerContextMenu::actionCopyReferenceAddressTriggered()
@@ -456,11 +524,20 @@ void DecompilerContextMenu::actionCopyDeepLinkTriggered()
void DecompilerContextMenu::actionAddCommentTriggered()
{
CommentsDialog::addOrEditComment(this->firstOffsetInLine, this);
if (firstOffsetInLine == RVA_INVALID) {
return;
}
CommentsDialog::addOrEditComment(this->firstOffsetInLine, parentWidget());
if (auto *widget = qobject_cast<DecompilerWidget *>(parentWidget())) {
widget->setFocus(Qt::OtherFocusReason);
}
}
void DecompilerContextMenu::actionDeleteCommentTriggered()
{
if (firstOffsetInLine == RVA_INVALID) {
return;
}
Core()->delComment(this->firstOffsetInLine);
}
@@ -676,6 +753,8 @@ void DecompilerContextMenu::updateTargetMenuActions()
RAddressString(annotationHere->reference.offset));
}
auto action = new QAction(name, this);
action->setIcon(actionShowInSubmenu.icon());
action->setIconVisibleInMenu(true);
showTargetMenuActions.append(action);
action->setMenu(menu);
insertActions(copySeparator, showTargetMenuActions);
+2
View File
@@ -105,6 +105,7 @@ private:
QAction actionXRefs;
QAction actionShowAddress;
QAction actionShowDisassembly;
QMenu *breakpointMenu;
QAction actionToggleBreakpoint;
@@ -137,6 +138,7 @@ private:
void setActionXRefs();
void setActionShowAddress();
void setActionShowDisassembly();
void setActionRenameThingHere();
void setActionDeleteName();
+64 -32
View File
@@ -52,8 +52,7 @@ DisassemblyContextMenu::DisassemblyContextMenu(QWidget *parent, MainWindow *main
, actionAnalyzeFunction(this)
, actionEditFunction(this)
, actionRename(this)
, actionAddFlag(this)
, actionEditFlag(this)
, actionFlag(this)
, actionAddFlagAtRef(this)
, actionEditFlagAtRef(this)
, actionSetFunctionVarTypes(this)
@@ -91,6 +90,7 @@ DisassemblyContextMenu::DisassemblyContextMenu(QWidget *parent, MainWindow *main
, showInSubmenu(this)
, actionToggleSelectionSync(this)
, actionToggleBBLines(this)
, actionToggleFcnLines(this)
, actionToggleXRefs(this)
, actionToggleVarSummary(this)
{
@@ -169,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
@@ -448,6 +457,16 @@ void DisassemblyContextMenu::buildRepresentationMenu()
Core()->triggerAsmOptionsChanged();
});
actionToggleFcnLines.setText(tr("Function boundaries"));
actionToggleFcnLines.setCheckable(true);
actionToggleFcnLines.setChecked(Config()->getConfigBool("asm.lines.fcn"));
setActionIcon(&actionToggleFcnLines, MenuIcon::View, QColor(191, 128, 32));
representationMenu->addAction(&actionToggleFcnLines);
connect(&actionToggleFcnLines, &QAction::toggled, this, [](bool checked) {
Config()->setConfig("asm.lines.fcn", checked);
Core()->triggerAsmOptionsChanged();
});
actionToggleXRefs.setText(tr("Show x-refs"));
actionToggleXRefs.setCheckable(true);
actionToggleXRefs.setChecked(Config()->getConfigBool("asm.xrefs"));
@@ -633,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);
@@ -683,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);
@@ -722,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()));
@@ -867,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
@@ -891,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;
@@ -1048,6 +1063,9 @@ void DisassemblyContextMenu::aboutToShowSlot()
actionToggleBBLines.blockSignals(true);
actionToggleBBLines.setChecked(Config()->getConfigBool("asm.lines.bb"));
actionToggleBBLines.blockSignals(false);
actionToggleFcnLines.blockSignals(true);
actionToggleFcnLines.setChecked(Config()->getConfigBool("asm.lines.fcn"));
actionToggleFcnLines.blockSignals(false);
actionToggleXRefs.blockSignals(true);
actionToggleXRefs.setChecked(Config()->getConfigBool("asm.xrefs"));
actionToggleXRefs.blockSignals(false);
@@ -1389,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()
@@ -1611,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()
+6 -2
View File
@@ -19,7 +19,11 @@ public:
DisassemblyContextMenu(QWidget *parent, MainWindow *mainWindow);
~DisassemblyContextMenu();
// The graph view shares this menu and uses F for branch navigation.
void registerFlagShortcut();
signals:
void flagAboutToChange();
void copy();
/**
* @brief Signal emitted to copy raw bytes of selected instructions
@@ -177,8 +181,7 @@ private:
QAction actionAnalyzeFunction;
QAction actionEditFunction;
QAction actionRename;
QAction actionAddFlag;
QAction actionEditFlag;
QAction actionFlag;
QAction actionAddFlagAtRef;
QAction actionEditFlagAtRef;
RVA flagRefAddr = 0;
@@ -241,6 +244,7 @@ private:
QAction showInSubmenu;
QAction actionToggleSelectionSync;
QAction actionToggleBBLines;
QAction actionToggleFcnLines;
QAction actionToggleXRefs;
QAction actionToggleVarSummary;
QList<QAction *> showTargetMenuActions;
+52
View File
@@ -42,6 +42,58 @@
<name>radare</name>
</developer>
<releases>
<release version="6.2.6" date="2026-11-06">
<description>
<p>Fix disasm lines glitch</p>
<p>Colorize basic blocks in minigraph</p>
<p>Toggle decompiler with tab and graph with space</p>
<p>Decai decompiler options</p>
<p>Global variable edition and navigation</p>
<p>Filesystem browser</p>
<p>Several bug fixes and optimizations</p>
<p>Zoom panels with meta+enter</p>
<p>Switch tabs with cmd+number</p>
<p>Cycle dock frames with cmd+JK</p>
<p>Hover indicators in disasm and decompile</p>
<p>Per-view seek history fixes</p>
<p>Folding braced blocks in decompiler</p>
</description>
</release>
<release version="6.2.4" date="2026-10-05">
<description>
<p>Collapse sparse gaps in the hexdump</p>
<p>TryCatch analysis via disasm and graph</p>
<p>View symbols, class, imports as tree</p>
<p>Compact large symbol names grouped by prefix</p>
<p>Optimize listings filtering</p>
<p>New advanced data analysis widget</p>
<p>Hand cursor over clickable items in disasm</p>
<p>Enable the bounded scrollbar by default</p>
</description>
</release>
<release version="6.2.2" date="2026-09-06">
<description>
<p>Scrollbars in disasm and hexdump</p>
<p>Configurable navigation bar block resolution</p>
<p>Better column order for a variety of listings</p>
<p>Zoomable branch lines with better highlights</p>
<p>Fix behaviour when closing load options dialog</p>
<p>Fix map and undo/redo write cache actions</p>
<p>Add tab for listing demangler plugins</p>
<p>Allow to edit system color themes</p>
<p>Toggle platform r2pm package listings and better error messages</p>
<p>Add function bound lines</p>
<p>Adaptative branch lines width</p>
<p>Persist load option values across runs</p>
<p>Respect map address in bininfo details</p>
</description>
</release>
<release version="6.2.0" date="2026-07-10">
<description>
<p>Support r2-6.2.0 api changes</p>
<p>Add iaito -H to list and show iaito vars</p>
</description>
</release>
<release version="6.1.8" date="2026-06-23">
<description>
<p>Add support for deeplinks (iaito://)</p>
+6 -2
View File
@@ -95,8 +95,12 @@ QVector<QDir> PluginManager::getPluginDirectories() const
#else
QChar listSeparator = QDir::listSeparator();
#endif
QString extra_plugin_dirs = IAITO_EXTRA_PLUGIN_DIRS;
for (auto &path : extra_plugin_dirs.split(listSeparator, IAITO_QT_SKIP_EMPTY_PARTS)) {
QStringList extraPluginDirs = QString::fromLocal8Bit(IAITO_EXTRA_PLUGIN_DIRS)
.split(listSeparator, IAITO_QT_SKIP_EMPTY_PARTS);
extraPluginDirs.append(
QString::fromLocal8Bit(qgetenv("IAITO_EXTRA_PLUGIN_DIRS"))
.split(listSeparator, IAITO_QT_SKIP_EMPTY_PARTS));
for (const auto &path : extraPluginDirs) {
result.push_back(QDir(path));
}
+2
View File
@@ -100,6 +100,8 @@
<file>img/icons/delete.svg</file>
<file>img/icons/previous.svg</file>
<file>img/icons/list.svg</file>
<file>img/icons/name-tree.svg</file>
<file>img/icons/name-list.svg</file>
<file>img/icons/fork.svg</file>
<file>img/icons/plugins.svg</file>
<file>img/icons/light/plugins.svg</file>
+9 -9
View File
@@ -1,21 +1,21 @@
[meta]
name=Cobalt
skin=modern
chrome=accent
chrome=accent-tabs
dark=true
[palette]
background=#16213a
panel=#0f1830
surface=#1f2d4d
border=#2c3f66
text=#e6ebf5
mutedText=#8a97b5
accent=#2f5fd0
background=#15213b
panel=#0d172d
surface=#1d2c4a
border=#344c78
text=#e8edf7
mutedText=#98a7c3
accent=#3478e5
accentText=#ffffff
[metrics]
radius=4
radius=5
lineWidth=1
focusWidth=2
spacing=6
+2 -2
View File
@@ -25,8 +25,8 @@ spacing=6
sliderGroove=4
[palette]
accent=#039be5
accentText=#ffffff
accent=#c6dff2
accentText=#00162e
background=#fafcfe
border=#bdbdbd
mutedText=#7d7d7d
+275
View File
@@ -0,0 +1,275 @@
#include "AddressScrollBar.h"
#include "common/TempConfig.h"
#include "core/Iaito.h"
#include <QApplication>
#include <QEasingCurve>
#include <QJsonObject>
#include <QVariantAnimation>
#include <QWheelEvent>
namespace {
// Half of the travel of the spring handle, zero is the middle of the trough
constexpr int SpringRange = 1000;
constexpr int SpringPageStep = 400;
// Travel kept on both sides of the rest position so the view can always scroll
constexpr int SpringMargin = 100;
// Largest span the bounded mode maps one byte per slider unit
constexpr RVA MaxSliderSpan = RVA(1) << 30;
constexpr int SpringTickMs = 16;
constexpr int BounceMs = 700;
// Screens per second when the handle is pulled all the way, once accelerated
constexpr qreal MaxPagesPerSecond = 6.0;
// Seconds the handle must be held before reaching full speed
constexpr qreal AccelerationSeconds = 3.0;
} // namespace
AddressScrollBar::AddressScrollBar(QWidget *parent)
: QScrollBar(Qt::Vertical, parent)
, mode(Config()->getMemoryScrollBarMode())
, bounce(new QVariantAnimation(this))
{
springTimer.setInterval(SpringTickMs);
springTimer.setTimerType(Qt::PreciseTimer);
bounce->setDuration(BounceMs);
bounce->setEasingCurve(QEasingCurve::OutElastic);
connect(bounce, &QVariantAnimation::valueChanged, this, [this](const QVariant &v) {
syncing = true;
setValue(v.toInt());
syncing = false;
});
connect(&springTimer, &QTimer::timeout, this, &AddressScrollBar::springTick);
connect(this, &QAbstractSlider::sliderPressed, this, &AddressScrollBar::startSpring);
connect(this, &QAbstractSlider::sliderReleased, this, &AddressScrollBar::releaseSpring);
connect(this, &QAbstractSlider::actionTriggered, this, &AddressScrollBar::onActionTriggered);
connect(this, &QAbstractSlider::valueChanged, this, &AddressScrollBar::onValueChanged);
connect(
Config(),
&Configuration::memoryScrollBarModeChanged,
this,
[this](Configuration::MemoryScrollBarMode newMode) {
mode = newMode;
applyMode();
});
connect(Core(), &IaitoCore::refreshAll, this, &AddressScrollBar::updateRange);
applyMode();
}
void AddressScrollBar::setViewport(int lines, RVA bytes)
{
pageLines = qMax(1, lines);
pageBytes = qMax<RVA>(1, bytes);
if (!isSpring() && !isHiddenMode()) {
applyRange();
}
}
void AddressScrollBar::setAddress(RVA newAddress)
{
address = newAddress;
if (isSpring() || isHiddenMode()) {
return;
}
int v = 0;
if (address >= rangeEnd) {
v = maximum();
} else if (address > rangeStart) {
v = int((address - rangeStart) / unit);
}
syncing = true;
setValue(v);
syncing = false;
}
void AddressScrollBar::setSeekAddress(RVA newAddress)
{
seekAddress = newAddress;
updateRest();
}
void AddressScrollBar::updateRange()
{
if (isHiddenMode()) {
return;
}
RVA from = 0;
RVA to = 0;
// Same boundaries the navigation bar shows, the whole file when unmapped
for (const char *where : {"bin.sections", "io.maps"}) {
TempConfig tempConfig;
tempConfig.set(QStringLiteral("search.in"), QString::fromLatin1(where));
const QJsonObject stats = Core()->cmdj(QStringLiteral("p-j 1")).object();
from = stats[QStringLiteral("from")].toVariant().toULongLong();
to = stats[QStringLiteral("to")].toVariant().toULongLong();
if (to > from) {
break;
}
}
if (to <= from) {
from = 0;
to = RVA_MAX;
}
rangeStart = from;
rangeEnd = to;
if (isSpring()) {
updateRest();
} else {
applyRange();
}
}
// Rests the spring handle where the seek lies within the range
void AddressScrollBar::updateRest()
{
if (!isSpring()) {
return;
}
const RVA span = rangeEnd - rangeStart;
qreal position = 0.5;
if (span > 0 && seekAddress > rangeStart) {
position = seekAddress >= rangeEnd ? 1.0 : qreal(seekAddress - rangeStart) / span;
}
const int travel = SpringRange - SpringMargin;
restValue = qRound(-travel + 2 * travel * position);
if (!isSliderDown() && bounce->state() != QAbstractAnimation::Running) {
syncing = true;
setValue(restValue);
syncing = false;
}
}
void AddressScrollBar::applyMode()
{
springTimer.stop();
bounce->stop();
springRemainder = 0;
setVisible(!isHiddenMode());
syncing = true;
if (isSpring()) {
setRange(-SpringRange, SpringRange);
setPageStep(SpringPageStep);
setSingleStep(1);
setToolTip(tr("Drag away from the seek to scroll, further and longer is faster"));
} else {
setToolTip(QString());
}
syncing = false;
updateRange();
}
void AddressScrollBar::applyRange()
{
const RVA span = rangeEnd - rangeStart;
// 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)));
setSingleStep(qMax<int>(1, int(pageBytes / pageLines / unit)));
syncing = false;
setAddress(address);
}
void AddressScrollBar::startSpring()
{
if (!isSpring()) {
return;
}
bounce->stop();
springRemainder = 0;
holdTimer.start();
tickTimer.start();
springTimer.start();
}
void AddressScrollBar::releaseSpring()
{
if (!isSpring()) {
return;
}
springTimer.stop();
bounce->stop();
bounce->setStartValue(value());
bounce->setEndValue(restValue);
bounce->start();
}
void AddressScrollBar::springTick()
{
const qreal dt = tickTimer.restart() / 1000.0;
const int offset = value() - restValue;
// Normalized by the travel left on that side, so the edge is always full speed
const int travel = qMax(1, offset > 0 ? SpringRange - restValue : SpringRange + restValue);
const qreal pull = qBound(-1.0, offset / qreal(travel), 1.0);
if (qFuzzyIsNull(pull)) {
springRemainder = 0;
return;
}
const qreal hold = holdTimer.elapsed() / 1000.0;
const qreal ramp = qMin(1.0, 0.1 + hold / AccelerationSeconds);
// Quadratic so that small pulls stay slow enough to read the listing
const qreal pagesPerSecond = pull * qAbs(pull) * MaxPagesPerSecond * ramp;
springRemainder += pagesPerSecond * pageLines * dt;
const int lines = int(springRemainder);
if (lines != 0) {
springRemainder -= lines;
emit scrollRequested(lines);
}
}
void AddressScrollBar::onActionTriggered(int action)
{
if (!isSpring()) {
return;
}
int lines = 0;
switch (action) {
case SliderSingleStepAdd:
lines = 1;
break;
case SliderSingleStepSub:
lines = -1;
break;
case SliderPageStepAdd:
lines = pageLines;
break;
case SliderPageStepSub:
lines = -pageLines;
break;
case SliderMove:
return;
default:
break;
}
// Steps scroll the view but never move the handle away from its rest
setSliderPosition(restValue);
if (lines != 0) {
emit scrollRequested(lines);
}
}
void AddressScrollBar::onValueChanged(int value)
{
if (syncing || isSpring()) {
return;
}
const RVA target = rangeStart + RVA(value) * unit;
if (target != address) {
address = target;
emit addressRequested(target);
}
}
void AddressScrollBar::wheelEvent(QWheelEvent *event)
{
if (!isSpring()) {
QScrollBar::wheelEvent(event);
return;
}
const int lines = -(event->angleDelta().y() / 120) * QApplication::wheelScrollLines();
if (lines != 0) {
emit scrollRequested(qBound(-pageLines, lines, pageLines));
}
event->accept();
}
+79
View File
@@ -0,0 +1,79 @@
#ifndef ADDRESSSCROLLBAR_H
#define ADDRESSSCROLLBAR_H
#include "common/Configuration.h"
#include "core/IaitoCommon.h"
#include <QElapsedTimer>
#include <QScrollBar>
#include <QTimer>
class QVariantAnimation;
/**
* @brief Vertical scrollbar for views that browse the whole 64 bit address space.
*
* Spring mode: the handle rests where the current seek lies within the
* navigation bar range. Dragging it scrolls with a speed that grows with the
* displacement and with the time it is held, and the handle bounces back to
* the rest position on release.
* Bounded mode: a regular scrollbar mapped over the navigation bar range.
* Hidden mode: no scrollbar at all.
* The mode follows Configuration::getMemoryScrollBarMode().
*/
class AddressScrollBar : public QScrollBar
{
Q_OBJECT
public:
explicit AddressScrollBar(QWidget *parent = nullptr);
/// Lines shown per screen and the bytes they cover, used for the steps
void setViewport(int lines, RVA bytes);
/// Sync the handle with the top address of the view without emitting anything
void setAddress(RVA address);
/// Current seek of the view, the rest position of the spring handle
void setSeekAddress(RVA address);
/// Re-fetch the address range of the bounded mode from the core
void updateRange();
bool isHiddenMode() const { return mode == Configuration::MemoryScrollBarMode::Hidden; }
signals:
/// Spring mode: scroll the view by `lines` (positive scrolls down)
void scrollRequested(int lines);
/// Bounded mode: show `address` at the top of the view
void addressRequested(RVA address);
protected:
void wheelEvent(QWheelEvent *event) override;
private:
Configuration::MemoryScrollBarMode mode;
int pageLines = 1;
RVA pageBytes = 1;
RVA address = 0;
RVA seekAddress = 0;
int restValue = 0;
RVA rangeStart = 0;
RVA rangeEnd = 0;
/// Bytes per slider unit in bounded mode, so any span fits in an int
RVA unit = 1;
bool syncing = false;
QTimer springTimer;
QElapsedTimer holdTimer;
QElapsedTimer tickTimer;
qreal springRemainder = 0;
QVariantAnimation *bounce;
bool isSpring() const { return mode == Configuration::MemoryScrollBarMode::Spring; }
void applyMode();
void applyRange();
void updateRest();
void springTick();
void startSpring();
void releaseSpring();
void onActionTriggered(int action);
void onValueChanged(int value);
};
#endif // ADDRESSSCROLLBAR_H
+94
View File
@@ -4,7 +4,11 @@
#include <QAction>
#include <QContextMenuEvent>
#include <QEvent>
#include <QLabel>
#include <QMenu>
#include <QStyle>
#include <QToolBar>
#include <QVBoxLayout>
AddressableDockWidget::AddressableDockWidget(MainWindow *parent)
: IaitoDockWidget(parent)
@@ -19,9 +23,99 @@ AddressableDockWidget::AddressableDockWidget(MainWindow *parent)
dockMenu = new QMenu(this);
dockMenu->addAction(&syncAction);
connect(seekable, &IaitoSeekable::syncChanged, this, [this]() {
if (!seekable->isSynchronized()) {
setupSeekHistoryBar();
}
updateSeekHistoryBar();
});
connect(
seekable,
&IaitoSeekable::historyChanged,
this,
&AddressableDockWidget::updateSeekHistoryBar);
connect(
seekable,
&IaitoSeekable::seekableSeekChanged,
this,
&AddressableDockWidget::updateSeekHistoryBar);
connect(this, &QDockWidget::visibilityChanged, this, [this](bool visible) {
if (visible) {
setupSeekHistoryBar();
}
});
setContextMenuPolicy(Qt::ContextMenuPolicy::DefaultContextMenu);
}
void AddressableDockWidget::setupSeekHistoryBar(bool alwaysVisible)
{
historyBarAlwaysVisible |= alwaysVisible;
if (seekHistoryBar || !widget()) {
updateSeekHistoryBar();
return;
}
QWidget *contents = widget();
seekHistoryBar = new QToolBar(contents);
seekHistoryBar->setObjectName(QStringLiteral("seekHistoryBar"));
seekHistoryBar->setIconSize(QSize(16, 16));
seekHistoryBar->setMovable(false);
seekHistoryBar->setFloatable(false);
seekHistoryBar->setFocusPolicy(Qt::NoFocus);
seekBackAction
= seekHistoryBar->addAction(style()->standardIcon(QStyle::SP_ArrowBack), tr("Back"));
seekBackAction->setObjectName(QStringLiteral("seekBackAction"));
seekForwardAction
= seekHistoryBar->addAction(style()->standardIcon(QStyle::SP_ArrowForward), tr("Forward"));
seekForwardAction->setObjectName(QStringLiteral("seekForwardAction"));
connect(seekBackAction, &QAction::triggered, seekable, &IaitoSeekable::seekPrev);
connect(seekForwardAction, &QAction::triggered, seekable, &IaitoSeekable::seekNext);
for (QAction *action : {seekBackAction, seekForwardAction}) {
seekHistoryBar->widgetForAction(action)->setFocusPolicy(Qt::NoFocus);
}
seekAddressLabel = new QLabel(seekHistoryBar);
seekAddressLabel->setMargin(3);
seekHistoryBar->addWidget(seekAddressLabel);
if (auto *layout = qobject_cast<QVBoxLayout *>(contents->layout())) {
layout->insertWidget(0, seekHistoryBar);
} else {
// Graph-only docks have no content layout in which to insert the bar.
auto *container = new QWidget(this);
auto *containerLayout = new QVBoxLayout(container);
containerLayout->setContentsMargins(0, 0, 0, 0);
containerLayout->setSpacing(0);
containerLayout->addWidget(seekHistoryBar);
containerLayout->addWidget(contents);
setWidget(container);
contents->show();
}
updateSeekHistoryBar();
}
void AddressableDockWidget::setSeekHistoryBarEnabled(bool enabled)
{
historyBarEnabled = enabled;
updateSeekHistoryBar();
}
void AddressableDockWidget::updateSeekHistoryBar()
{
if (!seekHistoryBar) {
return;
}
const bool synced = seekable->isSynchronized();
seekHistoryBar->setVisible(historyBarEnabled && (historyBarAlwaysVisible || !synced));
seekBackAction->setEnabled(seekable->canSeekPrev());
seekForwardAction->setEnabled(seekable->canSeekNext());
seekBackAction->setToolTip(synced ? tr("Back in shared seek history") : tr("Back in this view"));
seekForwardAction->setToolTip(
synced ? tr("Forward in shared seek history") : tr("Forward in this view"));
seekAddressLabel->setText(RAddressString(seekable->getOffset()));
seekAddressLabel->setToolTip(
synced ? tr("Shared offset") : tr("Independent offset for this view"));
}
QVariantMap AddressableDockWidget::serializeViewProprties()
{
auto result = IaitoDockWidget::serializeViewProprties();
+15
View File
@@ -7,6 +7,8 @@
#include <QAction>
class IaitoSeekable;
class QLabel;
class QToolBar;
class AddressableDockWidget : public IaitoDockWidget
{
@@ -28,8 +30,21 @@ protected:
QAction syncAction;
QMenu *dockMenu = nullptr;
void setupSeekHistoryBar(bool alwaysVisible = false);
void setSeekHistoryBarEnabled(bool enabled);
virtual QString getWindowTitle() const = 0;
void contextMenuEvent(QContextMenuEvent *event) override;
private:
QToolBar *seekHistoryBar = nullptr;
QLabel *seekAddressLabel = nullptr;
QAction *seekBackAction = nullptr;
QAction *seekForwardAction = nullptr;
bool historyBarAlwaysVisible = false;
bool historyBarEnabled = true;
void updateSeekHistoryBar();
};
#endif // ADDRESSABLE_DOCK_WIDGET_H
+64 -57
View File
@@ -5,6 +5,7 @@
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QMenu>
#include <QSet>
#include <QSortFilterProxyModel>
#include "IaitoDockWidget.h"
@@ -45,16 +46,42 @@ public:
void setAddressModel(AddressableItemModelI *model)
{
this->addressableModel = model;
setModel(model ? model->asItemModel() : nullptr);
}
BaseListWidget::setModel(this->addressableModel->asItemModel());
void setModel(QAbstractItemModel *model) override
{
BaseListWidget::setModel(model);
if (this->selectionModel()) {
this->connect(
this->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
&AddressableItemList<BaseListWidget>::onSelectedItemChanged);
}
}
this->connect(
this->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
&AddressableItemList<BaseListWidget>::onSelectedItemChanged);
auto cast_model = dynamic_cast<QAbstractItemModel *>(model);
IaitoTreeView::setModel(cast_model);
RVA itemAddress(const QModelIndex &index) const
{
auto provider = addressProvider();
auto source = mapToProviderIndex(provider, index);
return source.isValid() ? provider->address(source) : RVA_INVALID;
}
QList<RVA> selectedAddresses() const
{
QList<RVA> result;
QSet<RVA> seen;
if (auto selection = this->selectionModel()) {
for (const auto &index : selection->selectedIndexes()) {
const RVA address = itemAddress(index);
if (address != RVA_INVALID && !seen.contains(address)) {
seen.insert(address);
result.append(address);
}
}
}
return result;
}
void setMainWindow(MainWindow *mainWindow)
@@ -76,21 +103,17 @@ public:
protected:
virtual void showItemContextMenu(const QPoint &pt)
{
auto provider = addressProvider();
if (!provider) {
if (!itemContextMenu) {
return;
}
auto index = this->currentIndex();
if (index.isValid() && itemContextMenu) {
QModelIndex pIndex = mapToProviderIndex(provider, index);
if (!pIndex.isValid()) {
return;
}
auto offset = provider->address(pIndex);
auto name = provider->name(pIndex);
itemContextMenu->setTarget(offset, name);
itemContextMenu->exec(this->mapToGlobal(pt));
const auto index = this->indexAt(pt);
if (this->selectionModel()->isSelected(index)) {
this->selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
} else {
this->setCurrentIndex(index);
}
updateMenuFromItem(index);
itemContextMenu->exec(this->viewport()->mapToGlobal(pt));
}
virtual void onItemActivated(const QModelIndex &index)
@@ -107,18 +130,21 @@ protected:
return;
}
auto offset = provider->address(pIndex);
Core()->seekAndShow(offset);
if (offset != RVA_INVALID) {
Core()->seekAndShow(offset);
}
}
virtual void onSelectedItemChanged(const QModelIndex &index) { updateMenuFromItem(index); }
void updateMenuFromItem(const QModelIndex &index)
{
auto provider = addressProvider();
if (!provider) {
if (!provider || !itemContextMenu) {
return;
}
if (index.isValid()) {
QModelIndex pIndex = mapToProviderIndex(provider, index);
if (!pIndex.isValid()) {
if (!pIndex.isValid() || provider->address(pIndex) == RVA_INVALID) {
itemContextMenu->clearTarget();
return;
}
auto offset = provider->address(pIndex);
@@ -132,42 +158,21 @@ protected:
// Handle 'j' and 'k' keys to navigate and seek without pressing Enter
void keyPressEvent(QKeyEvent *event) override
{
if (event->modifiers() == Qt::NoModifier) {
if (event->modifiers() == Qt::NoModifier
&& (event->key() == Qt::Key_J || event->key() == Qt::Key_K)) {
auto selModel = this->selectionModel();
const QModelIndex curr = selModel->currentIndex();
if (curr.isValid()) {
if (event->key() == Qt::Key_J) {
// Move down
const QModelIndex parent = curr.parent();
const int total = this->model()->rowCount(parent);
const int newRow = qMin(curr.row() + 1, total - 1);
if (newRow != curr.row()) {
const QModelIndex newIndex
= this->model()->index(newRow, curr.column(), parent);
selModel->setCurrentIndex(
newIndex,
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
this->scrollTo(newIndex);
onItemActivated(newIndex);
this->setFocus();
}
return;
} else if (event->key() == Qt::Key_K) {
// Move up
if (curr.row() > 0) {
const QModelIndex parent = curr.parent();
const int newRow = curr.row() - 1;
const QModelIndex newIndex
= this->model()->index(newRow, curr.column(), parent);
selModel->setCurrentIndex(
newIndex,
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
this->scrollTo(newIndex);
onItemActivated(newIndex);
this->setFocus();
}
return;
auto tree = qobject_cast<QTreeView *>(this);
if (selModel && tree && this->currentIndex().isValid()) {
const auto next = event->key() == Qt::Key_J
? tree->indexBelow(this->currentIndex())
: tree->indexAbove(this->currentIndex());
if (next.isValid()) {
selModel->setCurrentIndex(
next, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
this->scrollTo(next);
onItemActivated(next);
}
return;
}
}
BaseListWidget::keyPressEvent(event);
@@ -228,4 +233,6 @@ private:
}
};
using AddressableItemView = AddressableItemList<>;
#endif // ADDRESSABLE_ITEM_LIST_H
+2 -2
View File
@@ -28,7 +28,7 @@
<number>0</number>
</property>
<item>
<widget class="AddressableItemList&lt;&gt;" name="breakpointTreeView">
<widget class="AddressableItemView" name="breakpointTreeView">
<property name="styleSheet">
<string notr="true">IaitoTreeView::item
{
@@ -80,7 +80,7 @@
</widget>
<customwidgets>
<customwidget>
<class>AddressableItemList&lt;&gt;</class>
<class>AddressableItemView</class>
<extends>QTreeView</extends>
<header>widgets/AddressableItemList.h</header>
<container>1</container>
+145 -52
View File
@@ -1,5 +1,8 @@
#include "ClassesWidget.h"
#include "NameListView.h"
#include "QuickFilterView.h"
#include "common/Helpers.h"
#include "common/NameListModel.h"
#include "common/SvgIconEngine.h"
#include "core/MainWindow.h"
#include "dialogs/EditMethodDialog.h"
@@ -9,6 +12,29 @@
#include <QList>
#include <QMenu>
#include <QMouseEvent>
#include <QShortcut>
RVA ClassesModel::address(const QModelIndex &index) const
{
const auto offset = index.data(OffsetRole);
return offset.isValid() ? offset.value<RVA>() : RVA_INVALID;
}
QString ClassesModel::name(const QModelIndex &index) const
{
const QString member = index.sibling(index.row(), NAME).data().toString();
if (!index.parent().isValid() || index.data(TypeRole).value<RowType>() == RowType::Base
|| index.data(TypeRole).value<RowType>() == RowType::VTable) {
return member;
}
const QString owner = index.parent().data(NameRole).toString();
const QString separator = owner.endsWith(';') ? QStringLiteral("->")
: owner.contains("::") ? QStringLiteral("::")
: owner.contains('/') ? QStringLiteral("/")
: owner.contains('.') ? QStringLiteral(".")
: QStringLiteral("::");
return member.startsWith(owner + separator) ? member : owner + separator + member;
}
QVariant ClassesModel::headerData(int section, Qt::Orientation, int role) const
{
@@ -44,6 +70,9 @@ void BinClassesModel::setClasses(const QList<BinClassDescription> &classes)
QModelIndex BinClassesModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent)) {
return {};
}
if (!parent.isValid()) {
return createIndex(row, column,
(quintptr) 0); // root function nodes have id = 0
@@ -70,6 +99,9 @@ QModelIndex BinClassesModel::parent(const QModelIndex &index) const
int BinClassesModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid() && parent.column() != 0) {
return 0;
}
if (!parent.isValid()) { // root
return classes.count();
}
@@ -89,6 +121,9 @@ int BinClassesModel::columnCount(const QModelIndex &) const
QVariant BinClassesModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
const BinClassDescription *cls;
const BinClassMethodDescription *meth = nullptr;
const BinClassFieldDescription *field = nullptr;
@@ -136,6 +171,15 @@ QVariant BinClassesModel::data(const QModelIndex &index, int role) const
return QVariant::fromValue(meth->addr);
case NameRole:
return meth->name;
case MangledNameRole:
return meth->mangledName;
case NameListModel::NameToolTipRole:
case Qt::ToolTipRole:
if (!meth->mangledName.isEmpty() && meth->mangledName != meth->name) {
return tr("<qt><b>Name:</b> %1<br><b>Mangled:</b> %2</qt>")
.arg(name(index).toHtmlEscaped(), meth->mangledName.toHtmlEscaped());
}
return QStringLiteral("<qt>%1</qt>").arg(name(index).toHtmlEscaped());
case TypeRole:
return QVariant::fromValue(RowType::Method);
default:
@@ -354,6 +398,9 @@ const QVector<AnalClassesModel::Attribute> &AnalClassesModel::getAttrs(const QSt
QModelIndex AnalClassesModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent)) {
return {};
}
if (!parent.isValid()) {
return createIndex(row, column,
(quintptr) 0); // root function nodes have id = 0
@@ -380,6 +427,9 @@ QModelIndex AnalClassesModel::parent(const QModelIndex &index) const
int AnalClassesModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid() && parent.column() != 0) {
return 0;
}
if (!parent.isValid()) { // root
return classes.count();
}
@@ -393,7 +443,8 @@ int AnalClassesModel::rowCount(const QModelIndex &parent) const
bool AnalClassesModel::hasChildren(const QModelIndex &parent) const
{
return !parent.isValid() || !parent.parent().isValid();
return !parent.isValid() ? !classes.isEmpty()
: parent.column() == 0 && parent.internalId() == 0;
}
int AnalClassesModel::columnCount(const QModelIndex &) const
@@ -403,6 +454,9 @@ int AnalClassesModel::columnCount(const QModelIndex &) const
QVariant AnalClassesModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
return {};
}
if (index.internalId() == 0) { // class row
if (index.row() >= classes.count()) {
return QVariant();
@@ -533,13 +587,29 @@ QVariant AnalClassesModel::data(const QModelIndex &index, int role) const
}
ClassesSortFilterProxyModel::ClassesSortFilterProxyModel(QObject *parent)
: QSortFilterProxyModel(parent)
{}
: AddressableFilterProxyModel(nullptr, parent)
{
setFilterCaseSensitivity(Qt::CaseInsensitive);
}
bool ClassesSortFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
QModelIndex index = sourceModel()->index(row, 0, parent);
return index.data(ClassesModel::NameRole).toString().contains(FILTER_REGEX);
const auto matches = [this](const QModelIndex &item) {
return item.data(ClassesModel::NameRole).toString().contains(FILTER_REGEX)
|| item.data(ClassesModel::MangledNameRole).toString().contains(FILTER_REGEX);
};
if (filterAcceptsType(row, parent)
&& (matches(index) || (parent.isValid() && matches(parent)))) {
return true;
}
// Keep the owning class when a member matches, but hide unrelated members.
for (int child = 0; child < sourceModel()->rowCount(index); ++child) {
if (filterAcceptsRow(child, index)) {
return true;
}
}
return false;
}
bool ClassesSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
@@ -577,11 +647,6 @@ bool ClassesSortFilterProxyModel::lessThan(const QModelIndex &left, const QModel
}
}
bool ClassesSortFilterProxyModel::hasChildren(const QModelIndex &parent) const
{
return !parent.isValid() || !parent.parent().isValid();
}
ClassesWidget::ClassesWidget(MainWindow *main)
: IaitoDockWidget(main)
, ui(new Ui::ClassesWidget)
@@ -592,9 +657,32 @@ ClassesWidget::ClassesWidget(MainWindow *main)
proxy_model = new ClassesSortFilterProxyModel(this);
ui->classesTreeView->setModel(proxy_model);
nameMenu = new QMenu(this);
names = new NameListView(
ui->classesTreeView, nameMenu, proxy_model, ClassesModel::NAME, objectName());
auto filter = new QuickFilterView(this);
filter->setTypeFilter(proxy_model, ClassesModel::TYPE);
ui->verticalLayout->insertWidget(1, filter);
filter->addActionButton(names->toggleAction());
connect(
filter,
&QuickFilterView::filterTextChanged,
proxy_model,
&AddressableFilterProxyModel::setFilterWildcard);
connect(
filter,
&QuickFilterView::filterClosed,
ui->classesTreeView,
static_cast<void (QWidget::*)()>(&QWidget::setFocus));
auto find = new QShortcut(QKeySequence::Find, this);
find->setContext(Qt::WidgetWithChildrenShortcut);
connect(find, &QShortcut::activated, filter, &QuickFilterView::showFilter);
auto clear = new QShortcut(QKeySequence(Qt::Key_Escape), this);
clear->setContext(Qt::WidgetWithChildrenShortcut);
connect(clear, &QShortcut::activated, filter, &QuickFilterView::clearFilter);
ui->classesTreeView->sortByColumn(ClassesModel::TYPE, Qt::AscendingOrder);
ui->classSourceCombo->setCurrentIndex(1);
ui->classSourceCombo->setCurrentIndex(0);
connect<void (QComboBox::*)(int)>(
ui->classSourceCombo, &QComboBox::currentIndexChanged, this, &ClassesWidget::refreshClasses);
@@ -603,11 +691,21 @@ ClassesWidget::ClassesWidget(MainWindow *main)
&QTreeView::customContextMenuRequested,
this,
&ClassesWidget::showContextMenu);
connect(Core(), &IaitoCore::refreshAll, this, &ClassesWidget::refreshClasses);
connect(Core(), &IaitoCore::codeRebased, this, &ClassesWidget::refreshClasses);
refreshClasses();
}
ClassesWidget::~ClassesWidget() {}
ClassesWidget::~ClassesWidget()
{
ui->classesTreeView->setModel(nullptr);
}
QModelIndex ClassesWidget::currentSourceIndex() const
{
return names->presentationModel()->mapToSource(ui->classesTreeView->currentIndex());
}
ClassesWidget::Source ClassesWidget::getSource()
{
@@ -625,7 +723,7 @@ void ClassesWidget::refreshClasses()
case Source::BIN:
if (!bin_model) {
bin_model = new BinClassesModel(this);
proxy_model->setSourceModel(bin_model);
proxy_model->setClassesModel(bin_model);
delete anal_model;
anal_model = nullptr;
}
@@ -634,7 +732,7 @@ void ClassesWidget::refreshClasses()
case Source::ANAL:
if (!anal_model) {
anal_model = new AnalClassesModel(this);
proxy_model->setSourceModel(anal_model);
proxy_model->setClassesModel(anal_model);
delete bin_model;
bin_model = nullptr;
}
@@ -651,59 +749,54 @@ void ClassesWidget::on_classesTreeView_doubleClicked(const QModelIndex &index)
if (!index.isValid())
return;
const auto type = index.data(ClassesModel::TypeRole);
if (!type.isValid() || type.value<ClassesModel::RowType>() == ClassesModel::RowType::Class) {
return;
}
QVariant offsetData = index.data(ClassesModel::OffsetRole);
if (!offsetData.isValid()) {
return;
}
RVA offset = offsetData.value<RVA>();
Core()->seekAndShow(offset);
if (offset != RVA_INVALID) {
Core()->seekAndShow(offset);
}
}
void ClassesWidget::showContextMenu(const QPoint &pt)
{
if (!anal_model) {
// no context menu for bin classes
return;
}
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
if (!index.isValid()) {
return;
}
auto type = static_cast<ClassesModel::RowType>(index.data(ClassesModel::TypeRole).toInt());
ui->classesTreeView->setCurrentIndex(ui->classesTreeView->indexAt(pt));
const QModelIndex index = currentSourceIndex();
const auto type = index.data(ClassesModel::TypeRole).value<ClassesModel::RowType>();
QMenu menu(ui->classesTreeView);
menu.addAction(ui->newClassAction);
if (type == ClassesModel::RowType::Class) {
menu.addAction(ui->renameClassAction);
menu.addAction(ui->deleteClassAction);
}
menu.addSeparator();
menu.addAction(ui->addMethodAction);
if (type == ClassesModel::RowType::Method) {
menu.addAction(ui->editMethodAction);
QString className = index.parent().data(ClassesModel::NameRole).toString();
QString methodName = index.data(ClassesModel::NameRole).toString();
AnalMethodDescription desc;
if (Core()->getAnalMethod(className, methodName, &desc)) {
if (desc.vtableOffset >= 0) {
menu.addAction(ui->seekToVTableAction);
if (anal_model) {
menu.addAction(ui->newClassAction);
if (index.isValid()) {
if (type == ClassesModel::RowType::Class) {
menu.addAction(ui->renameClassAction);
menu.addAction(ui->deleteClassAction);
}
menu.addAction(ui->addMethodAction);
if (type == ClassesModel::RowType::Method) {
menu.addAction(ui->editMethodAction);
const QString className = index.parent().data(ClassesModel::NameRole).toString();
const QString methodName = index.data(ClassesModel::NameRole).toString();
AnalMethodDescription desc;
if (Core()->getAnalMethod(className, methodName, &desc) && desc.vtableOffset >= 0) {
menu.addAction(ui->seekToVTableAction);
}
}
}
}
menu.exec(ui->classesTreeView->mapToGlobal(pt));
menu.addActions(nameMenu->actions());
menu.exec(ui->classesTreeView->viewport()->mapToGlobal(pt));
}
void ClassesWidget::on_seekToVTableAction_triggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = currentSourceIndex();
QString className = index.parent().data(ClassesModel::NameRole).toString();
QList<AnalVTableDescription> vtables = Core()->getAnalClassVTables(className);
@@ -726,7 +819,7 @@ void ClassesWidget::on_seekToVTableAction_triggered()
void ClassesWidget::on_addMethodAction_triggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = currentSourceIndex();
if (!index.isValid()) {
return;
}
@@ -744,7 +837,7 @@ void ClassesWidget::on_addMethodAction_triggered()
void ClassesWidget::on_editMethodAction_triggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = currentSourceIndex();
if (!index.isValid()
|| index.data(ClassesModel::TypeRole).toInt()
!= static_cast<int>(ClassesModel::RowType::Method)) {
@@ -767,7 +860,7 @@ void ClassesWidget::on_newClassAction_triggered()
void ClassesWidget::on_deleteClassAction_triggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = currentSourceIndex();
if (!index.isValid()
|| index.data(ClassesModel::TypeRole).toInt()
!= static_cast<int>(ClassesModel::RowType::Class)) {
@@ -786,7 +879,7 @@ void ClassesWidget::on_deleteClassAction_triggered()
void ClassesWidget::on_renameClassAction_triggered()
{
QModelIndex index = ui->classesTreeView->selectionModel()->currentIndex();
QModelIndex index = currentSourceIndex();
if (!index.isValid()
|| index.data(ClassesModel::TypeRole).toInt()
!= static_cast<int>(ClassesModel::RowType::Class)) {
+16 -4
View File
@@ -4,6 +4,7 @@
#include <memory>
#include "IaitoDockWidget.h"
#include "common/AddressableItemModel.h"
#include "core/Iaito.h"
#include <QAbstractListModel>
@@ -13,6 +14,8 @@ namespace Ui {
class ClassesWidget;
}
class NameListView;
class QMenu;
class QTreeWidget;
class QTreeWidgetItem;
class MainWindow;
@@ -21,7 +24,7 @@ class ClassesWidget;
/**
* @brief Common abstract base class for Bin and Anal classes models
*/
class ClassesModel : public QAbstractItemModel
class ClassesModel : public AddressableItemModel<>
{
public:
enum Columns { NAME = 0, TYPE, OFFSET, VTABLE, COUNT };
@@ -60,13 +63,16 @@ public:
* by vtable offset
*/
static const int VTableRole = Qt::UserRole + 3;
static const int MangledNameRole = Qt::UserRole + 4;
explicit ClassesModel(QObject *parent = nullptr)
: QAbstractItemModel(parent)
: AddressableItemModel<>(parent)
{}
QVariant headerData(
int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
RVA address(const QModelIndex &index) const override;
QString name(const QModelIndex &index) const override;
};
Q_DECLARE_METATYPE(ClassesModel::RowType)
@@ -158,17 +164,20 @@ public slots:
void classAttrsChanged(const QString &cls);
};
class ClassesSortFilterProxyModel : public QSortFilterProxyModel
class ClassesSortFilterProxyModel : public AddressableFilterProxyModel
{
Q_OBJECT
public:
explicit ClassesSortFilterProxyModel(QObject *parent = nullptr);
void setClassesModel(ClassesModel *model)
{
AddressableFilterProxyModel::setSourceModel(static_cast<AddressableItemModelI *>(model));
}
protected:
bool filterAcceptsRow(int row, const QModelIndex &parent) const override;
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
bool hasChildren(const QModelIndex &parent = QModelIndex()) const override;
};
class ClassesWidget : public IaitoDockWidget
@@ -203,6 +212,9 @@ private:
BinClassesModel *bin_model = nullptr;
AnalClassesModel *anal_model = nullptr;
ClassesSortFilterProxyModel *proxy_model;
NameListView *names;
QMenu *nameMenu;
QModelIndex currentSourceIndex() const;
};
#endif // CLASSESWIDGET_H
+75
View File
@@ -244,6 +244,24 @@ void CodeInfoWidget::buildUi()
functionTree->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
functionTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
layout->addWidget(functionTree);
exceptionStatus = new QLabel(functionSection);
exceptionStatus->setWordWrap(true);
layout->addWidget(exceptionStatus);
exceptionTree = new QTreeWidget(functionSection);
exceptionTree->setHeaderLabels({tr("Exception"), tr("Address / value")});
exceptionTree->setRootIsDecorated(true);
exceptionTree->setAlternatingRowColors(true);
exceptionTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
exceptionTree->setToolTip(tr("Activate an address to navigate. Range ends are exclusive."));
exceptionTree->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
connect(exceptionTree, &QTreeWidget::itemActivated, this, [](QTreeWidgetItem *item, int) {
const auto address = item->data(0, Qt::UserRole);
if (address.isValid()) {
Core()->seek(address.toULongLong());
}
});
layout->addWidget(exceptionTree);
}
rootLayout->addWidget(functionSection);
@@ -400,6 +418,7 @@ void CodeInfoWidget::refresh()
const RVA off = Core()->getOffset();
const QString addrStr = RAddressString(off);
refreshExceptions(off);
// Opcode (aoj)
QJsonDocument opDoc = Core()->cmdj("aoj");
@@ -492,6 +511,62 @@ void CodeInfoWidget::refresh()
}
}
void CodeInfoWidget::refreshExceptions(RVA address)
{
exceptionTree->clear();
const RVA function = Core()->getFunctionStart(address);
auto regions = Core()->getExceptionRegions(function);
if (function == RVA_INVALID) {
for (auto it = regions.begin(); it != regions.end();) {
if (!it->touches(address, 1)) {
it = regions.erase(it);
} else {
++it;
}
}
}
exceptionStatus->setText(
regions.isEmpty()
? tr("No exception metadata reported for this function.")
: tr("Exception handlers (%1) · activate an address to navigate").arg(regions.size()));
exceptionTree->setVisible(!regions.isEmpty());
exceptionStatus->setVisible(!regions.isEmpty());
for (const auto &region : regions) {
auto *entry = new QTreeWidgetItem(
exceptionTree,
{QStringLiteral("#%1 · %2").arg(region.index).arg(region.label()),
region.contains(address) ? tr("Protects current instruction") : QString()});
entry->setToolTip(
0,
region.description().toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("<br>")));
QPixmap swatch(12, 12);
swatch.fill(region.color());
entry->setIcon(0, QIcon(swatch));
const auto addAddress = [entry](const QString &name, quint64 value) {
auto *item = new QTreeWidgetItem(entry, {name, RAddressString(value)});
item->setData(0, Qt::UserRole, QVariant::fromValue(value));
};
addAddress(tr("Try start"), region.from);
addAddress(tr("Try end (exclusive)"), region.to);
addAddress(tr("Handler"), region.handler);
if (region.filter && region.filter != ExceptionRegion::Invalid) {
if (region.filter > 1) {
addAddress(tr("Filter"), region.filter);
} else {
new QTreeWidgetItem(entry, {tr("Filter constant"), QString::number(region.filter)});
}
}
if (region.typeFilter) {
new QTreeWidgetItem(entry, {tr("Type filter"), QString::number(region.typeFilter)});
}
if (region.source != ExceptionRegion::Invalid) {
addAddress(tr("Metadata source"), region.source);
}
entry->setExpanded(true);
}
fitTreeToContent(exceptionTree);
}
void CodeInfoWidget::onPatchInstruction()
{
if (mainWindow) {
+3
View File
@@ -57,6 +57,7 @@ private:
void buildUi();
void populateTree(QTreeWidget *tree, const QJsonValue &value);
void addJsonNode(QTreeWidgetItem *parent, const QString &key, const QJsonValue &value);
void refreshExceptions(RVA address);
CodeInfoSection *opcodeSection;
CodeInfoSection *blockSection;
@@ -73,6 +74,8 @@ private:
QTreeWidget *opcodeTree;
QTreeWidget *blockTree;
QTreeWidget *functionTree;
QTreeWidget *exceptionTree;
QLabel *exceptionStatus;
QPushButton *btnPatchInstr;
QPushButton *btnPatchBytes;
+2 -2
View File
@@ -399,7 +399,7 @@ void ColorShowWidget::setColor(const QColor &c)
void ColorShowWidget::paintEvent(QPaintEvent *event)
{
QPainter p(this);
const int miniRectWidth = rect().width() / 2;
const int miniRectWidth = qMax(2, rect().width() / 2);
for (int y = rect().topLeft().ry(); y < rect().bottomRight().ry(); y++) {
for (int x = rect().topLeft().rx(); x < rect().bottomRight().rx(); x++) {
p.setPen(
@@ -438,7 +438,7 @@ void AlphaChannelBar::paintEvent(QPaintEvent *event)
barRect.setWidth(barRect.width() - triangleSize);
const int miniRectWidth = barRect.width() / 2;
const int miniRectWidth = qMax(2, barRect.width() / 2);
for (int y = barRect.topLeft().ry(); y < barRect.bottomRight().ry(); y++) {
for (int x = barRect.topLeft().rx(); x < barRect.bottomRight().rx(); x++) {
p.setPen(
+17 -25
View File
@@ -1,46 +1,38 @@
#include "ComboQuickFilterView.h"
#include "ui_ComboQuickFilterView.h"
#include <QHBoxLayout>
#include <QLabel>
ComboQuickFilterView::ComboQuickFilterView(QWidget *parent)
: QWidget(parent)
, ui(new Ui::ComboQuickFilterView)
: QuickFilterView(parent)
, label(new QLabel(this))
, combo(new QComboBox(this))
{
ui->setupUi(this);
connect(ui->lineEdit, &QLineEdit::textChanged, this, [this](const QString &text) {
emit filterTextChanged(text);
});
}
ComboQuickFilterView::~ComboQuickFilterView()
{
delete ui;
label->setObjectName(QStringLiteral("label"));
combo->setObjectName(QStringLiteral("comboBox"));
auto row = qobject_cast<QHBoxLayout *>(layout());
row->addWidget(label);
row->addWidget(combo);
}
void ComboQuickFilterView::setLabelText(const QString &text)
{
ui->label->setText(text);
label->setText(text);
}
QComboBox *ComboQuickFilterView::comboBox()
{
return ui->comboBox;
}
void ComboQuickFilterView::showFilter()
{
show();
ui->lineEdit->setFocus();
return combo;
}
void ComboQuickFilterView::clearFilter()
{
ui->lineEdit->setText("");
QuickFilterView::clearFilter();
combo->setCurrentIndex(0);
}
void ComboQuickFilterView::closeFilter()
{
ui->lineEdit->setText("");
hide();
emit filterClosed();
combo->setCurrentIndex(0);
QuickFilterView::closeFilter();
}
+6 -15
View File
@@ -1,37 +1,28 @@
#ifndef COMBOQUICKFILTERVIEW_H
#define COMBOQUICKFILTERVIEW_H
#include "core/IaitoCommon.h"
#include "QuickFilterView.h"
#include <QComboBox>
#include <QWidget>
class QLabel;
namespace Ui {
class ComboQuickFilterView;
}
class IAITO_EXPORT ComboQuickFilterView : public QWidget
class IAITO_EXPORT ComboQuickFilterView : public QuickFilterView
{
Q_OBJECT
public:
explicit ComboQuickFilterView(QWidget *parent = nullptr);
~ComboQuickFilterView();
void setLabelText(const QString &text);
QComboBox *comboBox();
public slots:
void showFilter();
void closeFilter();
void clearFilter();
signals:
void filterTextChanged(const QString &text);
void filterClosed();
void closeFilter();
private:
Ui::ComboQuickFilterView *ui;
QLabel *label;
QComboBox *combo;
};
#endif // COMBOQUICKFILTERVIEW_H
-50
View File
@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ComboQuickFilterView</class>
<widget class="QWidget" name="ComboQuickFilterView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>378</width>
<height>20</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="lineEdit">
<property name="placeholderText">
<string>Quick Filter</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+3 -1
View File
@@ -44,6 +44,7 @@ static const char *refreshAllCommandPrefixes[] = {
"C",
"aa",
"af",
"avg",
"w",
};
@@ -150,7 +151,8 @@ ConsoleWidget::ConsoleWidget(MainWindow *main)
historyDownShortcut->setContext(Qt::WidgetShortcut);
QShortcut *completionShortcut
= ShortcutMgr()->registerShortcut("console.complete", ui->r2InputLineEdit);
= ShortcutMgr()
->registerShortcut("console.complete", ui->r2InputLineEdit, Qt::WidgetShortcut);
connect(completionShortcut, &QShortcut::activated, this, &ConsoleWidget::triggerCompletion);
connect(ui->r2InputLineEdit, &QLineEdit::editingFinished, this, &ConsoleWidget::disableCompletion);
+679
View File
@@ -0,0 +1,679 @@
#include "DataAnalysisView.h"
#include <QApplication>
#include <QContextMenuEvent>
#include <QGestureEvent>
#include <QKeyEvent>
#include <QLinearGradient>
#include <QMouseEvent>
#include <QNativeGestureEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPinchGesture>
#include <QToolTip>
#include <QWheelEvent>
#include <algorithm>
#include <cmath>
using namespace DataAnalysis;
DataAnalysisView::DataAnalysisView(bool overview, QWidget *parent)
: QWidget(parent)
, overview(overview)
{
setMouseTracking(true);
setFocusPolicy(Qt::StrongFocus);
#ifndef Q_OS_MACOS
// On macOS, handle native trackpad events directly to retain their position.
grabGesture(Qt::PinchGesture);
#endif
setMinimumSize(220, overview ? 70 : 180);
setAccessibleName(overview ? tr("Data overview") : tr("Data analysis graph"));
setAccessibleDescription(
overview
? tr("Drag the window or use the wheel to pan; drag its edges to resize; pinch to zoom."
" Double-click to fit.")
: tr("Click to seek; drag to select bytes; wheel to pan; pinch to zoom."
" Right-click for comments, flags and range actions."));
tooltipTimer.setSingleShot(true);
tooltipTimer.setInterval(650);
connect(&tooltipTimer, &QTimer::timeout, this, &DataAnalysisView::showTooltip);
}
QString DataAnalysisView::metricName(int metric)
{
static const char *names[]
= {QT_TR_NOOP("Entropy"),
QT_TR_NOOP("Printable ASCII"),
QT_TR_NOOP("0x00 bytes"),
QT_TR_NOOP("0xFF bytes"),
QT_TR_NOOP("Unique bytes"),
QT_TR_NOOP("Mean byte"),
QT_TR_NOOP("ASCII letters"),
QT_TR_NOOP("ASCII digits"),
QT_TR_NOOP("ASCII whitespace"),
QT_TR_NOOP("ASCII control"),
QT_TR_NOOP("High bytes")};
return metric >= 0 && metric < Count ? tr(names[metric]) : QString();
}
QString DataAnalysisView::metricValue(int metric, double value)
{
if (metric == Entropy) {
return tr("%1 bits/byte").arg(value * 8, 0, 'f', 3);
}
if (metric == Unique) {
return tr("%1 / 256").arg(qRound(value * 256));
}
if (metric == Mean) {
return QString::number(value * 255, 'f', 2);
}
return tr("%1%").arg(value * 100, 0, 'f', 2);
}
QString DataAnalysisView::metricDescription(int metric)
{
static const char *descriptions[]
= {QT_TR_NOOP(
"Shannon entropy: 0–8 bits per byte. Higher values indicate a more varied byte "
"distribution. Overlaid values are divided by 8."),
QT_TR_NOOP("Percentage of printable ASCII bytes (0x20–0x7e)."),
QT_TR_NOOP("Percentage of zero bytes (0x00), often padding or uninitialized data."),
QT_TR_NOOP("Percentage of 0xff bytes, often padding or erased storage."),
QT_TR_NOOP("Number of distinct byte values observed, out of 256 possible values."),
QT_TR_NOOP("Average byte value (0–255). Overlaid values are divided by 255."),
QT_TR_NOOP("Percentage of ASCII letters (A–Z and a–z)."),
QT_TR_NOOP("Percentage of ASCII decimal digits (0–9)."),
QT_TR_NOOP(
"Percentage of ASCII whitespace (space, tab, newline, vertical tab, form feed and "
"carriage return)."),
QT_TR_NOOP("Percentage of ASCII control bytes (0x00–0x1f and 0x7f)."),
QT_TR_NOOP("Percentage of bytes with the high bit set (0x80–0xff).")};
return metric >= 0 && metric < Count ? tr(descriptions[metric]) : QString();
}
QString DataAnalysisView::markerName(MarkerType type)
{
static const char *names[]
= {QT_TR_NOOP("Section"),
QT_TR_NOOP("Segment"),
QT_TR_NOOP("Map"),
QT_TR_NOOP("Flag"),
QT_TR_NOOP("Comment")};
return tr(names[type]);
}
Range DataAnalysisView::axisRange() const
{
return overview ? domain : visibleRange;
}
QRectF DataAnalysisView::plotRect() const
{
return QRectF(
46, overview ? 5 : 30, qMax(1, width() - 60), qMax(1, height() - (overview ? 26 : 59)));
}
qreal DataAnalysisView::xForAddress(RVA address) const
{
const Range range = axisRange();
if (!range.valid()) {
return plotRect().left();
}
const long double distance = address >= range.start ? (long double) (address - range.start)
: -(long double) (range.start - address);
return plotRect().left() + distance / range.size() * plotRect().width();
}
RVA DataAnalysisView::addressAt(qreal x) const
{
const Range range = axisRange();
if (!range.valid()) {
return RVA_INVALID;
}
const long double fraction = (x - plotRect().left()) / plotRect().width();
if (fraction <= 0) {
return range.start;
}
if (fraction >= 1) {
return range.end - 1;
}
return range.start + qMin<RVA>(RVA(fraction * range.size()), range.size() - 1);
}
void DataAnalysisView::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.fillRect(rect(), palette().color(QPalette::Base));
const QRectF plot = plotRect();
const Range range = axisRange();
const QColor text = palette().color(QPalette::Text);
QColor grid = text;
grid.setAlpha(35);
p.setPen(grid);
p.drawRect(plot);
if (!range.valid()) {
p.setPen(text);
p.drawText(plot, Qt::AlignCenter, tr("Open a file to analyze its data"));
return;
}
const bool split = !overview && separateTracks;
const int tracks = split ? qMax(1, int(metrics.size())) : 1;
for (int track = 0; track < tracks; ++track) {
const qreal top = plot.top() + track * plot.height() / tracks;
const qreal bottom = plot.top() + (track + 1) * plot.height() / tracks;
const qreal graphTop = top + (split ? 19 : 0);
const qreal graphBottom = bottom - (split ? 5 : 0);
const qreal graphHeight = qMax(1.0, graphBottom - graphTop);
if (!overview) {
for (int tick = 0; tick <= 4; ++tick) {
const qreal y = graphBottom - graphHeight * tick / 4;
p.setPen(grid);
p.drawLine(QPointF(plot.left(), y), QPointF(plot.right(), y));
if (tracks == 1 || tick == 0 || tick == 4) {
int maximum = 100;
if ((separateTracks || metrics.size() == 1) && !metrics.isEmpty()) {
const int metric = metrics[track];
maximum = metric == Entropy ? 8
: metric == Mean ? 255
: metric == Unique ? 256
: 100;
}
p.setPen(text);
QString label = QString::number(maximum * tick / 4.0, 'g', 3);
if (maximum == 100 && tick == 4) {
label += '%';
}
p.drawText(
QRectF(0, y - 8, plot.left() - 7, 16),
Qt::AlignRight | Qt::AlignVCenter,
label);
}
}
if (split && !metrics.isEmpty()) {
p.setPen(colors[metrics[track]]);
p.drawText(
QRectF(plot.left() + 5, top, plot.width() - 10, 18),
Qt::AlignLeft | Qt::AlignVCenter,
metricName(metrics[track]));
}
}
p.save();
p.setClipRect(QRectF(plot.left(), graphTop, plot.width(), graphHeight));
const QVector<int> drawn = overview ? QVector<int>{Entropy}
: split && !metrics.isEmpty() ? QVector<int>{metrics[track]}
: metrics;
for (int series = 0; series < drawn.size(); ++series) {
const int metric = drawn[series];
const QColor color = colors[metric];
QPainterPath path;
qreal firstX = 0, lastX = 0;
auto flush = [&]() {
if (path.isEmpty()) {
return;
}
if (overview || style == Areas) {
QPainterPath area = path;
area.lineTo(lastX, graphBottom);
area.lineTo(firstX, graphBottom);
area.closeSubpath();
QLinearGradient gradient(0, graphTop, 0, graphBottom);
QColor fill = color;
fill.setAlpha(overview ? 110 : 85);
gradient.setColorAt(0, fill);
fill.setAlpha(12);
gradient.setColorAt(1, fill);
p.fillPath(area, gradient);
}
p.setPen(QPen(color, overview ? 1 : 1.5));
p.drawPath(path);
path = QPainterPath();
};
for (const auto &sample : samples) {
if (!sample.bytes) {
flush();
continue;
}
const qreal left = xForAddress(sample.range.start);
const qreal right = xForAddress(sample.range.end);
const qreal y = graphBottom - qBound(0.0, sample.values[metric], 1.0) * graphHeight;
if (!overview && style == Bars) {
const qreal barWidth = (right - left) / qMax(1, int(drawn.size()));
QColor fill = color;
fill.setAlpha(190);
p.fillRect(
QRectF(left + series * barWidth, y, qMax(0.6, barWidth), graphBottom - y),
fill);
} else {
if (path.isEmpty()) {
firstX = left;
path.moveTo(left, y);
}
path.lineTo((left + right) / 2, y);
lastX = right;
// Retain single-bucket signals and reach both ends of the range.
path.lineTo(right, y);
}
}
flush();
}
p.restore();
}
// Markers are coalesced by pixel and category to keep dense flag sets cheap to paint.
std::array<int, MarkerCount> lastPixel;
lastPixel.fill(-1000);
qreal labelEnd = plot.left();
for (const auto &marker : markers) {
if (!markerEnabled[marker.type]) {
continue;
}
const QColor color = colors[(marker.type + 4) % Count];
for (RVA address : {marker.range.start, marker.range.end}) {
if (address == marker.range.end && marker.type >= Flag) {
continue;
}
if (address < range.start || address > range.end) {
continue;
}
const qreal x = xForAddress(address);
const int pixel = qRound(x);
if (std::abs(pixel - lastPixel[marker.type]) < 2) {
continue;
}
lastPixel[marker.type] = pixel;
QColor line = color;
line.setAlpha(95);
p.setPen(QPen(line, 1, marker.type >= Flag ? Qt::DotLine : Qt::DashLine));
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
p.setPen(color);
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.top() + 5));
}
if (!overview && marker.type <= Map && intersect(marker.range, range).valid()) {
const qreal left = qMax(plot.left(), xForAddress(marker.range.start));
const qreal right = qMin(plot.right(), xForAddress(marker.range.end));
if (left >= labelEnd && right - left > 38) {
p.setPen(color);
const QString label
= fontMetrics().elidedText(marker.name, Qt::ElideRight, int(right - left - 6));
p.drawText(QRectF(left + 3, 4, right - left - 6, 20), Qt::AlignLeft, label);
labelEnd = left + fontMetrics().horizontalAdvance(label) + 10;
}
}
}
auto highlight = [&](Range selected, bool window) {
selected = intersect(selected, range);
if (!selected.valid()) {
return;
}
const qreal left = xForAddress(selected.start), right = xForAddress(selected.end);
QColor fill = palette().color(QPalette::Highlight);
fill.setAlpha(window ? 35 : 55);
p.fillRect(QRectF(left, plot.top(), qMax(1.0, right - left), plot.height()), fill);
p.setPen(QPen(palette().color(QPalette::Highlight), window ? 2 : 1));
p.drawRect(QRectF(left, plot.top(), qMax(1.0, right - left), plot.height()));
if (window) {
p.fillRect(QRectF(left - 2, plot.center().y() - 7, 4, 14), text);
p.fillRect(QRectF(right - 2, plot.center().y() - 7, 4, 14), text);
}
};
if (overview) {
const QColor shade(0, 0, 0, 65);
const qreal left = xForAddress(visibleRange.start), right = xForAddress(visibleRange.end);
p.fillRect(
QRectF(plot.left(), plot.top(), qMax(0.0, left - plot.left()), plot.height()), shade);
p.fillRect(QRectF(right, plot.top(), qMax(0.0, plot.right() - right), plot.height()), shade);
highlight(visibleRange, true);
}
highlight(drag == Select && dragSelection.valid() ? dragSelection : selection, false);
if (range.contains(seekAddress)) {
const qreal x = xForAddress(seekAddress);
p.setPen(QPen(palette().color(QPalette::Highlight), 2));
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
p.setBrush(palette().color(QPalette::Highlight));
p.drawPolygon(QPolygonF(
QVector<QPointF>{
QPointF(x - 4, plot.top()), QPointF(x + 4, plot.top()), QPointF(x, plot.top() + 6)}));
}
if (!overview && hoverX >= plot.left() && hoverX <= plot.right()) {
p.setPen(QPen(text, 1, Qt::DotLine));
p.drawLine(QPointF(hoverX, plot.top()), QPointF(hoverX, plot.bottom()));
}
p.setPen(text);
const int addressWidth = fontMetrics().horizontalAdvance(RAddressString(range.end - 1));
const int labels = qBound(1, int(plot.width() / (addressWidth + 24)), 4);
for (int i = 0; i <= labels; ++i) {
const qreal x = plot.left() + plot.width() * i / labels;
const QString label = fontMetrics().elidedText(
RAddressString(addressAt(x)), Qt::ElideMiddle, int(plot.width() / (labels + 1)));
const qreal width = fontMetrics().horizontalAdvance(label) + 2;
const qreal left
= qBound(plot.left(), x - width / 2, qMax(plot.left(), plot.right() - width));
p.drawText(QRectF(left, plot.bottom() + 4, width, 20), Qt::AlignLeft, label);
}
const bool hasData = std::any_of(samples.cbegin(), samples.cend(), [](const Sample &sample) {
return sample.bytes > 0;
});
if (!overview && !hasData) {
p.drawText(plot, Qt::AlignCenter, loading ? tr("Analyzing…") : tr("No readable data"));
}
}
bool DataAnalysisView::event(QEvent *event)
{
if (event->type() == QEvent::Gesture) {
auto *gestures = static_cast<QGestureEvent *>(event);
if (auto *pinch = static_cast<QPinchGesture *>(gestures->gesture(Qt::PinchGesture))) {
if (pinch->state() != Qt::GestureFinished && pinch->state() != Qt::GestureCanceled
&& (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged)) {
const QPoint position = mapFromGlobal(pinch->centerPoint().toPoint());
pinchZoom(pinch->scaleFactor(), position.x());
}
gestures->accept(pinch);
return true;
}
} else if (event->type() == QEvent::NativeGesture) {
auto *gesture = static_cast<QNativeGestureEvent *>(event);
if (gesture->gestureType() == Qt::ZoomNativeGesture) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const qreal x = gesture->position().x();
#else
const qreal x = gesture->localPos().x();
#endif
pinchZoom(1.0 + gesture->value(), x);
event->accept();
return true;
}
}
return QWidget::event(event);
}
void DataAnalysisView::pinchZoom(qreal scaleFactor, qreal x)
{
cancelTooltip();
drag = None;
dragSelection = {};
if (std::isfinite(scaleFactor) && scaleFactor > 0 && scaleFactor != 1) {
// Magnifying the graph narrows the address range around the gesture.
zoom(1.0 / scaleFactor, addressAt(x));
}
update();
}
void DataAnalysisView::zoom(double factor, RVA anchor)
{
if (!domain.valid() || !visibleRange.valid() || !std::isfinite(factor) || factor <= 0) {
return;
}
long double scaled = (long double) visibleRange.size() * factor;
if (factor > 1) {
// Small pinch steps must still let the user zoom out from a one-byte range.
scaled = std::ceil(scaled);
}
const RVA size = scaled >= domain.size() ? domain.size() : qMax<RVA>(1, RVA(scaled));
anchor = qBound(visibleRange.start, anchor, visibleRange.end - 1);
const long double ratio = (long double) (anchor - visibleRange.start) / visibleRange.size();
const RVA before = qMin<RVA>(size - 1, RVA(ratio * size));
const RVA start = anchor - qMin(anchor - domain.start, before);
const RVA clamped = qMin(start, domain.end - size);
emit rangeRequested(clamped, clamped + size);
}
void DataAnalysisView::pan(int direction)
{
if (!visibleRange.valid() || !domain.valid()) {
return;
}
const RVA step = qMax<RVA>(1, visibleRange.size() / 4);
const RVA start = direction < 0
? visibleRange.start - qMin(step, visibleRange.start - domain.start)
: visibleRange.start + qMin(step, domain.end - visibleRange.end);
emit rangeRequested(start, start + visibleRange.size());
}
void DataAnalysisView::mousePressEvent(QMouseEvent *event)
{
cancelTooltip();
if (event->button() != Qt::LeftButton || !axisRange().valid()
|| !plotRect().contains(event->pos())) {
return;
}
setFocus();
pressPosition = event->pos();
pressAddress = addressAt(event->pos().x());
pressRange = visibleRange;
dragSelection = {};
if (overview) {
if (std::abs(event->pos().x() - xForAddress(visibleRange.start)) < 7) {
drag = LeftEdge;
} else if (std::abs(event->pos().x() - xForAddress(visibleRange.end)) < 7) {
drag = RightEdge;
} else {
drag = Move;
if (!visibleRange.contains(pressAddress)) {
pressRange = centered(domain, pressAddress, visibleRange.size());
emit rangeRequested(pressRange.start, pressRange.end);
}
}
} else {
drag = Select;
}
event->accept();
}
void DataAnalysisView::updateDrag(QPoint position)
{
const RVA address = addressAt(position.x());
if (drag == Select) {
dragSelection = {qMin(address, pressAddress), qMax(address, pressAddress) + 1};
update();
} else if (drag == LeftEdge) {
emit rangeRequested(qMin(address, pressRange.end - 1), pressRange.end);
} else if (drag == RightEdge) {
emit rangeRequested(pressRange.start, qMax(address + 1, pressRange.start + 1));
} else if (drag == Move) {
const RVA start = address >= pressAddress
? pressRange.start
+ qMin(address - pressAddress, domain.end - pressRange.end)
: pressRange.start
- qMin(pressAddress - address, pressRange.start - domain.start);
emit rangeRequested(start, start + pressRange.size());
}
}
void DataAnalysisView::mouseMoveEvent(QMouseEvent *event)
{
const bool moved = hoverPosition != event->pos();
hoverPosition = event->pos();
hoverX = event->pos().x();
if (drag != None) {
updateDrag(event->pos());
} else if (plotRect().contains(event->pos())) {
if (moved) {
cancelTooltip();
tooltipTimer.start();
}
const bool edge = overview
&& (std::abs(hoverX - xForAddress(visibleRange.start)) < 7
|| std::abs(hoverX - xForAddress(visibleRange.end)) < 7);
setCursor(edge ? Qt::SizeHorCursor : overview ? Qt::OpenHandCursor : Qt::CrossCursor);
} else {
cancelTooltip();
}
update();
}
void DataAnalysisView::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton || drag == None) {
return;
}
updateDrag(event->pos());
if (drag == Select) {
if ((event->pos() - pressPosition).manhattanLength() < QApplication::startDragDistance()) {
emit selectionRequested(0, 0);
emit seekRequested(nearestMarker(event->pos().x()));
} else {
emit selectionRequested(dragSelection.start, dragSelection.end);
}
}
drag = None;
dragSelection = {};
update();
}
void DataAnalysisView::mouseDoubleClickEvent(QMouseEvent *event)
{
cancelTooltip();
if (event->button() == Qt::LeftButton && axisRange().valid()) {
drag = None;
if (overview) {
emit rangeRequested(domain.start, domain.end);
} else {
zoom(0.5, addressAt(event->pos().x()));
}
}
}
void DataAnalysisView::wheelEvent(QWheelEvent *event)
{
cancelTooltip();
const QPoint delta = event->angleDelta();
const int movement = delta.y() ? delta.y() : delta.x();
if (!movement) {
event->ignore();
return;
}
pan(movement > 0 ? -1 : 1);
event->accept();
}
void DataAnalysisView::keyPressEvent(QKeyEvent *event)
{
cancelTooltip();
const RVA center = visibleRange.start + visibleRange.size() / 2;
switch (event->key()) {
case Qt::Key_Plus:
case Qt::Key_Equal:
zoom(0.5, center);
break;
case Qt::Key_Minus:
zoom(2, center);
break;
case Qt::Key_Left:
pan(-1);
break;
case Qt::Key_Right:
pan(1);
break;
case Qt::Key_Home:
emit rangeRequested(domain.start, domain.end);
break;
case Qt::Key_Escape:
drag = None;
emit selectionRequested(0, 0);
update();
break;
default:
QWidget::keyPressEvent(event);
return;
}
event->accept();
}
void DataAnalysisView::leaveEvent(QEvent *)
{
cancelTooltip();
hoverX = -1;
unsetCursor();
QToolTip::hideText();
update();
}
void DataAnalysisView::cancelTooltip()
{
tooltipTimer.stop();
QToolTip::hideText();
}
void DataAnalysisView::hideEvent(QHideEvent *event)
{
cancelTooltip();
QWidget::hideEvent(event);
}
RVA DataAnalysisView::nearestMarker(qreal x) const
{
qreal distance = 5;
RVA result = addressAt(x);
for (const auto &marker : markers) {
if (markerEnabled[marker.type] && axisRange().contains(marker.range.start)) {
const qreal candidate = std::abs(x - xForAddress(marker.range.start));
if (candidate < distance) {
distance = candidate;
result = marker.range.start;
}
}
}
return result;
}
void DataAnalysisView::showTooltip()
{
if (!isVisible() || drag != None || !plotRect().contains(hoverPosition)) {
return;
}
const RVA address = addressAt(hoverPosition.x());
if (address == RVA_INVALID) {
return;
}
QString tip = RAddressString(address);
for (const auto &sample : samples) {
if (!sample.range.contains(address)) {
continue;
}
tip += tr("\nBucket: %1 – %2 (%3 bytes)\nRead %4 of %5 mapped bytes")
.arg(RAddressString(sample.range.start), RAddressString(sample.range.end - 1))
.arg(sample.range.size())
.arg(sample.bytes)
.arg(sample.covered);
if (sample.bytes) {
for (int metric : metrics) {
tip += QString("\n%1: %2")
.arg(metricName(metric), metricValue(metric, sample.values[metric]));
}
} else {
tip += tr("\nNo readable data");
}
break;
}
int found = 0;
for (const auto &marker : markers) {
if (markerEnabled[marker.type]
&& (marker.range.contains(address)
|| std::abs(xForAddress(marker.range.start) - hoverPosition.x()) < 5)) {
tip += QString("\n%1: %2 @ %3")
.arg(
markerName(marker.type),
marker.name.left(160),
RAddressString(marker.range.start));
if (++found == 12) {
break;
}
}
}
QToolTip::showText(mapToGlobal(hoverPosition), tip.toHtmlEscaped().replace('\n', "<br>"), this);
}
void DataAnalysisView::contextMenuEvent(QContextMenuEvent *event)
{
cancelTooltip();
if (axisRange().valid()) {
emit contextRequested(nearestMarker(event->pos().x()), event->globalPos());
event->accept();
}
}
+88
View File
@@ -0,0 +1,88 @@
#ifndef DATAANALYSISVIEW_H
#define DATAANALYSISVIEW_H
#include "common/DataAnalysis.h"
#include <QColor>
#include <QTimer>
#include <QWidget>
class DataAnalysisView : public QWidget
{
Q_OBJECT
public:
enum MarkerType { Section, Segment, Map, Flag, Comment, MarkerCount };
enum Style { Lines, Areas, Bars };
struct Marker
{
DataAnalysis::Range range;
QString name;
MarkerType type;
};
explicit DataAnalysisView(bool overview, QWidget *parent = nullptr);
static QString metricName(int metric);
static QString metricDescription(int metric);
static QString metricValue(int metric, double value);
static QString markerName(MarkerType type);
DataAnalysis::Range domain;
DataAnalysis::Range visibleRange;
DataAnalysis::Range selection;
QVector<DataAnalysis::Sample> samples;
QVector<Marker> markers;
QVector<int> metrics = {DataAnalysis::Entropy, DataAnalysis::Printable, DataAnalysis::Zero};
std::array<QColor, DataAnalysis::Count> colors;
std::array<bool, MarkerCount> markerEnabled = {true, false, false, true, true};
RVA seekAddress = RVA_INVALID;
Style style = Areas;
bool separateTracks = false;
bool loading = false;
QRectF plotRect() const;
RVA addressAt(qreal x) const;
qreal xForAddress(RVA address) const;
void zoom(double factor, RVA anchor);
void pan(int direction);
signals:
void seekRequested(RVA address);
void selectionRequested(RVA start, RVA end); // Half-open.
void rangeRequested(RVA start, RVA end); // Half-open.
void contextRequested(RVA address, QPoint globalPosition);
protected:
bool event(QEvent *) override;
void paintEvent(QPaintEvent *) override;
void mousePressEvent(QMouseEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
void mouseReleaseEvent(QMouseEvent *) override;
void mouseDoubleClickEvent(QMouseEvent *) override;
void wheelEvent(QWheelEvent *) override;
void keyPressEvent(QKeyEvent *) override;
void leaveEvent(QEvent *) override;
void hideEvent(QHideEvent *) override;
void contextMenuEvent(QContextMenuEvent *) override;
private:
bool overview;
enum Drag { None, Select, Move, LeftEdge, RightEdge };
Drag drag = None;
QPoint pressPosition;
RVA pressAddress = 0;
DataAnalysis::Range pressRange;
DataAnalysis::Range dragSelection;
int hoverX = -1;
QTimer tooltipTimer;
QPoint hoverPosition;
DataAnalysis::Range axisRange() const;
RVA nearestMarker(qreal x) const;
void pinchZoom(qreal scaleFactor, qreal x);
void updateDrag(QPoint position);
void showTooltip();
void cancelTooltip();
};
#endif
+966
View File
@@ -0,0 +1,966 @@
#include "DataAnalysisWidget.h"
#include "common/IaitoSeekable.h"
#include "core/MainWindow.h"
#include "dialogs/CommentsDialog.h"
#include "dialogs/FlagDialog.h"
#include "ui_DataAnalysisWidget.h"
#include <QActionGroup>
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QColorDialog>
#include <QDialogButtonBox>
#include <QElapsedTimer>
#include <QFileDialog>
#include <QFormLayout>
#include <QJsonArray>
#include <QJsonObject>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QSaveFile>
#include <QScrollArea>
#include <QSet>
#include <QTextStream>
#include <QToolButton>
#include <algorithm>
using namespace DataAnalysis;
namespace {
RVA number(const QJsonObject &object, const char *key)
{
return object[QLatin1String(key)].toVariant().toULongLong();
}
QByteArray readData(RVA address, int length)
{
auto core = Core()->core();
QByteArray bytes(length, '\0');
int count = 0;
while (count < length) {
// nread distinguishes short/failed reads from actual 0xff bytes. A read
// can end at a submap boundary even when the following map is readable.
const int n = r_io_nread_at(
core->io,
address + count,
reinterpret_cast<ut8 *>(bytes.data()) + count,
length - count);
if (n <= 0) {
break;
}
count += n;
}
bytes.resize(count);
return bytes;
}
} // namespace
DataAnalysisWidget::DataAnalysisWidget(MainWindow *main)
: AddressableDockWidget(main)
, ui(new Ui::DataAnalysisWidget)
{
setObjectName("DataAnalysisWidget");
updateWindowTitle();
auto *contents = new QWidget(this);
ui->setupUi(contents);
ui->legend->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
setWidget(contents);
plot = new DataAnalysisView(false, contents);
plot->setObjectName("analysisPlot");
overview = new DataAnalysisView(true, contents);
overview->setObjectName("analysisOverview");
overview->setFixedHeight(80);
auto *scroll = new QScrollArea(contents);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setWidget(plot);
ui->plotLayout->addWidget(scroll, 1);
ui->plotLayout->addWidget(overview);
ui->verticalLayout->setStretch(3, 1);
setupActions();
updateColors();
refreshDeferrer = createRefreshDeferrer([this]() { reloadTimer.start(); });
reloadTimer.setSingleShot(true);
reloadTimer.setInterval(100);
rangeTimer.setSingleShot(true);
rangeTimer.setInterval(80);
sampleTimer.setInterval(0);
connect(&reloadTimer, &QTimer::timeout, this, &DataAnalysisWidget::reload);
connect(&rangeTimer, &QTimer::timeout, this, [this]() {
if (!samplingOverview) {
beginSampling(false);
}
});
connect(&sampleTimer, &QTimer::timeout, this, &DataAnalysisWidget::sampleBatch);
connect(plot, &DataAnalysisView::seekRequested, seekable, &IaitoSeekable::seek);
connect(plot, &DataAnalysisView::selectionRequested, this, [this](RVA start, RVA end) {
selectRange({start, end}, true);
});
for (auto *view : {plot, overview}) {
connect(view, &DataAnalysisView::rangeRequested, this, [this](RVA start, RVA end) {
setRange({start, end});
});
connect(view, &DataAnalysisView::contextRequested, this, &DataAnalysisWidget::showContextMenu);
}
connect(seekable, &IaitoSeekable::seekableSeekChanged, this, &DataAnalysisWidget::onSeekChanged);
connect(Core(), &IaitoCore::addressRangeSelectionChanged, this, [this](RVA start, RVA end) {
if (!publishingSelection && seekable->isSynchronized()
&& Config()->getAddressRangeSelectionSyncEnabled()) {
selectRange(
start != RVA_INVALID && end != RVA_INVALID && end >= start ? Range{start, end + 1}
: Range{},
false);
}
});
connect(Core(), &IaitoCore::refreshAll, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::codeRebased, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::ioModeChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::ioCacheChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::flagsChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::commentsChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::refreshCodeViews, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::instructionChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::debugTaskStateChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Config(), &Configuration::colorsUpdated, this, &DataAnalysisWidget::updateColors);
connect(this, &QDockWidget::visibilityChanged, this, [this](bool visible) {
if (!visible) {
metadataDirty |= sampleTimer.isActive() || rangeTimer.isActive();
sampleTimer.stop();
rangeTimer.stop();
reloadTimer.stop();
} else if (metadataDirty) {
scheduleReload();
}
});
scheduleReload();
}
DataAnalysisWidget::~DataAnalysisWidget() = default;
QWidget *DataAnalysisWidget::widgetToFocusOnRaise()
{
return plot;
}
void DataAnalysisWidget::setupActions()
{
auto menuButton = [this](const QString &label, QMenu *menu) {
auto *button = new QToolButton(ui->toolbar);
button->setText(label);
button->setPopupMode(QToolButton::InstantPopup);
button->setMenu(menu);
ui->toolbar->addWidget(button);
};
auto *ranges = new QMenu(this);
ranges->addAction(tr("All data"), this, [this]() { setRange(plot->domain); });
ranges->addAction(tr("Current section"), this, [this]() {
fitMarker(DataAnalysisView::Section);
});
ranges->addAction(tr("Current segment"), this, [this]() {
fitMarker(DataAnalysisView::Segment);
});
ranges->addAction(tr("Current map"), this, [this]() { fitMarker(DataAnalysisView::Map); });
ranges->addAction(tr("Current block"), this, [this]() {
const int size = Core()->core()->blocksize;
setRange(sizedRange(seekable->getOffset(), qMax(1, size)));
});
selectionAction
= ranges->addAction(tr("Fit selection"), this, &DataAnalysisWidget::fitSelection);
selectionAction->setEnabled(false);
ranges->addSeparator();
ranges->addAction(tr("Edit range…"), this, &DataAnalysisWidget::editRange);
menuButton(tr("Range"), ranges);
auto *algorithms = new QMenu(this);
for (int i = 0; i < Count; ++i) {
auto *action = algorithms->addAction(DataAnalysisView::metricName(i));
action->setCheckable(true);
action->setChecked(plot->metrics.contains(i));
metricActions[i] = action;
action->setToolTip(DataAnalysisView::metricDescription(i));
auto *entry = ui->legend->addAction(DataAnalysisView::metricName(i));
entry->setToolTip(
DataAnalysisView::metricDescription(i) + tr("\nClick to configure this algorithm."));
legendActions[i] = entry;
connect(entry, &QAction::triggered, this, [this, i]() { configureMetric(i); });
connect(action, &QAction::toggled, this, &DataAnalysisWidget::updateMetrics);
}
menuButton(tr("Algorithms"), algorithms);
ui->toolbar
->addAction(
tr("+"),
this,
[this]() { plot->zoom(0.5, plot->visibleRange.start + plot->visibleRange.size() / 2); })
->setToolTip(tr("Zoom in (+)"));
ui->toolbar
->addAction(
tr("−"),
this,
[this]() { plot->zoom(2, plot->visibleRange.start + plot->visibleRange.size() / 2); })
->setToolTip(tr("Zoom out (-)"));
ui->toolbar->addAction(tr("Fit"), this, [this]() { setRange(plot->domain); })
->setToolTip(tr("Fit all data (Home)"));
followAction = ui->toolbar->addAction(tr("Follow"));
followAction->setCheckable(true);
followAction->setChecked(true);
followAction->setToolTip(tr("Pan to keep the current disassembly or hexdump address visible"));
connect(followAction, &QAction::toggled, this, [this](bool checked) {
if (checked) {
onSeekChanged(seekable->getOffset());
}
});
optionsMenu = new QMenu(this);
tracksAction = optionsMenu->addAction(tr("Separate algorithm tracks"));
tracksAction->setCheckable(true);
connect(tracksAction, &QAction::toggled, this, [this](bool checked) {
plot->separateTracks = checked;
plot->setMinimumHeight(checked ? qMax(180, int(plot->metrics.size()) * 90) : 180);
plot->update();
});
auto *styles = optionsMenu->addMenu(tr("Drawing style"));
auto *styleGroup = new QActionGroup(styles);
const QStringList styleNames = {tr("Lines"), tr("Filled lines"), tr("Vertical bars")};
for (int i = 0; i < styleNames.size(); ++i) {
auto *action = styles->addAction(styleNames[i]);
styleGroup->addAction(action);
action->setCheckable(true);
action->setData(i);
action->setChecked(i == plot->style);
connect(action, &QAction::triggered, this, [this, i]() {
plot->style = static_cast<DataAnalysisView::Style>(i);
plot->update();
});
}
styles->setObjectName("analysisStyles");
auto *colors = optionsMenu->addMenu(tr("Colors"));
auto *colorGroup = new QActionGroup(colors);
const QStringList modes
= {tr("Greyscale"), tr("Rainbow"), tr("Theme single color"), tr("Theme palette")};
for (int i = 0; i < modes.size(); ++i) {
auto *action = colors->addAction(modes[i]);
colorGroup->addAction(action);
action->setCheckable(true);
action->setData(i);
action->setChecked(i == colorMode);
connect(action, &QAction::triggered, this, [this, i]() {
colorMode = i;
updateColors();
});
}
colors->setObjectName("analysisColors");
colors->addSeparator();
auto *custom = colors->addMenu(tr("Customize algorithm colors"));
for (int i = 0; i < Count; ++i) {
custom->addAction(DataAnalysisView::metricName(i), this, [this, i]() {
const QColor color
= QColorDialog::getColor(plot->colors[i], this, DataAnalysisView::metricName(i));
if (color.isValid()) {
customColors[i] = color;
updateColors();
}
});
}
custom->addSeparator();
custom->addAction(tr("Reset custom colors"), this, [this]() {
customColors.fill(QColor());
updateColors();
});
auto *markers = optionsMenu->addMenu(tr("Markers"));
markers->setObjectName("analysisMarkers");
for (int i = 0; i < DataAnalysisView::MarkerCount; ++i) {
auto *action = markers->addAction(
DataAnalysisView::markerName(static_cast<DataAnalysisView::MarkerType>(i)));
action->setCheckable(true);
action->setData(i);
action->setChecked(plot->markerEnabled[i]);
connect(action, &QAction::toggled, this, [this, i](bool checked) {
plot->markerEnabled[i] = overview->markerEnabled[i] = checked;
plot->update();
overview->update();
});
}
auto *samples = optionsMenu->addMenu(tr("Resolution (buckets)"));
samples->setObjectName("analysisResolution");
auto *sampleGroup = new QActionGroup(samples);
for (int n : {32, 64, 128, 256, 512, 1024, 2048, 4096}) {
auto *action = samples->addAction(QString::number(n));
sampleGroup->addAction(action);
action->setCheckable(true);
action->setData(n);
action->setChecked(n == resolution);
connect(action, &QAction::triggered, this, [this, n]() {
resolution = n;
beginSampling(samplingOverview);
});
}
optionsMenu->addSeparator();
optionsMenu->addAction(&syncAction);
optionsMenu->addAction(tr("Find marker…"), this, &DataAnalysisWidget::findMarker);
optionsMenu->addAction(tr("Refresh"), this, &DataAnalysisWidget::scheduleReload);
exportAction = optionsMenu->addAction(
tr("Export visible samples as CSV…"), this, &DataAnalysisWidget::exportCsv);
exportAction->setEnabled(false);
menuButton(tr("Options"), optionsMenu);
ui->legend->setToolTip(
tr("Click a legend entry to configure its color and visibility."
" Overlaid values use a 0–100% scale; entropy is divided by 8."
" Separate tracks use native units."));
updateMetrics();
}
void DataAnalysisWidget::updateMetrics()
{
plot->metrics.clear();
for (int i = 0; i < Count; ++i) {
legendActions[i]->setVisible(metricActions[i]->isChecked());
if (metricActions[i]->isChecked()) {
plot->metrics.append(i);
}
}
overview->metrics = plot->metrics;
plot->setMinimumHeight(plot->separateTracks ? qMax(180, int(plot->metrics.size()) * 90) : 180);
plot->update();
}
void DataAnalysisWidget::updateColors()
{
const char *keys[]
= {"gui.navbar.code",
"gui.navbar.str",
"gui.navbar.data",
"num",
"flag",
"comment",
"flow",
"func_var",
"reg",
"invalid",
"other"};
QColor base = Config()->getColor("gui.navbar.code");
if (!base.isValid()) {
base = QColor(120, 150, 230);
}
QSet<QRgb> used;
for (int i = 0; i < Count; ++i) {
QColor color;
switch (colorMode) {
case 0:
color = QColor::fromHsv(0, 0, 85 + i * 16);
break;
case 1:
color = QColor::fromHsv((270 + i * 137) % 360, 165, 220);
break;
case 2:
color = QColor::fromHsv(qMax(0, base.hue()), 70 + i * 16, 235 - i * 9);
break;
default:
color = Config()->getColor(keys[i]);
if (!color.isValid() || used.contains(color.rgb())
|| color == palette().color(QPalette::Base)) {
color = QColor::fromHsv((270 + i * 137) % 360, 155, 210);
}
break;
}
if (customColors[i].isValid()) {
color = customColors[i];
}
used.insert(color.rgb());
plot->colors[i] = overview->colors[i] = color;
QPixmap swatch(12, 12);
swatch.fill(color);
metricActions[i]->setIcon(QIcon(swatch));
legendActions[i]->setIcon(QIcon(swatch));
}
plot->update();
overview->update();
}
void DataAnalysisWidget::configureMetric(int metric)
{
QDialog dialog(this);
dialog.setObjectName("analysisMetricSettings");
dialog.setWindowTitle(tr("%1 settings").arg(DataAnalysisView::metricName(metric)));
QFormLayout layout(&dialog);
QLabel description(DataAnalysisView::metricDescription(metric));
description.setWordWrap(true);
description.setMaximumWidth(420);
QCheckBox visible(tr("Show this algorithm"));
visible.setObjectName("metricVisible");
visible.setChecked(metricActions[metric]->isChecked());
QColor chosen = customColors[metric];
QPushButton colorButton(tr("Change color…"));
colorButton.setIcon(legendActions[metric]->icon());
QPushButton resetButton(tr("Use palette color"));
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout.addRow(&description);
layout.addRow(tr("Color:"), &colorButton);
layout.addRow(&resetButton);
layout.addRow(&visible);
layout.addRow(&buttons);
connect(&colorButton, &QPushButton::clicked, &dialog, [&]() {
const QColor color = QColorDialog::getColor(
chosen.isValid() ? chosen : plot->colors[metric],
&dialog,
DataAnalysisView::metricName(metric));
if (color.isValid()) {
chosen = color;
QPixmap swatch(12, 12);
swatch.fill(color);
colorButton.setIcon(QIcon(swatch));
colorButton.setText(color.name());
}
});
connect(&resetButton, &QPushButton::clicked, &dialog, [&]() {
chosen = QColor();
colorButton.setIcon(QIcon());
colorButton.setText(tr("Palette default"));
});
connect(&buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (dialog.exec() == QDialog::Accepted) {
customColors[metric] = chosen;
metricActions[metric]->setChecked(visible.isChecked());
updateColors();
}
}
void DataAnalysisWidget::scheduleReload()
{
metadataDirty = true;
if (refreshDeferrer->attemptRefresh(nullptr)) {
sampleTimer.stop();
reloadTimer.start();
}
}
void DataAnalysisWidget::loadMetadata()
{
using View = DataAnalysisView;
virtualAddresses = Core()->getConfigb("io.va");
readable.clear();
plot->markers.clear();
auto addMarker = [this](Range range, const QString &name, View::MarkerType type) {
if (range.valid()) {
plot->markers.append({range, name, type});
}
};
for (const auto &value : Core()->cmdj("omj").array()) {
const QJsonObject map = value.toObject();
const RVA start = number(map, "from"), last = number(map, "to");
const Range range = last >= start ? Range{start, last == RVA_MAX ? RVA_MAX : last + 1}
: Range{};
if (virtualAddresses) {
addMarker(range, map["name"].toString(), View::Map);
if (range.valid() && map["perm"].toString().contains('r')) {
readable.append(range);
}
}
}
if (!virtualAddresses) {
auto core = Core()->core();
const RVA size = r_io_size(core->io);
if (size != RVA_MAX && size) {
readable.append({0, size});
}
}
readable = mergeRanges(readable);
const Range domain = readable.isEmpty() ? Range{}
: Range{readable.first().start, readable.last().end};
const bool changed = domain != plot->domain;
plot->domain = overview->domain = domain;
if (changed || !plot->visibleRange.valid()) {
plot->visibleRange = overview->visibleRange = domain;
}
for (const auto &entry :
{qMakePair(QString("iSj"), View::Section), qMakePair(QString("iSSj"), View::Segment)}) {
const QJsonDocument document = Core()->cmdj(entry.first);
const QJsonArray objects
= document.isArray()
? document.array()
: document.object()[entry.second == View::Section ? "sections" : "segments"]
.toArray();
for (const auto &value : objects) {
const QJsonObject object = value.toObject();
const RVA start = number(object, virtualAddresses ? "vaddr" : "paddr");
const RVA size = number(object, virtualAddresses ? "vsize" : "size");
addMarker(sizedRange(start, size), object["name"].toString(), entry.second);
}
}
// @F temporarily selects all flag spaces without changing the user's space.
for (const auto &value : Core()->cmdj("fj @F:*").array()) {
const QJsonObject flag = value.toObject();
const RVA address = number(flag, flag.contains("offset") ? "offset" : "addr");
addMarker(
sizedRange(address, qMax<RVA>(1, number(flag, "size"))),
flag["name"].toString(),
View::Flag);
}
for (const auto &comment : Core()->getAllComments("CCu")) {
addMarker(sizedRange(comment.offset, 1), comment.name, View::Comment);
}
std::stable_sort(
plot->markers.begin(),
plot->markers.end(),
[](const View::Marker &a, const View::Marker &b) { return a.range.start < b.range.start; });
overview->markers = plot->markers;
metadataDirty = false;
}
void DataAnalysisWidget::reload()
{
if (!refreshDeferrer->attemptRefresh(nullptr) || Core()->isDebugTaskInProgress()) {
return;
}
sampleTimer.stop();
rangeTimer.stop();
loadMetadata();
plot->seekAddress = overview->seekAddress = seekable->getOffset();
if (requestedRange.valid()) {
const Range requested = intersect(requestedRange, plot->domain);
if (requested.valid()) {
plot->visibleRange = overview->visibleRange = requested;
}
requestedRange = {};
}
if (seekable->isSynchronized() && Config()->getAddressRangeSelectionSyncEnabled()
&& Core()->hasAddressRangeSelection()) {
selectRange(
{Core()->getAddressRangeSelectionStart(), Core()->getAddressRangeSelectionEnd() + 1},
false);
}
plot->samples.clear();
overview->samples.clear();
beginSampling(true);
}
void DataAnalysisWidget::beginSampling(bool mini)
{
rangeTimer.stop();
sampleTimer.stop();
if (!isVisibleToUser()) {
metadataDirty = true;
refreshDeferrer->attemptRefresh(nullptr);
return;
}
samplingOverview = mini;
pendingRange = mini ? plot->domain : plot->visibleRange;
pending.clear();
nextBucket = 0;
plot->loading = pendingRange.valid();
exportAction->setEnabled(false);
if (pendingRange.valid()) {
const int count = int(qMin<RVA>(mini ? 256 : resolution, pendingRange.size()));
pending.resize(count);
sampleTimer.start();
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::sampleBatch()
{
if (!isVisibleToUser() || Core()->isDebugTaskInProgress()) {
sampleTimer.stop();
metadataDirty = true;
refreshDeferrer->attemptRefresh(nullptr);
return;
}
QElapsedTimer timer;
timer.start();
// Yield to navigation and painting every few milliseconds. A range change
// cancels the old job; no stale worker result can overwrite the new range.
do {
pending[nextBucket]
= sample(bucket(pendingRange, nextBucket, pending.size()), readable, readData);
++nextBucket;
} while (nextBucket < pending.size() && timer.elapsed() < 6);
if (nextBucket < pending.size()) {
ui->statusLabel->setText(tr("Analyzing %1… %2%")
.arg(samplingOverview ? tr("overview") : tr("visible range"))
.arg(nextBucket * 100 / pending.size()));
return;
}
sampleTimer.stop();
if (samplingOverview) {
overview->samples = pending;
overview->update();
beginSampling(false);
} else {
plot->samples = pending;
plot->loading = false;
exportAction->setEnabled(!plot->samples.isEmpty());
updateLabels();
plot->update();
}
}
void DataAnalysisWidget::setRange(Range range)
{
range = intersect(range, plot->domain);
if (!range.valid() || range == plot->visibleRange) {
return;
}
plot->visibleRange = overview->visibleRange = range;
plot->samples.clear();
plot->loading = true;
exportAction->setEnabled(false);
if (!samplingOverview) {
sampleTimer.stop();
}
rangeTimer.start();
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::showRange(RVA start, RVA end)
{
if (start == RVA_INVALID || end == RVA_INVALID || end < start) {
return;
}
requestedRange = {start, end + 1};
raiseMemoryWidget();
seekable->seek(start);
if (metadataDirty) {
scheduleReload();
} else {
setRange(requestedRange);
requestedRange = {};
}
}
void DataAnalysisWidget::onSeekChanged(RVA address)
{
plot->seekAddress = overview->seekAddress = address;
if (followAction->isChecked() && plot->domain.contains(address)
&& !plot->visibleRange.contains(address)) {
setRange(centered(plot->domain, address, plot->visibleRange.size()));
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::selectRange(Range range, bool publish)
{
plot->selection = overview->selection = range;
selectionAction->setEnabled(range.valid());
if (publish && seekable->isSynchronized() && Config()->getAddressRangeSelectionSyncEnabled()) {
publishingSelection = true;
if (range.valid()) {
Core()->setAddressRangeSelection(range.start, range.end - 1);
} else {
Core()->clearAddressRangeSelection();
}
publishingSelection = false;
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::updateLabels()
{
const Range range = plot->visibleRange;
if (!range.valid()) {
ui->rangeLabel->setText(tr("No readable data in the current address space"));
ui->statusLabel->setText(tr("Open a file or refresh after mapping data."));
return;
}
QString text = tr("%1: %2 – %3 · %4 bytes · Cursor %5")
.arg(
virtualAddresses ? tr("Addresses") : tr("File offsets"),
RAddressString(range.start),
RAddressString(range.end - 1))
.arg(range.size())
.arg(RAddressString(seekable->getOffset()));
if (plot->selection.valid()) {
text += tr("\nSelection: %1 – %2 · %3 bytes")
.arg(
RAddressString(plot->selection.start),
RAddressString(plot->selection.end - 1))
.arg(plot->selection.size());
}
ui->rangeLabel->setText(text);
if (plot->loading) {
ui->statusLabel->setText(tr("Analyzing…"));
} else {
RVA bytes = 0, covered = 0;
for (const auto &sample : plot->samples) {
bytes += sample.bytes;
covered += sample.covered;
}
if (!bytes) {
ui->statusLabel->setText(tr("No readable bytes in the visible range."));
return;
}
ui->statusLabel->setText(
tr("%1 buckets · %2 / %3 mapped bytes read (%4) · Overview: entropy")
.arg(plot->samples.size())
.arg(bytes)
.arg(covered)
.arg(bytes == covered ? tr("complete") : tr("sampled or unreadable")));
ui->statusLabel->setToolTip(
tr("Each bucket reads up to 4096 bytes from centered windows in readable spans."
" Zoom in for complete coverage. Unmapped bytes are excluded."
" Shannon entropy: 0–8 bits/byte. Overlays use normalized percentages."));
}
}
void DataAnalysisWidget::fitSelection()
{
if (plot->selection.valid()) {
setRange(plot->selection);
}
}
void DataAnalysisWidget::fitMarker(DataAnalysisView::MarkerType type)
{
Range best;
for (const auto &marker : plot->markers) {
if (marker.type == type && marker.range.contains(seekable->getOffset())
&& (!best.valid() || marker.range.size() < best.size())) {
best = marker.range;
}
}
if (best.valid()) {
setRange(best);
} else {
ui->statusLabel->setText(
tr("No %1 contains the current address.").arg(DataAnalysisView::markerName(type)));
}
}
void DataAnalysisWidget::editRange()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Data analysis range"));
QFormLayout layout(&dialog);
QLineEdit start(RAddressString(plot->visibleRange.start));
QLineEdit end(RAddressString(plot->visibleRange.valid() ? plot->visibleRange.end - 1 : 0));
QLabel hint(tr("Enter addresses or radare2 expressions. Both endpoints are inclusive."));
hint.setWordWrap(true);
QLabel error;
error.setWordWrap(true);
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout.addRow(&hint);
layout.addRow(tr("Start:"), &start);
layout.addRow(tr("End (inclusive):"), &end);
layout.addRow(&error);
layout.addRow(&buttons);
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
connect(&buttons, &QDialogButtonBox::accepted, &dialog, [&]() {
RVA first, last;
if (!Core()->tryMath(start.text(), first) || !Core()->tryMath(end.text(), last)
|| first == RVA_INVALID || last == RVA_INVALID || first > last) {
error.setText(tr("Enter a valid start and an end greater than or equal to it."));
return;
}
const Range requested{first, last + 1};
if (intersect(requested, plot->domain) != requested) {
error.setText(
tr("The range must be within %1 – %2.")
.arg(RAddressString(plot->domain.start), RAddressString(plot->domain.end - 1)));
return;
}
setRange(requested);
dialog.accept();
});
dialog.exec();
}
void DataAnalysisWidget::findMarker()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Find section, map, flag or comment"));
dialog.resize(620, 400);
QVBoxLayout layout(&dialog);
QLineEdit filter;
filter.setPlaceholderText(tr("Filter by name, comment, type or hexadecimal address…"));
QListWidget list;
QLabel hint(
tr("Activate a result to seek. Section, segment and map results also fit the range."
" Showing up to 500 matches."));
hint.setWordWrap(true);
QDialogButtonBox buttons(QDialogButtonBox::Close);
layout.addWidget(&filter);
layout.addWidget(&list);
layout.addWidget(&hint);
layout.addWidget(&buttons);
// Keep result indices stable while the modal loop receives metadata updates.
const auto markers = plot->markers;
auto fill = [&]() {
list.clear();
for (int i = 0; i < markers.size(); ++i) {
const auto &marker = markers[i];
const QString label = QString("%1 · %2 · %3")
.arg(
RAddressString(marker.range.start),
DataAnalysisView::markerName(marker.type),
marker.name);
if (label.contains(filter.text(), Qt::CaseInsensitive)) {
auto *item = new QListWidgetItem(label, &list);
item->setData(Qt::UserRole, i);
if (list.count() >= 500) {
break;
}
}
}
};
connect(&filter, &QLineEdit::textChanged, &dialog, fill);
connect(&list, &QListWidget::itemActivated, &dialog, [&](QListWidgetItem *item) {
const int index = item->data(Qt::UserRole).toInt();
if (index < 0 || index >= markers.size()) {
return;
}
const auto marker = markers[index];
seekable->seek(marker.range.start);
if (marker.type <= DataAnalysisView::Map) {
setRange(marker.range);
} else if (!plot->visibleRange.contains(marker.range.start)) {
setRange(centered(plot->domain, marker.range.start, plot->visibleRange.size()));
}
dialog.accept();
});
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
fill();
dialog.exec();
}
void DataAnalysisWidget::showContextMenu(RVA address, QPoint position)
{
QMenu menu(this);
menu.addAction(RAddressString(address))->setEnabled(false);
menu.addAction(tr("Seek here"), this, [this, address]() { seekable->seek(address); });
if (mainWindow) {
auto *showIn = mainWindow->createShowInMenu(&menu, address);
showIn->setTitle(tr("Show in"));
menu.addMenu(showIn);
}
menu.addAction(tr("Copy address"), this, [address]() {
QApplication::clipboard()->setText(RAddressString(address));
});
menu.addAction(tr("Add or edit comment…"), this, [this, address]() {
CommentsDialog::addOrEditComment(address, this);
});
const Range selection = plot->selection;
const RVA flagSize = selection.contains(address) ? selection.end - address : 1;
menu.addAction(tr("Add or edit flag…"), this, [this, address, flagSize]() {
FlagDialog dialog(address, flagSize, this);
dialog.exec();
});
menu.addSeparator();
menu.addAction(selectionAction);
menu.addAction(tr("Clear selection"), this, [this]() { selectRange({}, true); });
menu.addAction(tr("Edit range…"), this, &DataAnalysisWidget::editRange);
menu.addAction(tr("Find marker…"), this, &DataAnalysisWidget::findMarker);
menu.addAction(tr("Fit all data"), this, [this]() { setRange(plot->domain); });
menu.addSeparator();
menu.addAction(&syncAction);
menu.exec(position);
}
void DataAnalysisWidget::exportCsv()
{
const auto samples = plot->samples;
const auto metrics = plot->metrics;
const QString path = QFileDialog::getSaveFileName(
this, tr("Export data analysis"), QString(), tr("CSV files (*.csv)"));
if (path.isEmpty()) {
return;
}
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, tr("Export failed"), file.errorString());
return;
}
QTextStream out(&file);
out << "start,end_exclusive,bytes_read,mapped_bytes";
for (int metric : metrics) {
QString name = DataAnalysisView::metricName(metric);
name.replace('"', "\"\"");
out << ",\"" << name << " (normalized 0-1)\"";
}
out << '\n';
for (const auto &sample : samples) {
out << RAddressString(sample.range.start) << ',' << RAddressString(sample.range.end) << ','
<< sample.bytes << ',' << sample.covered;
for (int metric : metrics) {
out << ',';
if (sample.bytes) {
out << QString::number(sample.values[metric], 'g', 12);
}
}
out << '\n';
}
out.flush();
if (!file.commit()) {
QMessageBox::warning(this, tr("Export failed"), file.errorString());
}
}
QVariantMap DataAnalysisWidget::serializeViewProprties()
{
auto properties = AddressableDockWidget::serializeViewProprties();
QVariantList metrics, colors, markers;
for (int i = 0; i < Count; ++i) {
if (metricActions[i]->isChecked()) {
metrics.append(i);
}
colors.append(customColors[i].isValid() ? customColors[i].name() : QString());
}
for (bool enabled : plot->markerEnabled) {
markers.append(enabled);
}
properties["metrics"] = metrics;
properties["colors"] = colors;
properties["markers"] = markers;
properties["style"] = int(plot->style);
properties["colorMode"] = colorMode;
properties["resolution"] = resolution;
properties["tracks"] = tracksAction->isChecked();
properties["follow"] = followAction->isChecked();
return properties;
}
void DataAnalysisWidget::deserializeViewProperties(const QVariantMap &properties)
{
AddressableDockWidget::deserializeViewProperties(properties);
const QVariantList metrics
= properties.value("metrics", QVariantList{Entropy, Printable, Zero}).toList();
const QVariantList colors = properties.value("colors").toList();
for (int i = 0; i < Count; ++i) {
metricActions[i]->setChecked(metrics.contains(i));
customColors[i] = i < colors.size() ? QColor(colors[i].toString()) : QColor();
}
auto restoreMenu = [this](const char *name, int selected) {
for (auto *action : optionsMenu->findChild<QMenu *>(name)->actions()) {
if (action->isCheckable() && action->data().toInt() == selected) {
action->trigger();
}
}
};
restoreMenu("analysisStyles", qBound(0, properties.value("style", 1).toInt(), 2));
restoreMenu("analysisColors", qBound(0, properties.value("colorMode", 3).toInt(), 3));
int count = properties.value("resolution", 1024).toInt();
if (!QList<int>{32, 64, 128, 256, 512, 1024, 2048, 4096}.contains(count)) {
count = 1024;
}
restoreMenu("analysisResolution", count);
const QVariantList markers
= properties.value("markers", QVariantList{true, false, false, true, true}).toList();
for (auto *action : optionsMenu->findChild<QMenu *>("analysisMarkers")->actions()) {
const int index = action->data().toInt();
action->setChecked(index < markers.size() && markers[index].toBool());
}
tracksAction->setChecked(properties.value("tracks", false).toBool());
followAction->setChecked(properties.value("follow", true).toBool());
updateColors();
}
+81
View File
@@ -0,0 +1,81 @@
#ifndef DATAANALYSISWIDGET_H
#define DATAANALYSISWIDGET_H
#include "AddressableDockWidget.h"
#include "DataAnalysisView.h"
#include <memory>
#include <QTimer>
namespace Ui {
class DataAnalysisWidget;
}
class QMenu;
class DataAnalysisWidget : public AddressableDockWidget
{
Q_OBJECT
public:
explicit DataAnalysisWidget(MainWindow *main);
~DataAnalysisWidget() override;
QVariantMap serializeViewProprties() override;
void deserializeViewProperties(const QVariantMap &properties) override;
// Reusable entry point for other widgets. Both endpoints are inclusive.
void showRange(RVA start, RVA end);
protected:
QString getWindowTitle() const override { return tr("Data Analysis"); }
QWidget *widgetToFocusOnRaise() override;
private:
std::unique_ptr<Ui::DataAnalysisWidget> ui;
DataAnalysisView *plot;
DataAnalysisView *overview;
RefreshDeferrer *refreshDeferrer;
QTimer reloadTimer;
QTimer sampleTimer;
QTimer rangeTimer;
QVector<DataAnalysis::Range> readable;
QVector<DataAnalysis::Sample> pending;
DataAnalysis::Range pendingRange;
DataAnalysis::Range requestedRange;
int nextBucket = 0;
int resolution = 1024;
int colorMode = 3;
bool samplingOverview = false;
bool virtualAddresses = true;
bool metadataDirty = true;
bool publishingSelection = false;
QAction *followAction;
QAction *tracksAction;
QAction *selectionAction;
QAction *exportAction;
QMenu *optionsMenu;
std::array<QAction *, DataAnalysis::Count> metricActions;
std::array<QAction *, DataAnalysis::Count> legendActions;
std::array<QColor, DataAnalysis::Count> customColors;
void setupActions();
void scheduleReload();
void reload();
void loadMetadata();
void beginSampling(bool mini);
void sampleBatch();
void setRange(DataAnalysis::Range range);
void updateLabels();
void updateColors();
void updateMetrics();
void configureMetric(int metric);
void onSeekChanged(RVA address);
void selectRange(DataAnalysis::Range range, bool publish);
void fitSelection();
void editRange();
void fitMarker(DataAnalysisView::MarkerType type);
void findMarker();
void showContextMenu(RVA address, QPoint position);
void exportCsv();
};
#endif
Loaded 100 of 193 files, more files were not shown because too many files have changed in this diff. Show more