Compare commits

...
Author SHA1 Message Date
Murphy 18d51daa6a Route r2frida commands and live memory through the native views.
The extra scripts, console, and hexdump tabs are removed so : commands and process bytes use the bottom console and the Hexdump widget.
2026-10-08 17:23:32 +02:00
Murphy f20ddbc357 Group r2frida views under one dock and sign the macOS plugin.
The session views are tabs on the r2frida panel, and the dylib is re-signed so macOS does not kill iaito when loading it.
2026-10-08 17:03:00 +02:00
Murphy c47985f1b0 WIP: Add an r2frida plugin that keeps the static session open.
Connect and attach through a second file descriptor so Frida does not replace the binary already under analysis.
2026-10-08 16:37:06 +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
106 changed files with 8601 additions and 532 deletions

No files matched your search

+4 -2
View File
@@ -1,8 +1,8 @@
name: iaito CI
env:
R2V: 6.2.2
R2GHIDRA_VERSION: 6.2.2
R2V: 6.2.4
R2GHIDRA_VERSION: 6.2.4
QT_MAC_INSTALL_VERSION: 6.11.*
QT_WIN_INSTALL_VERSION: 6.10.*
@@ -264,6 +264,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
+3
View File
@@ -3,6 +3,9 @@ AStyle*
./config.mk
config.mk
# Generated configuration header (qmake / Meson)
IaitoConfig.h
# OSX files
*.DS_Store
.AppleDouble
+24 -1
View File
@@ -48,6 +48,25 @@ plugin core_plugin: iaito
rm -f $(R2_USER_PLUGINS)/IaitoPlugin.$(R2_LIBEXT)
cp -f build/CorePlugin.$(R2_LIBEXT) $(R2_USER_PLUGINS)/IaitoPlugin.$(R2_LIBEXT)
# iaito plugin: r2frida as a second IO descriptor, not a native debug session.
ifeq ($(shell uname),Darwin)
IAITO_FRIDA_PLUGIN_DIR=$(HOME)/Library/Application Support/radareorg/iaito/plugins/native
else
IAITO_FRIDA_PLUGIN_DIR=$(HOME)/.local/share/radareorg/iaito/plugins/native
endif
frida-plugin:
mkdir -p build/frida-plugin
cd build/frida-plugin && $(QMAKE) ../../src/plugins/r2frida/IaitoFridaPlugin.pro $(QMAKE_FLAGS)
$(MAKE) -C build/frida-plugin
mkdir -p "$(IAITO_FRIDA_PLUGIN_DIR)"
cp -f build/plugins/native/libIaitoFridaPlugin.* "$(IAITO_FRIDA_PLUGIN_DIR)/"
ifeq ($(shell uname),Darwin)
# The linker’s ad-hoc signature is not valid for dlopen. macOS then
# SIGKILLs iaito with “Code Signature Invalid” while loading the plugin.
codesign --force --sign - "$(IAITO_FRIDA_PLUGIN_DIR)/libIaitoFridaPlugin.dylib"
endif
asan:
export CXXFLAGS=-fsanitize=address ; \
export CFLAGS=-fsanitize=address ; \
@@ -69,7 +88,7 @@ endif
mrproper: clean
git clean -xdf
.PHONY: install run user-install dist macos clean mrproper install-translations
.PHONY: install run user-install dist macos clean mrproper install-translations frida-plugin
.PHONY: dockindent dockindent-image indent indent-host
.PHONY: install-deeplink uninstall-deeplink
@@ -128,7 +147,11 @@ endif
user-uninstall:
$(MAKE) uninstall DESTDIR=/ PREFIX=${HOME}/.local MANDIR=${HOME}/.local/share/man
ifneq ($(shell uname),Darwin)
run: install-desktop-user
endif
run:
rarun2 libpath=$(shell r2 -H R2_LIBDIR) program=$(BIN)
install-desktop-user:
+3
View File
@@ -83,6 +83,9 @@ make
make run
```
On macOS, `make run` launches the app from `build/iaito.app` without installing it.
Registering the `iaito://` URI handler is optional; see [deep links](doc/deeplinks.md#macos).
To install the app in your home:
```sh
Vendored
+5 -5
View File
@@ -129,12 +129,12 @@ done
: ${INSTALL_PROGRAM_STRIP:=${INSTALL} -m 755 -s}
: ${INSTALL_MAN:=${INSTALL} -m 444}
: ${INSTALL_LIB:=${INSTALL} -m 755 -c}
PKGNAME='iaito' ; VERSION='6.2.2' ; VERSION_MAJOR=6; VERSION_MINOR=2; VERSION_PATCH=2; VERSION_NUMBER=60202; CONTACT_MAIL="[email protected]" ; CONTACT_NAME="pancake" ; CONTACT="pancake <[email protected]>" ;
PKGNAME='iaito' ; VERSION='6.2.4' ; VERSION_MAJOR=6; VERSION_MINOR=2; VERSION_PATCH=4; VERSION_NUMBER=60204; CONTACT_MAIL="[email protected]" ; CONTACT_NAME="pancake" ; CONTACT="pancake <[email protected]>" ;
}
show_usage() {
cat <<EOF2
'configure' configures iaito-6.2.2 to adapt to many kinds of systems.
'configure' configures iaito-6.2.4 to adapt to many kinds of systems.
Usage: ./configure [OPTION]... [VAR=VALUE]...
@@ -212,10 +212,10 @@ ocho() {
show_version() {
if [ "$QUIET" = 1 ]; then
echo "6.2.2"
echo "6.2.4"
exit 0
fi
echo "iaito-6.2.2 configuration script done with acr v2.2.8.
echo "iaito-6.2.4 configuration script done with acr v2.2.8.
The 'Free Software Foundation' message is only for autodetection.
Originally written by pancake <nopcode.org>."
exit 0
@@ -244,7 +244,7 @@ case $flag in
show_version ; ;;
-r|--r|--report)
echo "PKGNAME: iaito"
echo "VERSION: 6.2.2"
echo "VERSION: 6.2.4"
echo "AUTHOR: pancake"
echo "EMAIL: [email protected]"
echo "LANGS: c++"
+1 -1
View File
@@ -1,5 +1,5 @@
PKGNAME iaito
VERSION 6.2.2
VERSION 6.2.4
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
+22 -4
View File
@@ -4,7 +4,7 @@ TARGET = iaito
IAITO_VERSION_MAJOR = 6
IAITO_VERSION_MINOR = 2
IAITO_VERSION_PATCH = 2
IAITO_VERSION_PATCH = 4
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 \
@@ -314,6 +316,7 @@ SOURCES += \
common/HighDpiPixmap.cpp \
widgets/GraphGridLayout.cpp \
widgets/HexWidget.cpp \
widgets/HexPatWidget.cpp \
widgets/AddressScrollBar.cpp \
widgets/R2AIWidget.cpp \
widgets/R2McpWidget.cpp \
@@ -333,6 +336,8 @@ SOURCES += \
common/R2retdecDecompiler.cpp \
menus/AddressableItemContextMenu.cpp \
common/AddressableItemModel.cpp \
common/NameListModel.cpp \
widgets/NameListView.cpp \
widgets/ListDockWidget.cpp \
dialogs/MultitypeFileSaveDialog.cpp \
widgets/BoolToggleDelegate.cpp \
@@ -355,12 +360,17 @@ SOURCES += \
dialogs/AssemblerDialog.cpp \
dialogs/ScriptManagerDialog.cpp \
dialogs/ScriptManagerWidget.cpp \
widgets/ZoomWidget.cpp
widgets/ZoomWidget.cpp \
common/DataAnalysis.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 \
@@ -526,6 +536,8 @@ HEADERS += \
widgets/GraphLayout.h \
widgets/GraphGridLayout.h \
widgets/HexWidget.h \
widgets/HexPatWidget.h \
common/SparseHexLayout.h \
widgets/AddressScrollBar.h \
widgets/R2AIWidget.h \
widgets/R2McpWidget.h \
@@ -542,6 +554,8 @@ HEADERS += \
common/R2retdecDecompiler.h \
menus/AddressableItemContextMenu.h \
common/AddressableItemModel.h \
common/NameListModel.h \
widgets/NameListView.h \
widgets/ListDockWidget.h \
widgets/AddressableItemList.h \
dialogs/MultitypeFileSaveDialog.h \
@@ -567,11 +581,15 @@ HEADERS += \
dialogs/AssemblerDialog.h \
dialogs/ScriptManagerDialog.h \
dialogs/ScriptManagerWidget.h \
widgets/ZoomWidget.h
widgets/ZoomWidget.h \
common/DataAnalysis.h \
widgets/DataAnalysisView.h \
widgets/DataAnalysisWidget.h
GRAPHVIZ_HEADERS = widgets/GraphvizLayout.h
FORMS += \
widgets/DataAnalysisWidget.ui \
dialogs/AboutDialog.ui \
dialogs/EditStringDialog.ui \
dialogs/Base64EnDecodedWriteDialog.ui \
@@ -595,6 +613,7 @@ FORMS += \
widgets/FlagsWidget.ui \
widgets/StringsWidget.ui \
widgets/HexdumpWidget.ui \
widgets/HexPatWidget.ui \
dialogs/SaveProjectDialog.ui \
dialogs/settings/SettingsDialog.ui \
dialogs/settings/AppearanceOptionsWidget.ui \
@@ -627,7 +646,6 @@ FORMS += \
dialogs/SetToDataDialog.ui \
dialogs/EditVariablesDialog.ui \
widgets/IaitoTreeView.ui \
widgets/ComboQuickFilterView.ui \
dialogs/HexdumpRangeDialog.ui \
dialogs/WelcomeDialog.ui \
dialogs/EditMethodDialog.ui \
+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
+5 -4
View File
@@ -687,14 +687,15 @@ void Configuration::setVisualNavbarResolution(int divisor)
Configuration::MemoryScrollBarMode Configuration::getMemoryScrollBarMode() const
{
const int mode
= s.value("memoryScrollBarMode", static_cast<int>(MemoryScrollBarMode::Hidden)).toInt();
= s.value("memoryScrollBarMode", static_cast<int>(MemoryScrollBarMode::Bounded)).toInt();
switch (mode) {
case static_cast<int>(MemoryScrollBarMode::Spring):
return MemoryScrollBarMode::Spring;
case static_cast<int>(MemoryScrollBarMode::Bounded):
return MemoryScrollBarMode::Bounded;
default:
case static_cast<int>(MemoryScrollBarMode::Hidden):
return MemoryScrollBarMode::Hidden;
case static_cast<int>(MemoryScrollBarMode::Bounded):
default:
return MemoryScrollBarMode::Bounded;
}
}
+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
+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);
}
+12
View File
@@ -308,6 +308,12 @@ 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,
@@ -399,6 +405,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",
+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
+35 -2
View File
@@ -3307,6 +3307,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();
@@ -3442,13 +3462,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;
@@ -3818,6 +3847,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;
+3
View File
@@ -4,6 +4,7 @@
#define MONOTHREAD 1
#include "common/BasicInstructionHighlighter.h"
#include "common/ExceptionInfo.h"
#include "core/IaitoCommon.h"
#include "core/IaitoDescriptions.h"
@@ -464,6 +465,7 @@ public:
void suspendDebug();
private:
int exceptionJsonSupport = -1;
void finishStopDebug();
public:
@@ -580,6 +582,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();
+2
View File
@@ -98,6 +98,7 @@ struct SymbolDescription
QString bind;
QString type;
QString name;
QString demangledName;
};
struct CommentDescription
@@ -264,6 +265,7 @@ struct BinClassBaseClassDescription
struct BinClassMethodDescription
{
QString name;
QString mangledName;
RVA addr = RVA_INVALID;
st64 vtableOffset = -1;
};
+44 -2
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"
@@ -61,6 +62,7 @@
#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"
@@ -1151,6 +1153,14 @@ void MainWindow::initUI()
addDockWidget(Qt::DockWidgetArea::TopDockWidgetArea, r2aiDock);
m_dockManager->addPluginDock(r2aiDock);
}
{
auto *hexPatDock = new HexPatWidget(this);
addWidget(hexPatDock);
ui->menuTools->insertAction(ui->actionStart_Web_Server, hexPatDock->toggleViewAction());
addDockWidget(Qt::DockWidgetArea::TopDockWidgetArea, hexPatDock);
m_dockManager->addPluginDock(hexPatDock);
connect(ui->menuTools, &QMenu::aboutToShow, hexPatDock, &HexPatWidget::updateAvailability);
}
// Check if plugins are loaded and display tooltips accordingly
ui->menuWindows->setToolTipsVisible(true);
@@ -1208,6 +1218,7 @@ void MainWindow::initUI()
connectMenuStatusTips(ui->menuEdit);
connectMenuStatusTips(ui->menuCode);
connectMenuStatusTips(ui->menuAnalysis);
connectMenuStatusTips(ui->menuData);
connectMenuStatusTips(ui->menuTools);
connectMenuStatusTips(ui->menuPlugins);
@@ -1578,8 +1589,15 @@ void MainWindow::initDocks()
r2GraphDock = new R2GraphWidget(this),
callGraphDock = new CallGraphWidget(this, false),
globalCallGraphDock = new CallGraphWidget(this, true),
};
QList<IaitoDockWidget *> dataDocks = {
dataAnalysisDock = new DataAnalysisWidget(this),
zoomDock = new ZoomWidget(this),
};
dataAnalysisDock->toggleViewAction()->setText(tr("Analysis"));
connect(dataAnalysisDock, &QWidget::windowTitleChanged, this, [this]() {
dataAnalysisDock->toggleViewAction()->setText(tr("Analysis"));
});
auto makeActionList = [this](QList<IaitoDockWidget *> docks) {
QList<QAction *> result;
@@ -1599,6 +1617,7 @@ void MainWindow::initDocks()
ui->menuCode->addSeparator();
ui->menuCode->addActions(makeActionList(codeDocks));
ui->menuAnalysis->addActions(makeActionList(analysisDocks));
ui->menuData->addActions(makeActionList(dataDocks));
QList<IaitoDockWidget *> windowDocks2 = {
consoleDock,
};
@@ -1624,7 +1643,7 @@ void MainWindow::initDocks()
ui->menuAddDebugWidgets->addActions(makeActionList(debugDocks));
auto uniqueDocks = mainViewDocks + windowDocks2 + infoDocks + analysisDocks;
uniqueDocks += codeDocks + ioDocks + debugDocks;
uniqueDocks += codeDocks + ioDocks + debugDocks + dataDocks;
uniqueDocks.append(overviewDock);
for (auto dock : uniqueDocks) {
if (dock) { // ignore nullptr used as separators
@@ -1678,6 +1697,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);
@@ -2460,6 +2480,8 @@ void MainWindow::restoreDocks()
filesDock->hide();
binariesDock->hide();
tabifyDockWidget(dashboardDock, zoomDock);
tabifyDockWidget(dashboardDock, dataAnalysisDock);
dataAnalysisDock->hide();
for (const auto &it : m_dockManager->docks()) {
// Check whether or not current widgets is graph, hexdump or disasm
if (isExtraMemoryWidget(it)) {
@@ -2538,7 +2560,8 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type)
for (auto &dock : m_dockManager->docks()) {
if (auto memoryWidget = qobject_cast<MemoryDockWidget *>(dock)) {
if (memoryWidget->getType() == type && memoryWidget->getSeekable()->isSynchronized()) {
memoryWidget->tryRaiseMemoryWidget();
// Explicit view requests should also open an empty graph.
memoryWidget->raiseMemoryWidget();
return;
}
}
@@ -2547,6 +2570,11 @@ void MainWindow::showMemoryWidget(MemoryWidgetType type)
memoryDockWidget->raiseMemoryWidget();
}
void MainWindow::showDataAnalysis(RVA start, RVA end)
{
dataAnalysisDock->showRange(start, end);
}
void MainWindow::gotoOffset(const QString &offset)
{
ut64 address;
@@ -2590,6 +2618,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();
});
@@ -3201,6 +3239,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
+3
View File
@@ -66,6 +66,7 @@ class FilesWidget;
class FilesystemWidget;
class BinariesWidget;
class ZoomWidget;
class DataAnalysisWidget;
class XrefsWidget;
class RefsWidget;
class CodeInfoWidget;
@@ -151,6 +152,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(
@@ -366,6 +368,7 @@ private:
CallGraphWidget *callGraphDock = nullptr;
CallGraphWidget *globalCallGraphDock = nullptr;
ZoomWidget *zoomDock = nullptr;
DataAnalysisWidget *dataAnalysisDock = nullptr;
XrefsWidget *xrefsDock = nullptr;
RefsWidget *refsDock = nullptr;
CodeInfoWidget *codeInfoDock = nullptr;
+6
View File
@@ -130,9 +130,15 @@
<string>Analysis</string>
</property>
</widget>
<widget class="QMenu" name="menuData">
<property name="title">
<string>Data</string>
</property>
</widget>
<addaction name="actionHighlight"/>
<addaction name="separator"/>
<addaction name="menuAnalysis"/>
<addaction name="menuData"/>
<addaction name="menuAddInfoWidgets"/>
<addaction name="menuAddIoWidgets"/>
<addaction name="separator"/>
+45 -29
View File
@@ -48,15 +48,19 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
: QDialog(parent)
, m_process(new QProcess(this))
{
setWindowTitle(tr("Package Manager"));
setWindowTitle(tr("Radare2 Package Manager (r2pm)"));
resize(900, 600);
auto *layout = new QVBoxLayout(this);
auto *filterLayout = new QHBoxLayout();
m_filterLineEdit = new QLineEdit(this);
m_filterLineEdit->setPlaceholderText(tr("Filter packages..."));
m_filterLineEdit->setMinimumHeight(32);
m_binaryPackagesCheckBox = new QCheckBox(tr("Binary packages"), this);
m_binaryPackagesCheckBox->setToolTip(
tr("Install ready-to-use packages, including scripts, without building from source."));
m_showAllPlatformsCheckBox = new QCheckBox(tr("Show all platforms"), this);
filterLayout->addWidget(m_filterLineEdit);
filterLayout->addWidget(m_binaryPackagesCheckBox);
filterLayout->addWidget(m_showAllPlatformsCheckBox);
layout->addLayout(filterLayout);
@@ -77,11 +81,15 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
auto *buttonLayout = new QHBoxLayout();
m_refreshButton = new QPushButton(tr("Refresh"), this);
m_installButton = new QPushButton(tr("Install"), this);
m_uninstallButton = new QPushButton(tr("Uninstall"), this);
m_installButton = new QPushButton(tr("Install"), this);
for (auto *button : {m_refreshButton, m_uninstallButton, m_installButton}) {
button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
}
buttonLayout->addWidget(m_refreshButton);
buttonLayout->addWidget(m_installButton);
buttonLayout->addStretch();
buttonLayout->addWidget(m_uninstallButton);
buttonLayout->addWidget(m_installButton);
layout->addLayout(buttonLayout);
m_logTextEdit = new QTextEdit(this);
@@ -90,7 +98,12 @@ PackageManagerDialog::PackageManagerDialog(QWidget *parent)
layout->addWidget(m_logTextEdit);
connect(m_filterLineEdit, &QLineEdit::textChanged, this, &PackageManagerDialog::filterPackages);
connect(m_showAllPlatformsCheckBox, &QCheckBox::toggled, this, [this]() { refreshPackages(); });
connect(m_binaryPackagesCheckBox, &QCheckBox::toggled, this, [this]() {
filterPackages(m_filterLineEdit->text());
});
connect(m_showAllPlatformsCheckBox, &QCheckBox::toggled, this, [this]() {
filterPackages(m_filterLineEdit->text());
});
connect(m_refreshButton, &QPushButton::clicked, this, &PackageManagerDialog::refreshPackages);
connect(m_installButton, &QPushButton::clicked, this, &PackageManagerDialog::installPackage);
connect(m_uninstallButton, &QPushButton::clicked, this, &PackageManagerDialog::uninstallPackage);
@@ -128,21 +141,18 @@ void PackageManagerDialog::refreshPackages()
// Then list packages
QProcess listProc;
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
if (m_showAllPlatformsCheckBox->isChecked()) {
environment.insert("R2PM_PLATFORM", "any");
} else {
environment.remove("R2PM_PLATFORM");
}
environment.remove("R2PM_PLATFORM");
listProc.setProcessEnvironment(environment);
listProc.start("r2pm", QStringList() << "-sj");
listProc.start("r2pm", QStringList() << "-Asj");
if (!listProc.waitForFinished(30000)) {
QMessageBox::warning(this, tr("Error"), processError(listProc, tr("Failed to run r2pm -sj.")));
QMessageBox::warning(
this, tr("Error"), processError(listProc, tr("Failed to run r2pm -Asj.")));
return;
}
QByteArray out = listProc.readAllStandardOutput();
QJsonDocument doc = QJsonDocument::fromJson(out);
if (!doc.isArray()) {
QMessageBox::warning(this, tr("Error"), tr("Invalid JSON output from r2pm -sj"));
QMessageBox::warning(this, tr("Error"), tr("Invalid JSON output from r2pm -Asj"));
return;
}
QJsonArray array = doc.array();
@@ -158,19 +168,11 @@ void PackageManagerDialog::refreshPackages()
if (desc == "") {
desc = obj["desc"].toString();
}
QStringList platformTags;
QStringList platformNames;
QStringList platforms;
for (const QJsonValue &platformValue : obj["platforms"].toArray()) {
const QString platform = platformValue.toString();
if (platform == "windows") {
platformTags << "w";
platformNames << tr("Windows");
} else if (platform == "unix") {
platformTags << "u";
platformNames << tr("Unix");
} else if (platform == "qjs") {
platformTags << "j";
platformNames << tr("QuickJS");
if (!platform.isEmpty()) {
platforms << platform;
}
}
int row = m_tableWidget->rowCount();
@@ -180,11 +182,13 @@ void PackageManagerDialog::refreshPackages()
itemInstalled->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
m_tableWidget->setItem(row, 0, itemInstalled);
QTableWidgetItem *itemName = new QTableWidgetItem(name);
itemName->setData(Qt::UserRole, obj["binary"].toBool());
itemName->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
m_tableWidget->setItem(row, 1, itemName);
QTableWidgetItem *itemPlatforms = new QTableWidgetItem(platformTags.join(' '));
itemPlatforms->setToolTip(platformNames.join(", "));
itemPlatforms->setTextAlignment(Qt::AlignCenter);
QTableWidgetItem *itemPlatforms = new QTableWidgetItem(platforms.join(' '));
itemPlatforms->setData(Qt::UserRole, obj["supported"].toBool(true));
itemPlatforms->setToolTip(platforms.join(", "));
itemPlatforms->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
itemPlatforms->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
m_tableWidget->setItem(row, 2, itemPlatforms);
QTableWidgetItem *itemDesc = new QTableWidgetItem(desc);
@@ -216,29 +220,41 @@ void PackageManagerDialog::filterPackages(const QString &text)
|| m_tableWidget->item(row, 1)->text().contains(text, Qt::CaseInsensitive)
|| m_tableWidget->item(row, 2)->text().contains(text, Qt::CaseInsensitive)
|| m_tableWidget->item(row, 3)->text().contains(text, Qt::CaseInsensitive);
if (m_binaryPackagesCheckBox->isChecked()) {
visible = visible && m_tableWidget->item(row, 1)->data(Qt::UserRole).toBool();
}
if (!m_showAllPlatformsCheckBox->isChecked()) {
visible = visible && m_tableWidget->item(row, 2)->data(Qt::UserRole).toBool();
}
m_tableWidget->setRowHidden(row, !visible);
}
if (m_tableWidget->currentRow() >= 0
&& m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
m_tableWidget->setCurrentItem(nullptr);
m_tableWidget->clearSelection();
}
}
void PackageManagerDialog::installPackage()
{
if (m_process->state() != QProcess::NotRunning)
return;
if (m_tableWidget->currentRow() < 0) {
if (m_tableWidget->currentRow() < 0 || m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
QMessageBox::information(this, tr("Install"), tr("Please select a package to install."));
return;
}
QString pkg = m_tableWidget->item(m_tableWidget->currentRow(), 1)->text();
m_logTextEdit->clear();
m_logTextEdit->setVisible(true);
m_process->start("r2pm", QStringList() << "-ci" << pkg);
const QString flags = m_binaryPackagesCheckBox->isChecked() ? "-bi" : "-ci";
m_process->start("r2pm", QStringList() << flags << pkg);
}
void PackageManagerDialog::uninstallPackage()
{
if (m_process->state() != QProcess::NotRunning)
return;
if (m_tableWidget->currentRow() < 0) {
if (m_tableWidget->currentRow() < 0 || m_tableWidget->isRowHidden(m_tableWidget->currentRow())) {
QMessageBox::information(this, tr("Uninstall"), tr("Please select a package to uninstall."));
return;
}
+1
View File
@@ -43,6 +43,7 @@ private slots:
private:
QLineEdit *m_filterLineEdit;
QCheckBox *m_binaryPackagesCheckBox;
QCheckBox *m_showAllPlatformsCheckBox;
QTableWidget *m_tableWidget;
QPushButton *m_refreshButton;
+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);
}
}
}
+12
View File
@@ -42,6 +42,18 @@
<name>radare</name>
</developer>
<releases>
<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>
+273
View File
@@ -0,0 +1,273 @@
#include "FridaBackend.h"
#include "common/R2Task.h"
#include "core/Iaito.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
FridaBackend::FridaBackend(QObject *parent)
: QObject(parent)
{}
bool FridaBackend::pluginAvailable()
{
if (availabilityChecked_) {
return available_;
}
availabilityChecked_ = true;
const QString listed = Core()->cmd(QStringLiteral("oL"));
available_ = listed.contains(QStringLiteral("frida"), Qt::CaseInsensitive);
return available_;
}
int FridaBackend::currentFd() const
{
bool ok = false;
const int fd = Core()->cmd(QStringLiteral("oqq")).trimmed().toInt(&ok);
return ok ? fd : -1;
}
QSet<int> FridaBackend::openFds() const
{
QSet<int> fds;
const QJsonArray files = Core()->cmdj(QStringLiteral("oj")).array();
for (const QJsonValue value : files) {
fds.insert(value.toObject().value(QStringLiteral("fd")).toInt());
}
return fds;
}
int FridaBackend::findNewFridaFd(const QSet<int> &before) const
{
const QJsonArray files = Core()->cmdj(QStringLiteral("oj")).array();
for (const QJsonValue value : files) {
const QJsonObject file = value.toObject();
const int fd = file.value(QStringLiteral("fd")).toInt();
const QString uri = file.value(QStringLiteral("uri")).toString();
if (!before.contains(fd) && uri.startsWith(QStringLiteral("frida://"))) {
return fd;
}
}
return -1;
}
void FridaBackend::closeFd(int fd)
{
if (fd < 0) {
return;
}
Core()->cmd(QStringLiteral("o-") + QString::number(fd));
if (fridaFd_ == fd) {
fridaFd_ = -1;
}
}
int FridaBackend::restoreFd() const
{
const int current = currentFd();
if (current > 0 && current != fridaFd_) {
return current;
}
if (hostFd_ > 0 && hostFd_ != fridaFd_) {
return hostFd_;
}
return -1;
}
void FridaBackend::runBatch(const QString &batch, const std::function<void(QString)> &done)
{
if (busy_) {
if (done) {
done(QString());
}
return;
}
const QString savedColor = Core()->getConfig(QStringLiteral("scr.color"));
QString script = QStringLiteral("e scr.color=0\n");
script += batch;
if (!script.endsWith(QLatin1Char('\n'))) {
script += QLatin1Char('\n');
}
script += QStringLiteral("e scr.color=");
script += savedColor;
script += QLatin1Char('\n');
busy_ = true;
task_ = QSharedPointer<R2Task>(new R2Task(script));
connect(task_.data(), &R2Task::finished, this, [this, done, savedColor]() {
const QString output = task_ ? task_->getResult() : QString();
task_.clear();
busy_ = false;
Core()->setConfig(QStringLiteral("scr.color"), savedColor);
if (done) {
done(output);
}
});
task_->startTask();
}
QString FridaBackend::fridaCommand(const QString &command)
{
if (fridaFd_ < 0 || command.isEmpty()) {
return QString();
}
const int back = restoreFd();
QString batch = QStringLiteral("o=") + QString::number(fridaFd_) + QStringLiteral("\n:")
+ command + QLatin1Char('\n');
if (back > 0) {
batch += QStringLiteral("o=") + QString::number(back) + QLatin1Char('\n');
}
return Core()->cmd(batch);
}
static QString stripAnsi(QString text)
{
static const QRegularExpression ansi(QStringLiteral("\x1b\\[[0-9;]*m"));
text.replace(ansi, QString());
text.replace(QChar(0x2502), QLatin1Char('|'));
return text;
}
static bool isSeparatorLine(const QString &line)
{
if (line.isEmpty()) {
return true;
}
for (const QChar ch : line) {
if (ch.isSpace() || ch == QLatin1Char('-') || ch == QLatin1Char('|')
|| ch == QLatin1Char('+') || ch == QChar(0x2500) || ch == QChar(0x250c)
|| ch == QChar(0x2510)
|| ch == QChar(0x2514) || ch == QChar(0x2518) || ch == QChar(0x251c)
|| ch == QChar(0x2524) || ch == QChar(0x252c) || ch == QChar(0x2534)
|| ch == QChar(0x253c)) {
continue;
}
return false;
}
return true;
}
static QStringList cellsOf(const QString &line)
{
QString trimmed = line.trimmed();
if (trimmed.startsWith(QLatin1Char('|'))) {
QStringList cells;
const QStringList parts = trimmed.split(QLatin1Char('|'));
for (QString part : parts) {
part = part.trimmed();
if (!part.isEmpty()) {
cells.append(part);
}
}
return cells;
}
return trimmed.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
}
QList<FridaDeviceInfo> FridaBackend::parseDevices(const QString &output)
{
QList<FridaDeviceInfo> devices;
const QStringList lines = stripAnsi(output).split(QLatin1Char('\n'));
for (const QString &raw : lines) {
if (isSeparatorLine(raw.trimmed())) {
continue;
}
const QStringList cells = cellsOf(raw);
if (cells.size() < 3) {
continue;
}
const QString type = cells.at(1).toLower();
if (type != QStringLiteral("local") && type != QStringLiteral("usb")
&& type != QStringLiteral("remote")) {
continue;
}
FridaDeviceInfo device;
device.id = cells.at(0);
device.type = type;
device.name = cells.mid(2).join(QLatin1Char(' '));
devices.append(device);
}
return devices;
}
QList<FridaTargetInfo> FridaBackend::parseTargets(const QString &output, FridaListKind kind)
{
QList<FridaTargetInfo> targets;
const QStringList lines = stripAnsi(output).split(QLatin1Char('\n'));
for (const QString &raw : lines) {
if (isSeparatorLine(raw.trimmed())) {
continue;
}
const QStringList cells = cellsOf(raw);
if (cells.isEmpty()) {
continue;
}
const QString pid = cells.at(0);
const bool pidOk = pid == QStringLiteral("-")
|| QRegularExpression(QStringLiteral("^\\d+$")).match(pid).hasMatch();
if (!pidOk || pid.compare(QStringLiteral("PID"), Qt::CaseInsensitive) == 0) {
continue;
}
FridaTargetInfo target;
target.pid = pid;
if (kind == FridaListKind::Applications && cells.size() >= 3) {
target.identifier = cells.last();
target.name = cells.mid(1, cells.size() - 2).join(QLatin1Char(' '));
} else if (cells.size() >= 2) {
target.name = cells.mid(1).join(QLatin1Char(' '));
}
targets.append(target);
}
return targets;
}
QJsonDocument FridaBackend::parseJsonLoose(const QString &text)
{
const int arrayAt = text.indexOf(QLatin1Char('['));
const int objectAt = text.indexOf(QLatin1Char('{'));
int start = -1;
if (arrayAt >= 0 && (objectAt < 0 || arrayAt < objectAt)) {
start = arrayAt;
} else {
start = objectAt;
}
if (start < 0) {
return QJsonDocument();
}
const QChar open = text.at(start);
const QChar close = open == QLatin1Char('[') ? QLatin1Char(']') : QLatin1Char('}');
const int end = text.lastIndexOf(close);
if (end < start) {
return QJsonDocument();
}
return QJsonDocument::fromJson(text.mid(start, end - start + 1).toUtf8());
}
quint64 FridaBackend::parseAddress(const QJsonValue &value)
{
if (value.isString()) {
QString text = value.toString().trimmed();
bool ok = false;
int base = 10;
if (text.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) {
text = text.mid(2);
base = 16;
}
const quint64 parsed = text.toULongLong(&ok, base);
return ok ? parsed : 0;
}
if (value.isDouble()) {
return static_cast<quint64>(value.toDouble());
}
if (value.isObject()) {
const QJsonObject object = value.toObject();
for (const char *key : {"address", "value", "$handle", "base"}) {
if (object.contains(QLatin1String(key))) {
return parseAddress(object.value(QLatin1String(key)));
}
}
}
return 0;
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include "FridaTypes.h"
#include <QJsonDocument>
#include <QJsonValue>
#include <QObject>
#include <QSet>
#include <QSharedPointer>
#include <functional>
class R2Task;
// Talks to io_frida through the already-open radare2 core.
// The static file stays the current descriptor. Frida is opened with `of`
// (no maps) and selected only for the duration of a `:` command.
class FridaBackend : public QObject
{
Q_OBJECT
public:
explicit FridaBackend(QObject *parent = nullptr);
bool pluginAvailable();
bool busy() const { return busy_; }
int currentFd() const;
int hostFd() const { return hostFd_; }
int fridaFd() const { return fridaFd_; }
void setHostFd(int fd) { hostFd_ = fd; }
void setFridaFd(int fd) { fridaFd_ = fd; }
QSet<int> openFds() const;
int findNewFridaFd(const QSet<int> &before) const;
void closeFd(int fd);
void runBatch(const QString &batch, const std::function<void(QString)> &done);
QString fridaCommand(const QString &command);
static QList<FridaDeviceInfo> parseDevices(const QString &output);
static QList<FridaTargetInfo> parseTargets(const QString &output, FridaListKind kind);
static QJsonDocument parseJsonLoose(const QString &text);
static quint64 parseAddress(const QJsonValue &value);
private:
int restoreFd() const;
int hostFd_ = -1;
int fridaFd_ = -1;
bool busy_ = false;
bool availabilityChecked_ = false;
bool available_ = false;
QSharedPointer<R2Task> task_;
};
+344
View File
@@ -0,0 +1,344 @@
#include "FridaConnectDialog.h"
#include "FridaSession.h"
#include "FridaUri.h"
#include <QButtonGroup>
#include <QComboBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QStyle>
#include <QTreeWidget>
#include <QVBoxLayout>
namespace {
QPushButton *railButton(const QString &text, QStyle::StandardPixmap icon, QWidget *parent)
{
auto *button = new QPushButton(text, parent);
button->setCheckable(true);
button->setIcon(parent->style()->standardIcon(icon));
button->setStyleSheet(QStringLiteral(
"QPushButton { text-align: left; padding: 8px 14px; border: none; border-radius: 6px; }"
"QPushButton:checked { background-color: palette(highlight);"
" color: palette(highlighted-text); }"));
return button;
}
QPushButton *kindButton(const QString &text, QWidget *parent)
{
auto *button = new QPushButton(text, parent);
button->setCheckable(true);
button->setStyleSheet(QStringLiteral(
"QPushButton { padding: 6px 12px; border: 1px solid palette(mid); border-radius: 4px; }"
"QPushButton:checked { background-color: palette(highlight);"
" color: palette(highlighted-text); }"));
return button;
}
}
FridaConnectDialog::FridaConnectDialog(FridaSession *session, QWidget *parent)
: QDialog(parent)
, session(session)
{
setWindowTitle(tr("Connect to r2frida"));
setMinimumSize(640, 460);
auto *layout = new QHBoxLayout(this);
auto *rail = new QVBoxLayout();
transportGroup = new QButtonGroup(this);
localButton = railButton(tr("Local"), QStyle::SP_ComputerIcon, this);
usbButton = railButton(tr("USB"), QStyle::SP_DriveHDIcon, this);
remoteButton = railButton(tr("Remote"), QStyle::SP_DriveNetIcon, this);
localButton->setChecked(true);
transportGroup->addButton(localButton, int(FridaTransport::Local));
transportGroup->addButton(usbButton, int(FridaTransport::Usb));
transportGroup->addButton(remoteButton, int(FridaTransport::Remote));
rail->addWidget(localButton);
rail->addWidget(usbButton);
rail->addWidget(remoteButton);
rail->addStretch();
layout->addLayout(rail);
auto *mainColumn = new QVBoxLayout();
mainColumn->addWidget(new QLabel(tr("Select a device"), this));
auto *deviceRow = new QHBoxLayout();
devices = new QComboBox(this);
auto *refreshDevicesButton = new QPushButton(tr("Refresh"), this);
deviceRow->addWidget(devices, 1);
deviceRow->addWidget(refreshDevicesButton);
mainColumn->addLayout(deviceRow);
remoteHost = new QLineEdit(this);
remoteHost->setPlaceholderText(tr("10.0.0.3:9999"));
remoteHost->setVisible(false);
mainColumn->addWidget(remoteHost);
auto *kindRow = new QHBoxLayout();
kindGroup = new QButtonGroup(this);
appsButton = kindButton(tr("Applications"), this);
processButton = kindButton(tr("Processes"), this);
appsButton->setChecked(true);
kindGroup->addButton(appsButton, int(FridaListKind::Applications));
kindGroup->addButton(processButton, int(FridaListKind::Processes));
kindRow->addWidget(appsButton);
kindRow->addWidget(processButton);
kindRow->addStretch();
mainColumn->addLayout(kindRow);
search = new QLineEdit(this);
search->setPlaceholderText(tr("Search"));
mainColumn->addWidget(search);
targets = new QTreeWidget(this);
targets->setHeaderLabels({tr("Name"), tr("Identifier"), tr("PID")});
targets->setRootIsDecorated(false);
targets->setAlternatingRowColors(true);
targets->header()->setStretchLastSection(false);
targets->header()->setSectionResizeMode(0, QHeaderView::Stretch);
targets->header()->setSectionResizeMode(1, QHeaderView::Stretch);
mainColumn->addWidget(targets, 1);
status = new QLabel(this);
status->setWordWrap(true);
mainColumn->addWidget(status);
auto *buttons = new QHBoxLayout();
auto *attachButton = new QPushButton(tr("Attach"), this);
auto *spawnButton = new QPushButton(tr("Spawn"), this);
auto *launchButton = new QPushButton(tr("Launch"), this);
auto *closeButton = new QPushButton(tr("Cancel"), this);
attachButton->setDefault(true);
buttons->addWidget(attachButton);
buttons->addWidget(spawnButton);
buttons->addWidget(launchButton);
buttons->addStretch();
buttons->addWidget(closeButton);
mainColumn->addLayout(buttons);
layout->addLayout(mainColumn, 1);
const auto transportChanged = [this](bool checked) {
if (!checked) {
return;
}
remoteHost->setVisible(transport() == FridaTransport::Remote);
applyDevices(deviceCache);
refreshTargets();
};
connect(localButton, &QPushButton::toggled, this, transportChanged);
connect(usbButton, &QPushButton::toggled, this, transportChanged);
connect(remoteButton, &QPushButton::toggled, this, transportChanged);
connect(appsButton, &QPushButton::toggled, this, [this](bool checked) {
if (checked) {
refreshTargets();
}
});
connect(processButton, &QPushButton::toggled, this, [this](bool checked) {
if (checked) {
refreshTargets();
}
});
connect(
devices,
qOverload<int>(&QComboBox::currentIndexChanged),
this,
[this](int) { refreshTargets(); });
connect(refreshDevicesButton, &QPushButton::clicked, this, &FridaConnectDialog::refreshDevices);
connect(search, &QLineEdit::textChanged, this, &FridaConnectDialog::filterTargets);
connect(attachButton, &QPushButton::clicked, this, [this]() { choose(FridaAction::Attach); });
connect(spawnButton, &QPushButton::clicked, this, [this]() { choose(FridaAction::Spawn); });
connect(launchButton, &QPushButton::clicked, this, [this]() { choose(FridaAction::Launch); });
connect(closeButton, &QPushButton::clicked, this, &QDialog::reject);
connect(targets, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem *, int) {
const FridaTargetInfo target = selectedTarget();
const bool running = target.pid != QStringLiteral("-") && !target.pid.isEmpty();
choose(running ? FridaAction::Attach : FridaAction::Spawn);
});
if (!session->checkAvailable()) {
status->setText(
tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
attachButton->setEnabled(false);
spawnButton->setEnabled(false);
launchButton->setEnabled(false);
return;
}
refreshDevices();
}
FridaTransport FridaConnectDialog::transport() const
{
return static_cast<FridaTransport>(transportGroup->checkedId());
}
FridaListKind FridaConnectDialog::listKind() const
{
return static_cast<FridaListKind>(kindGroup->checkedId());
}
QString FridaConnectDialog::deviceId() const
{
if (transport() == FridaTransport::Remote) {
const QString host = remoteHost->text().trimmed();
if (!host.isEmpty()) {
return host;
}
}
return devices->currentData().toString();
}
QString FridaConnectDialog::deviceLabel() const
{
if (transport() == FridaTransport::Remote && !remoteHost->text().trimmed().isEmpty()) {
return remoteHost->text().trimmed();
}
const QString text = devices->currentText();
return text.isEmpty() ? deviceId() : text;
}
FridaTargetInfo FridaConnectDialog::selectedTarget() const
{
FridaTargetInfo target;
QTreeWidgetItem *item = targets->currentItem();
if (!item) {
return target;
}
target.name = item->text(0);
target.identifier = item->text(1);
target.pid = item->text(2);
return target;
}
void FridaConnectDialog::refreshDevices()
{
if (!session->checkAvailable()) {
return;
}
status->setText(tr("Listing devices..."));
const int generation = ++probeGeneration;
session->probe(FridaUri::listDevices(), [this, generation](const QString &output) {
if (generation != probeGeneration) {
return;
}
deviceCache = FridaBackend::parseDevices(output);
applyDevices(deviceCache);
if (deviceCache.isEmpty()) {
status->setText(
output.trimmed().isEmpty() ? tr("No devices found.") : output.trimmed());
}
refreshTargets();
});
}
void FridaConnectDialog::applyDevices(const QList<FridaDeviceInfo> &devices)
{
const QString previous = deviceId();
this->devices->blockSignals(true);
this->devices->clear();
const QString want = transport() == FridaTransport::Local
? QStringLiteral("local")
: (transport() == FridaTransport::Usb ? QStringLiteral("usb")
: QStringLiteral("remote"));
for (const FridaDeviceInfo &device : devices) {
if (device.type != want) {
continue;
}
const QString label
= device.name.isEmpty()
? device.id
: device.name + QStringLiteral(" (") + device.id + QLatin1Char(')');
this->devices->addItem(label, device.id);
}
const int index = this->devices->findData(previous);
if (index >= 0) {
this->devices->setCurrentIndex(index);
}
this->devices->blockSignals(false);
}
void FridaConnectDialog::refreshTargets()
{
if (!session->checkAvailable() || session->busy()) {
return;
}
status->setText(tr("Listing targets..."));
targets->clear();
const int generation = ++probeGeneration;
const QString uri = FridaUri::listTargets(transport(), listKind(), deviceId());
session->probe(uri, [this, generation](const QString &output) {
if (generation != probeGeneration) {
return;
}
const QList<FridaTargetInfo> parsed = FridaBackend::parseTargets(output, listKind());
applyTargets(parsed);
if (parsed.isEmpty()) {
status->setText(
output.trimmed().isEmpty() ? tr("No targets found.") : output.trimmed());
} else {
status->setText(tr("%1 targets").arg(parsed.size()));
}
filterTargets(search->text());
});
}
void FridaConnectDialog::applyTargets(const QList<FridaTargetInfo> &targets)
{
this->targets->clear();
for (const FridaTargetInfo &target : targets) {
auto *item = new QTreeWidgetItem(this->targets);
item->setText(0, target.name);
item->setText(1, target.identifier);
item->setText(2, target.pid);
}
}
void FridaConnectDialog::filterTargets(const QString &text)
{
const QString needle = text.trimmed();
for (int row = 0; row < targets->topLevelItemCount(); ++row) {
QTreeWidgetItem *item = targets->topLevelItem(row);
const bool match = needle.isEmpty() || item->text(0).contains(needle, Qt::CaseInsensitive)
|| item->text(1).contains(needle, Qt::CaseInsensitive)
|| item->text(2).contains(needle, Qt::CaseInsensitive);
item->setHidden(!match);
}
}
void FridaConnectDialog::choose(FridaAction action)
{
const FridaTargetInfo target = selectedTarget();
QString id;
if (action == FridaAction::Attach) {
bool pidOk = false;
const int pid = target.pid.toInt(&pidOk);
if (pidOk && pid > 0) {
id = target.pid;
}
}
if (id.isEmpty()) {
id = !target.identifier.isEmpty() ? target.identifier : target.name;
}
if (!FridaUri::targetIsSafe(id)) {
QMessageBox::warning(
this,
tr("Connect to target"),
tr("Select an application or process."));
return;
}
if (transport() == FridaTransport::Remote && deviceId().isEmpty()) {
QMessageBox::warning(this, tr("Connect to target"), tr("Enter a remote host:port."));
return;
}
const QString uri = FridaUri::target(transport(), action, deviceId(), id);
QString transportName = tr("local");
if (transport() == FridaTransport::Usb) {
transportName = tr("usb");
} else if (transport() == FridaTransport::Remote) {
transportName = tr("remote");
}
session->openTarget(uri, action == FridaAction::Spawn, deviceLabel(), transportName);
accept();
}
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include "FridaTypes.h"
#include <QDialog>
#include <QList>
class FridaSession;
class QButtonGroup;
class QComboBox;
class QLineEdit;
class QLabel;
class QTreeWidget;
class QPushButton;
class FridaConnectDialog : public QDialog
{
Q_OBJECT
public:
explicit FridaConnectDialog(FridaSession *session, QWidget *parent = nullptr);
private:
void refreshDevices();
void refreshTargets();
void applyDevices(const QList<FridaDeviceInfo> &devices);
void applyTargets(const QList<FridaTargetInfo> &targets);
void filterTargets(const QString &text);
void choose(FridaAction action);
FridaTransport transport() const;
FridaListKind listKind() const;
QString deviceId() const;
QString deviceLabel() const;
FridaTargetInfo selectedTarget() const;
FridaSession *session = nullptr;
QButtonGroup *transportGroup = nullptr;
QButtonGroup *kindGroup = nullptr;
QPushButton *localButton = nullptr;
QPushButton *usbButton = nullptr;
QPushButton *remoteButton = nullptr;
QPushButton *appsButton = nullptr;
QPushButton *processButton = nullptr;
QLineEdit *remoteHost = nullptr;
QComboBox *devices = nullptr;
QLineEdit *search = nullptr;
QTreeWidget *targets = nullptr;
QLabel *status = nullptr;
QList<FridaDeviceInfo> deviceCache;
int probeGeneration = 0;
};
+511
View File
@@ -0,0 +1,511 @@
#include "FridaPanels.h"
#include "FridaSession.h"
#include "FridaWidget.h"
#include "core/Iaito.h"
#include <QApplication>
#include <QClipboard>
#include <QComboBox>
#include <QFileInfo>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QJsonObject>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QRegularExpression>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QTreeView>
#include <QVBoxLayout>
namespace {
QString formatSize(quint64 size)
{
if (size >= (1024ull * 1024ull * 1024ull)) {
return QString::number(size / (1024.0 * 1024.0 * 1024.0), 'f', 1) + QStringLiteral(" GB");
}
if (size >= (1024ull * 1024ull)) {
return QString::number(size / (1024.0 * 1024.0), 'f', 1) + QStringLiteral(" MB");
}
if (size >= 1024ull) {
return QString::number(size / 1024.0, 'f', 1) + QStringLiteral(" KB");
}
return QString::number(size) + QStringLiteral(" B");
}
QStandardItem *textItem(const QString &text)
{
auto *item = new QStandardItem(text);
item->setEditable(false);
return item;
}
QStandardItem *addressItem(const QString &text, quint64 runtime)
{
auto *item = textItem(text);
item->setData(QVariant::fromValue(runtime), Qt::UserRole);
return item;
}
quint64 roleAddress(const QModelIndex &index)
{
return index.data(Qt::UserRole).toULongLong();
}
void host(QWidget *page, QWidget *child)
{
auto *layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(child);
}
void loadJava(FridaSession *session, QPlainTextEdit *view)
{
if (!session->isAttached()) {
view->setPlainText(QObject::tr("Not attached."));
return;
}
view->setPlainText(QObject::tr("Loading..."));
const QString source = QStringLiteral(
"if (!Java.available) { console.log('Java runtime is not available'); }\n"
"else Java.perform(function () {\n"
" var n = 0;\n"
" Java.enumerateLoadedClasses({\n"
" onMatch: function (name) { if (n++ < 2000) console.log(name); },\n"
" onComplete: function () { if (n >= 2000) console.log('... truncated'); }\n"
" });\n"
"});\n");
const QString path = session->writeScript(source);
if (path.isEmpty()) {
view->setPlainText(QObject::tr("Could not write the Java script."));
return;
}
session->command(QStringLiteral(". ") + path, [view](const QString &output) {
view->setPlainText(output);
});
}
void showCommand(FridaSession *session, const QString &command, QPlainTextEdit *view)
{
if (!session->isAttached()) {
view->setPlainText(QObject::tr("Not attached."));
return;
}
view->setPlainText(QObject::tr("Loading..."));
session->command(command, [view](const QString &output) {
QString text = output;
if (text.size() > 1000000) {
text.truncate(1000000);
text += QObject::tr("\n... truncated");
}
view->setPlainText(text);
});
}
class FridaOutputDock : public QWidget
{
public:
FridaOutputDock(
const QString &title,
const QString &objectName,
FridaSession *fridaSession,
const std::function<void(QPlainTextEdit *)> &load)
: QWidget(nullptr)
, session(fridaSession)
{
setObjectName(objectName);
setWindowTitle(title);
auto *root = new QWidget(this);
auto *layout = new QVBoxLayout(root);
auto *refresh = new QPushButton(tr("Refresh"), root);
view = new QPlainTextEdit(root);
view->setReadOnly(true);
layout->addWidget(refresh);
layout->addWidget(view);
host(this, root);
connect(refresh, &QPushButton::clicked, this, [this, load]() { load(view); });
connect(session, &FridaSession::changed, this, [this]() {
if (!session->isAttached()) {
view->clear();
}
});
}
private:
FridaSession *session = nullptr;
QPlainTextEdit *view = nullptr;
};
class FridaModulesDock : public QWidget
{
public:
FridaModulesDock(FridaSession *fridaSession)
: QWidget(nullptr)
, session(fridaSession)
{
setObjectName(QStringLiteral("FridaModulesDock"));
setWindowTitle(tr("Modules"));
model = new QStandardItemModel(this);
model->setHorizontalHeaderLabels({tr("Name"), tr("Base"), tr("Size"), tr("Path")});
proxy = new QSortFilterProxyModel(this);
proxy->setSourceModel(model);
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxy->setFilterKeyColumn(-1);
auto *root = new QWidget(this);
auto *layout = new QVBoxLayout(root);
auto *row = new QHBoxLayout();
auto *filter = new QLineEdit(root);
filter->setPlaceholderText(tr("Filter"));
auto *mode = new QComboBox(root);
mode->addItem(tr("Runtime"), int(FridaAddressMode::Runtime));
mode->addItem(tr("Static"), int(FridaAddressMode::Static));
mode->addItem(tr("Module + offset"), int(FridaAddressMode::ModuleOffset));
row->addWidget(filter, 1);
row->addWidget(mode);
view = new QTreeView(root);
view->setModel(proxy);
view->setRootIsDecorated(false);
view->setAlternatingRowColors(true);
view->setContextMenuPolicy(Qt::CustomContextMenu);
view->header()->setStretchLastSection(true);
view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
layout->addLayout(row);
layout->addWidget(view);
host(this, root);
connect(
filter, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterFixedString);
connect(mode, qOverload<int>(&QComboBox::currentIndexChanged), this, [this, mode](int index) {
session->setAddressMode(static_cast<FridaAddressMode>(mode->itemData(index).toInt()));
});
connect(session, &FridaSession::changed, this, [this]() { refill(); });
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
fridaSeekRuntime(session, roleAddress(index.sibling(index.row(), 0)));
});
connect(view, &QTreeView::customContextMenuRequested, this, [this](const QPoint &pos) {
const QModelIndex index = view->indexAt(pos);
if (!index.isValid()) {
return;
}
const QModelIndex nameIndex = index.sibling(index.row(), 0);
const quint64 runtime = roleAddress(nameIndex);
const QString name = nameIndex.data().toString();
QMenu menu(this);
menu.addAction(tr("Seek static address"), this, [this, runtime]() {
fridaSeekRuntime(session, runtime);
});
menu.addAction(tr("Copy runtime address"), this, [runtime]() {
QApplication::clipboard()->setText(RAddressString(runtime));
});
menu.addAction(tr("Live hexdump"), this, [this, runtime]() {
fridaShowLiveHex(this, session, runtime);
});
menu.addAction(tr("Show exports"), this, [this, name]() {
fridaShowExports(this, session, name);
});
menu.addAction(tr("Import symbols into analysis"), this, [this, name]() {
session->importExports(name);
});
menu.exec(view->viewport()->mapToGlobal(pos));
});
refill();
}
private:
void refill()
{
model->removeRows(0, model->rowCount());
for (const FridaModuleInfo &module : session->modules()) {
auto *name = textItem(module.name);
name->setData(QVariant::fromValue(module.base), Qt::UserRole);
name->setToolTip(session->describe(module.base));
model->appendRow(
{name,
addressItem(session->formatRuntime(module.base), module.base),
textItem(formatSize(module.size)),
textItem(module.path)});
}
}
FridaSession *session = nullptr;
QStandardItemModel *model = nullptr;
QSortFilterProxyModel *proxy = nullptr;
QTreeView *view = nullptr;
};
class FridaMapsDock : public QWidget
{
public:
FridaMapsDock(FridaSession *fridaSession)
: QWidget(nullptr)
, session(fridaSession)
{
setObjectName(QStringLiteral("FridaMemoryMapDock"));
setWindowTitle(tr("Memory Map"));
model = new QStandardItemModel(this);
model->setHorizontalHeaderLabels({tr("Start"), tr("End"), tr("Perm"), tr("Module")});
proxy = new QSortFilterProxyModel(this);
proxy->setSourceModel(model);
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxy->setFilterKeyColumn(-1);
auto *root = new QWidget(this);
auto *layout = new QVBoxLayout(root);
auto *filter = new QLineEdit(root);
filter->setPlaceholderText(tr("Filter"));
view = new QTreeView(root);
view->setModel(proxy);
view->setRootIsDecorated(false);
view->setAlternatingRowColors(true);
view->setContextMenuPolicy(Qt::CustomContextMenu);
view->header()->setStretchLastSection(true);
layout->addWidget(filter);
layout->addWidget(view);
host(this, root);
connect(
filter, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterFixedString);
connect(session, &FridaSession::changed, this, [this]() { refill(); });
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
fridaSeekRuntime(session, roleAddress(index.sibling(index.row(), 0)));
});
connect(view, &QTreeView::customContextMenuRequested, this, [this](const QPoint &pos) {
const QModelIndex index = view->indexAt(pos);
if (!index.isValid()) {
return;
}
const quint64 runtime = roleAddress(index.sibling(index.row(), 0));
QMenu menu(this);
menu.addAction(tr("Seek static address"), this, [this, runtime]() {
fridaSeekRuntime(session, runtime);
});
menu.addAction(tr("Live hexdump"), this, [this, runtime]() {
fridaShowLiveHex(this, session, runtime);
});
menu.addAction(tr("Copy address"), this, [runtime]() {
QApplication::clipboard()->setText(RAddressString(runtime));
});
menu.exec(view->viewport()->mapToGlobal(pos));
});
refill();
}
private:
void refill()
{
model->removeRows(0, model->rowCount());
for (const FridaMapInfo &map : session->maps()) {
const QString module = QFileInfo(map.path).fileName();
auto *start = addressItem(session->formatRuntime(map.start), map.start);
start->setToolTip(session->describe(map.start));
auto *name = textItem(module.isEmpty() ? map.path : module);
name->setToolTip(map.path);
model->appendRow(
{start,
addressItem(session->formatRuntime(map.end), map.end),
textItem(map.permission),
name});
}
}
FridaSession *session = nullptr;
QStandardItemModel *model = nullptr;
QSortFilterProxyModel *proxy = nullptr;
QTreeView *view = nullptr;
};
class FridaHooksDock : public QWidget
{
public:
FridaHooksDock(FridaSession *fridaSession)
: QWidget(nullptr)
, session(fridaSession)
{
setObjectName(QStringLiteral("FridaHooksDock"));
setWindowTitle(tr("Hooks"));
model = new QStandardItemModel(this);
model->setHorizontalHeaderLabels({tr("Address"), tr("Summary")});
view = new QTreeView(this);
view->setModel(model);
view->setRootIsDecorated(false);
view->setAlternatingRowColors(true);
view->header()->setStretchLastSection(true);
host(this, view);
connect(session, &FridaSession::changed, this, [this]() { refill(); });
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
fridaSeekRuntime(session, roleAddress(index.sibling(index.row(), 0)));
});
refill();
}
private:
void refill()
{
model->removeRows(0, model->rowCount());
for (const FridaHookInfo &hook : session->hooks()) {
model->appendRow(
{addressItem(session->formatRuntime(hook.address), hook.address),
textItem(hook.summary)});
}
}
FridaSession *session = nullptr;
QStandardItemModel *model = nullptr;
QTreeView *view = nullptr;
};
class FridaTracesDock : public QWidget
{
public:
FridaTracesDock(FridaSession *fridaSession)
: QWidget(nullptr)
, session(fridaSession)
{
setObjectName(QStringLiteral("FridaTracesDock"));
setWindowTitle(tr("Traces"));
model = new QStandardItemModel(this);
model->setHorizontalHeaderLabels({tr("Function"), tr("Hits")});
view = new QTreeView(this);
view->setModel(model);
view->setRootIsDecorated(false);
view->setAlternatingRowColors(true);
view->header()->setStretchLastSection(true);
view->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
auto *root = new QWidget(this);
auto *layout = new QVBoxLayout(root);
auto *refresh = new QPushButton(tr("Refresh"), root);
layout->addWidget(refresh);
layout->addWidget(view);
host(this, root);
connect(refresh, &QPushButton::clicked, this, [this]() { reload(); });
connect(session, &FridaSession::changed, this, [this]() {
if (!session->isAttached()) {
model->removeRows(0, model->rowCount());
}
});
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
const QString line = index.sibling(index.row(), 0).data().toString();
static const QRegularExpression addressRe(QStringLiteral("0x[0-9a-fA-F]+"));
const QRegularExpressionMatch match = addressRe.match(line);
if (!match.hasMatch()) {
return;
}
bool ok = false;
const quint64 addr = match.captured(0).toULongLong(&ok, 16);
if (ok) {
fridaSeekRuntime(session, addr);
}
});
}
private:
void reload()
{
if (!session->isAttached()) {
model->removeRows(0, model->rowCount());
return;
}
session->command(QStringLiteral("dt"), [this](const QString &output) {
model->removeRows(0, model->rowCount());
for (const QString &raw : output.split(QLatin1Char('\n'))) {
const QString line = raw.trimmed();
if (line.isEmpty()) {
continue;
}
QString function = line;
QString hits;
const int space = line.lastIndexOf(QLatin1Char(' '));
if (space > 0) {
bool ok = false;
line.mid(space + 1).toULongLong(&ok);
if (ok) {
function = line.left(space).trimmed();
hits = line.mid(space + 1);
}
}
model->appendRow({textItem(function), textItem(hits)});
}
});
}
FridaSession *session = nullptr;
QStandardItemModel *model = nullptr;
QTreeView *view = nullptr;
};
class FridaRuntimePage : public QWidget
{
public:
explicit FridaRuntimePage(FridaSession *fridaSession)
: QWidget(nullptr)
, session(fridaSession)
{
setWindowTitle(tr("ObjC/Java Runtime"));
auto *layout = new QVBoxLayout(this);
auto *row = new QHBoxLayout();
kind = new QComboBox(this);
kind->addItem(tr("Objective-C"));
kind->addItem(tr("Java"));
auto *refresh = new QPushButton(tr("Refresh"), this);
view = new QPlainTextEdit(this);
view->setReadOnly(true);
row->addWidget(kind);
row->addWidget(refresh);
row->addStretch();
layout->addLayout(row);
layout->addWidget(view);
connect(refresh, &QPushButton::clicked, this, [this]() { reload(); });
connect(session, &FridaSession::changed, this, [this]() {
if (!session->isAttached()) {
view->clear();
}
});
}
private:
void reload()
{
if (kind->currentIndex() == 0) {
showCommand(session, QStringLiteral("ic"), view);
} else {
loadJava(session, view);
}
}
FridaSession *session = nullptr;
QComboBox *kind = nullptr;
QPlainTextEdit *view = nullptr;
};
} // namespace
QList<QWidget *> createFridaPanels(FridaSession *session)
{
const auto command = [session](const QString &name) {
return [session, name](QPlainTextEdit *view) { showCommand(session, name, view); };
};
QList<QWidget *> panels;
panels << new FridaModulesDock(session);
panels << new FridaOutputDock(
QObject::tr("Threads"),
QStringLiteral("FridaThreadsPage"),
session,
command(QStringLiteral("dpt")));
panels << new FridaMapsDock(session);
panels << new FridaOutputDock(
QObject::tr("Breakpoints"),
QStringLiteral("FridaBreakpointsPage"),
session,
command(QStringLiteral("db")));
panels << new FridaRuntimePage(session);
panels << new FridaHooksDock(session);
panels << new FridaTracesDock(session);
return panels;
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <QList>
class FridaSession;
class QWidget;
QList<QWidget *> createFridaPanels(FridaSession *session);
+501
View File
@@ -0,0 +1,501 @@
#include "FridaSession.h"
#include "FridaUri.h"
#include "core/Iaito.h"
#include <QDir>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonObject>
#include <QRegularExpression>
#include <QTemporaryFile>
FridaSession::FridaSession(QObject *parent)
: QObject(parent)
{}
bool FridaSession::checkAvailable()
{
available_ = backend_.pluginAvailable();
return available_;
}
void FridaSession::setAddressMode(FridaAddressMode mode)
{
if (addressMode_ == mode) {
return;
}
addressMode_ = mode;
emit changed();
}
void FridaSession::setState(FridaSessionState state)
{
state_ = state;
emit changed();
}
void FridaSession::clearTarget()
{
suspended_ = false;
process_ = FridaProcessInfo();
modules_.clear();
maps_.clear();
hooks_.clear();
staticBase_ = 0;
runtimeBase_ = 0;
matchedSize_ = 0;
slide_ = 0;
uri_.clear();
deviceLabel_.clear();
transportLabel_.clear();
}
bool FridaSession::containsStatic(quint64 addr) const
{
return matchedSize_ > 0 && addr >= staticBase_ && addr - staticBase_ < matchedSize_;
}
bool FridaSession::containsRuntime(quint64 addr) const
{
return matchedSize_ > 0 && addr >= runtimeBase_ && addr - runtimeBase_ < matchedSize_;
}
quint64 FridaSession::toRuntime(quint64 staticVa) const
{
if (!containsStatic(staticVa)) {
return RVA_INVALID;
}
return runtimeBase_ + (staticVa - staticBase_);
}
quint64 FridaSession::toStatic(quint64 runtimeVa) const
{
if (!containsRuntime(runtimeVa)) {
return RVA_INVALID;
}
return staticBase_ + (runtimeVa - runtimeBase_);
}
QString FridaSession::formatRuntime(quint64 runtimeVa) const
{
if (addressMode_ == FridaAddressMode::ModuleOffset) {
for (const FridaModuleInfo &module : modules_) {
if (module.size > 0 && runtimeVa >= module.base
&& runtimeVa - module.base < module.size) {
return module.name + QStringLiteral(" + ")
+ RAddressString(runtimeVa - module.base);
}
}
}
if (addressMode_ == FridaAddressMode::Static) {
const quint64 staticVa = toStatic(runtimeVa);
if (staticVa != RVA_INVALID) {
return RAddressString(staticVa);
}
}
return RAddressString(runtimeVa);
}
QString FridaSession::describe(quint64 addr) const
{
quint64 staticVa = addr;
quint64 runtimeVa = addr;
if (containsStatic(addr)) {
runtimeVa = toRuntime(addr);
} else if (containsRuntime(addr)) {
staticVa = toStatic(addr);
} else {
return tr("No static mapping for %1").arg(RAddressString(addr));
}
QString moduleText = RAddressString(runtimeVa);
for (const FridaModuleInfo &module : modules_) {
if (module.size > 0 && runtimeVa >= module.base && runtimeVa - module.base < module.size) {
moduleText = module.name + QStringLiteral(" + ")
+ RAddressString(runtimeVa - module.base);
break;
}
}
return tr("static %1\nruntime %2\n%3\nslide %4")
.arg(
RAddressString(staticVa),
RAddressString(runtimeVa),
moduleText,
RAddressString(slide_));
}
void FridaSession::probe(const QString &uri, const std::function<void(QString)> &done)
{
if (!checkAvailable()) {
if (done) {
done(tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
}
return;
}
if (backend_.busy()) {
if (done) {
done(tr("r2frida is busy."));
}
return;
}
const QSet<int> before = backend_.openFds();
// r2frida prints the table with r_cons_gprintf, which follows the
// thread-local console. A background task drains a child console, so the
// table never comes back. Run here, on the console cmd() captures.
// `of` still performs the plugin open, and stays quiet when it returns
// no descriptor. `o` would log "Cannot open file" for that.
const QString savedColor = Core()->getConfig(QStringLiteral("scr.color"));
const QString batch = QStringLiteral("e scr.color=0\nof ") + FridaUri::quote(uri)
+ QStringLiteral("\ne scr.color=") + savedColor + QLatin1Char('\n');
RCons *previous = r_cons_singleton();
RCons *cons = r_core_get_cons(Core()->core_);
if (cons) {
r_cons_global(cons);
}
const QString output = Core()->cmd(batch);
if (previous && previous != cons) {
r_cons_global(previous);
}
const int leaked = backend_.findNewFridaFd(before);
backend_.closeFd(leaked);
QStringList kept;
for (const QString &line : output.split(QLatin1Char('\n'))) {
if (line.contains(QStringLiteral("Cannot open"))) {
continue;
}
kept.append(line);
}
if (done) {
done(kept.join(QLatin1Char('\n')));
}
}
void FridaSession::openTarget(
const QString &uri, bool suspended, const QString &deviceLabel, const QString &transportLabel)
{
if (!checkAvailable()) {
lastError_ = tr(
"r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito.");
setState(FridaSessionState::Error);
emit errorMessage(lastError_);
return;
}
if (backend_.busy()) {
emit errorMessage(tr("r2frida is busy."));
return;
}
if (backend_.fridaFd() >= 0) {
detach();
}
const int generation = ++generation_;
const QSet<int> before = backend_.openFds();
backend_.setHostFd(backend_.currentFd());
deviceLabel_ = deviceLabel;
transportLabel_ = transportLabel;
uri_ = uri;
suspended_ = suspended;
setState(FridaSessionState::Connecting);
emit consoleMessage(tr("opening %1").arg(uri));
const QString batch = QStringLiteral("of ") + FridaUri::quote(uri);
backend_.runBatch(batch, [this, before, generation, suspended](const QString &output) {
const int fd = backend_.findNewFridaFd(before);
if (generation != generation_) {
backend_.closeFd(fd);
return;
}
if (fd < 0) {
lastError_ = output.trimmed().isEmpty()
? tr("Could not open the r2frida target.")
: output.trimmed();
clearTarget();
setState(FridaSessionState::Error);
emit errorMessage(lastError_);
emit consoleMessage(lastError_);
return;
}
backend_.setFridaFd(fd);
const int host = backend_.hostFd();
if (host > 0 && backend_.currentFd() != host) {
Core()->cmd(QStringLiteral("o=") + QString::number(host));
}
suspended_ = suspended;
readSnapshot();
setState(FridaSessionState::Attached);
emit consoleMessage(
tr("attached pid %1 (%2)").arg(process_.pid).arg(process_.name));
});
}
void FridaSession::detach()
{
++generation_;
const int fd = backend_.fridaFd();
const int host = backend_.hostFd();
if (host > 0 && backend_.currentFd() == fd) {
Core()->cmd(QStringLiteral("o=") + QString::number(host));
}
backend_.closeFd(fd);
clearTarget();
lastError_.clear();
setState(FridaSessionState::Disconnected);
emit consoleMessage(tr("detached"));
}
void FridaSession::resume()
{
if (!isAttached()) {
return;
}
command(QStringLiteral("dc"), [this](const QString &output) {
suspended_ = false;
emit consoleMessage(output.trimmed().isEmpty() ? tr("resumed") : output.trimmed());
emit changed();
});
}
void FridaSession::command(const QString &fridaCmd, const std::function<void(QString)> &done)
{
if (!isAttached()) {
if (done) {
done(tr("Not attached."));
}
return;
}
if (backend_.busy()) {
if (done) {
done(tr("r2frida is busy."));
}
return;
}
const int generation = generation_;
const int fd = backend_.fridaFd();
const int back = backend_.currentFd() == fd ? backend_.hostFd() : backend_.currentFd();
QString batch = QStringLiteral("o=") + QString::number(fd) + QStringLiteral("\n:") + fridaCmd
+ QLatin1Char('\n');
if (back > 0 && back != fd) {
batch += QStringLiteral("o=") + QString::number(back) + QLatin1Char('\n');
}
backend_.runBatch(batch, [this, generation, done](const QString &output) {
if (generation != generation_) {
return;
}
if (done) {
done(output);
}
});
}
QByteArray FridaSession::readBytes(quint64 runtime, int length)
{
if (!isAttached() || length <= 0 || backend_.busy()) {
return {};
}
const int fd = backend_.fridaFd();
const int current = backend_.currentFd();
const int back = current == fd ? backend_.hostFd() : current;
const QString savedColor = Core()->getConfig(QStringLiteral("scr.color"));
QString batch = QStringLiteral("e scr.color=0\no=") + QString::number(fd);
batch += QStringLiteral("\np8 ") + QString::number(length);
batch += QStringLiteral(" @ ") + RAddressString(runtime) + QLatin1Char('\n');
if (back > 0 && back != fd) {
batch += QStringLiteral("o=") + QString::number(back) + QLatin1Char('\n');
}
batch += QStringLiteral("e scr.color=") + savedColor + QLatin1Char('\n');
RCons *previous = r_cons_singleton();
RCons *cons = r_core_get_cons(Core()->core_);
if (cons) {
r_cons_global(cons);
}
const QString output = Core()->cmd(batch);
if (previous && previous != cons) {
r_cons_global(previous);
}
if (output.contains(QStringLiteral("ERROR")) || output.contains(QStringLiteral("Cannot"))) {
return {};
}
QString hex;
hex.reserve(output.size());
for (const QChar ch : output) {
if (ch.isDigit() || (ch >= QLatin1Char('a') && ch <= QLatin1Char('f'))
|| (ch >= QLatin1Char('A') && ch <= QLatin1Char('F'))) {
hex.append(ch);
}
}
return QByteArray::fromHex(hex.toLatin1());
}
void FridaSession::addHook(quint64 address, const QString &summary)
{
FridaHookInfo hook;
hook.address = address;
hook.summary = summary;
hooks_.append(hook);
emit changed();
}
void FridaSession::note(const QString &text)
{
emit consoleMessage(text);
}
QString FridaSession::writeScript(const QString &source)
{
QString dir = QDir::tempPath();
if (dir.contains(QLatin1Char(' '))) {
dir = QStringLiteral("/tmp");
}
auto *file = new QTemporaryFile(dir + QStringLiteral("/iaito-frida-XXXXXX.js"), this);
file->setAutoRemove(true);
if (!file->open()) {
delete file;
return QString();
}
file->write(source.toUtf8());
file->flush();
return file->fileName();
}
void FridaSession::importExports(const QString &moduleName)
{
if (!isAttached() || moduleName.isEmpty()) {
return;
}
const QString commandText = QStringLiteral("iEj ") + moduleName;
command(commandText, [this, moduleName](const QString &output) {
const QJsonDocument doc = FridaBackend::parseJsonLoose(output);
const QJsonArray exports = doc.array();
QString script = QStringLiteral("fs frida\n");
int imported = 0;
const QString prefix = QStringLiteral("frida.") + moduleName + QLatin1Char('.');
for (const QJsonValue value : exports) {
const QJsonObject exp = value.toObject();
const quint64 runtime
= FridaBackend::parseAddress(exp.value(QStringLiteral("address")));
const quint64 staticVa = toStatic(runtime);
if (staticVa == RVA_INVALID) {
continue;
}
QString name = exp.value(QStringLiteral("name")).toString();
name.replace(QRegularExpression(QStringLiteral("[^A-Za-z0-9._]")), QStringLiteral("_"));
if (name.isEmpty()) {
continue;
}
script += QStringLiteral("f ");
script += prefix;
script += name;
script += QStringLiteral(" = ");
script += RAddressString(staticVa);
script += QLatin1Char('\n');
++imported;
if (imported >= 20000) {
break;
}
}
script += QStringLiteral("fs *\n");
if (imported == 0) {
emit errorMessage(tr("No exports from %1 map into the static file.").arg(moduleName));
return;
}
Core()->cmd(script);
Core()->triggerRefreshAll();
emit consoleMessage(tr("imported %1 symbols from %2").arg(imported).arg(moduleName));
});
}
void FridaSession::readSnapshot()
{
const QString blob = backend_.fridaCommand(
QStringLiteral("ij\n?e ---IAITO-FRIDA---\n:ilj\n?e ---IAITO-FRIDA---\n:dmj"));
QStringList parts = blob.split(QStringLiteral("---IAITO-FRIDA---"));
if (parts.size() < 3) {
parts = QStringList{blob, QString(), QString()};
}
applyInfo(FridaBackend::parseJsonLoose(parts.value(0)).object());
applyModules(FridaBackend::parseJsonLoose(parts.value(1)).array());
applyMaps(FridaBackend::parseJsonLoose(parts.value(2)).array());
}
void FridaSession::applyInfo(const QJsonObject &info)
{
process_ = FridaProcessInfo();
process_.pid = info.value(QStringLiteral("pid")).toInt(-1);
process_.arch = info.value(QStringLiteral("arch")).toString();
process_.bits = info.value(QStringLiteral("bits")).toInt();
process_.os = info.value(QStringLiteral("os")).toString();
process_.moduleName = info.value(QStringLiteral("modulename")).toString();
process_.moduleBase = FridaBackend::parseAddress(info.value(QStringLiteral("modulebase")));
const QString app = info.value(QStringLiteral("appname")).toString();
const QString packageName = info.value(QStringLiteral("packageName")).toString();
const QString bundle = info.value(QStringLiteral("bundle")).toString();
process_.identifier = !bundle.isEmpty() ? bundle : packageName;
if (!app.isEmpty()) {
process_.name = app;
} else if (!packageName.isEmpty()) {
process_.name = packageName;
} else if (!bundle.isEmpty()) {
process_.name = bundle;
} else {
process_.name = process_.moduleName;
}
}
void FridaSession::applyModules(const QJsonArray &modules)
{
modules_.clear();
const QString opened = QFileInfo(Core()->getFilePath()).fileName();
int match = -1;
for (const QJsonValue value : modules) {
const QJsonObject object = value.toObject();
FridaModuleInfo module;
module.name = object.value(QStringLiteral("name")).toString();
module.path = object.value(QStringLiteral("path")).toString();
module.base = FridaBackend::parseAddress(object.value(QStringLiteral("base")));
module.size = static_cast<quint64>(object.value(QStringLiteral("size")).toDouble());
const QString fileName = QFileInfo(module.path).fileName();
const bool nameMatch = !opened.isEmpty() && module.name == opened;
const bool pathMatch = !opened.isEmpty() && fileName == opened;
modules_.append(module);
if (nameMatch || pathMatch) {
if (match < 0 || nameMatch) {
match = modules_.size() - 1;
}
}
}
staticBase_ = 0;
runtimeBase_ = 0;
matchedSize_ = 0;
slide_ = 0;
if (match < 0) {
return;
}
const QJsonObject bin
= Core()->cmdj(QStringLiteral("ij")).object().value(QStringLiteral("bin")).toObject();
staticBase_ = bin.value(QStringLiteral("baddr")).toVariant().toULongLong();
FridaModuleInfo &module = modules_[match];
module.matched = true;
runtimeBase_ = module.base;
matchedSize_ = module.size;
slide_ = runtimeBase_ - staticBase_;
}
void FridaSession::applyMaps(const QJsonArray &maps)
{
maps_.clear();
for (const QJsonValue value : maps) {
const QJsonObject object = value.toObject();
FridaMapInfo map;
map.start = FridaBackend::parseAddress(object.value(QStringLiteral("base")));
map.end = map.start + static_cast<quint64>(object.value(QStringLiteral("size")).toDouble());
map.permission = object.value(QStringLiteral("protection")).toString();
const QJsonValue file = object.value(QStringLiteral("file"));
if (file.isObject()) {
map.path = file.toObject().value(QStringLiteral("path")).toString();
} else if (file.isString()) {
map.path = file.toString();
}
maps_.append(map);
}
}
+95
View File
@@ -0,0 +1,95 @@
#pragma once
#include "FridaBackend.h"
#include "FridaTypes.h"
#include <QByteArray>
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <functional>
class FridaSession : public QObject
{
Q_OBJECT
public:
explicit FridaSession(QObject *parent = nullptr);
bool checkAvailable();
bool available() const { return available_; }
FridaSessionState state() const { return state_; }
bool isAttached() const { return state_ == FridaSessionState::Attached && fridaFd() >= 0; }
bool isSuspended() const { return suspended_; }
bool busy() const { return backend_.busy(); }
QString lastError() const { return lastError_; }
QString deviceLabel() const { return deviceLabel_; }
QString transportLabel() const { return transportLabel_; }
QString uri() const { return uri_; }
const FridaProcessInfo &process() const { return process_; }
const QList<FridaModuleInfo> &modules() const { return modules_; }
const QList<FridaMapInfo> &maps() const { return maps_; }
const QList<FridaHookInfo> &hooks() const { return hooks_; }
quint64 staticBase() const { return staticBase_; }
quint64 slide() const { return slide_; }
bool hasStaticMapping() const { return matchedSize_ > 0; }
FridaAddressMode addressMode() const { return addressMode_; }
void setAddressMode(FridaAddressMode mode);
quint64 toRuntime(quint64 staticVa) const;
quint64 toStatic(quint64 runtimeVa) const;
bool containsStatic(quint64 addr) const;
bool containsRuntime(quint64 addr) const;
QString formatRuntime(quint64 runtimeVa) const;
QString describe(quint64 addr) const;
void probe(const QString &uri, const std::function<void(QString)> &done);
void openTarget(
const QString &uri,
bool suspended,
const QString &deviceLabel,
const QString &transportLabel);
void detach();
void resume();
void command(const QString &fridaCmd, const std::function<void(QString)> &done);
QByteArray readBytes(quint64 runtime, int length);
void importExports(const QString &moduleName);
QString writeScript(const QString &source);
void note(const QString &text);
void addHook(quint64 address, const QString &summary);
signals:
void changed();
void errorMessage(const QString &message);
void consoleMessage(const QString &message);
private:
int fridaFd() const { return backend_.fridaFd(); }
void setState(FridaSessionState state);
void clearTarget();
void readSnapshot();
void applyInfo(const QJsonObject &info);
void applyModules(const QJsonArray &modules);
void applyMaps(const QJsonArray &maps);
FridaBackend backend_;
FridaSessionState state_ = FridaSessionState::Disconnected;
bool available_ = false;
bool suspended_ = false;
int generation_ = 0;
QString lastError_;
QString deviceLabel_;
QString transportLabel_;
QString uri_;
FridaProcessInfo process_;
QList<FridaModuleInfo> modules_;
QList<FridaMapInfo> maps_;
QList<FridaHookInfo> hooks_;
quint64 staticBase_ = 0;
quint64 runtimeBase_ = 0;
quint64 matchedSize_ = 0;
quint64 slide_ = 0;
FridaAddressMode addressMode_ = FridaAddressMode::Runtime;
};
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <QString>
#include <QtGlobal>
enum class FridaTransport { Local, Usb, Remote };
enum class FridaAction { Attach, Spawn, Launch };
enum class FridaListKind { Devices, Processes, Applications };
enum class FridaAddressMode { Static, Runtime, ModuleOffset };
enum class FridaSessionState { Disconnected, Connecting, Attached, Error };
struct FridaDeviceInfo
{
QString id;
QString type;
QString name;
};
struct FridaTargetInfo
{
QString pid;
QString name;
QString identifier;
};
struct FridaModuleInfo
{
QString name;
QString path;
quint64 base = 0;
quint64 size = 0;
bool matched = false;
};
struct FridaMapInfo
{
quint64 start = 0;
quint64 end = 0;
QString permission;
QString path;
};
struct FridaProcessInfo
{
int pid = -1;
QString name;
QString identifier;
QString arch;
int bits = 0;
QString os;
QString moduleName;
quint64 moduleBase = 0;
};
struct FridaHookInfo
{
quint64 address = 0;
QString summary;
};
+89
View File
@@ -0,0 +1,89 @@
#include "FridaUri.h"
static QString actionName(FridaAction action)
{
switch (action) {
case FridaAction::Spawn:
return QStringLiteral("spawn");
case FridaAction::Launch:
return QStringLiteral("launch");
case FridaAction::Attach:
break;
}
return QStringLiteral("attach");
}
static QString linkName(FridaTransport transport)
{
switch (transport) {
case FridaTransport::Usb:
return QStringLiteral("usb");
case FridaTransport::Remote:
return QStringLiteral("remote");
case FridaTransport::Local:
break;
}
return QStringLiteral("local");
}
QString FridaUri::quote(const QString &text)
{
QString escaped = text;
escaped.replace(QLatin1Char('\''), QStringLiteral("'\\''"));
return QStringLiteral("'") + escaped + QStringLiteral("'");
}
bool FridaUri::targetIsSafe(const QString &targetId)
{
if (targetId.isEmpty()) {
return false;
}
const QString forbidden = QStringLiteral("/'\"`;|@\n\r");
for (const QChar ch : targetId) {
if (forbidden.contains(ch)) {
return false;
}
}
return true;
}
QString FridaUri::listDevices()
{
// Three components with an empty device id: r2frida dumps every device
// (local, usb, and remote), then fails the open on purpose.
return QStringLiteral("frida://list/usb/");
}
QString FridaUri::listTargets(
FridaTransport transport, FridaListKind kind, const QString &deviceId)
{
const QString action = kind == FridaListKind::Applications ? QStringLiteral("apps")
: QStringLiteral("list");
QString id = deviceId;
if (transport == FridaTransport::Usb && (id.isEmpty() || id == QStringLiteral("usb"))) {
id.clear();
}
if (transport == FridaTransport::Local) {
id.clear();
}
return QStringLiteral("frida://") + action + QStringLiteral("/") + linkName(transport)
+ QStringLiteral("/") + id + QStringLiteral("/");
}
QString FridaUri::target(
FridaTransport transport,
FridaAction action,
const QString &deviceId,
const QString &targetId)
{
const QString verb = actionName(action);
if (transport == FridaTransport::Local) {
return QStringLiteral("frida://") + verb + QStringLiteral("/") + targetId;
}
QString id = deviceId;
if (transport == FridaTransport::Usb && (id.isEmpty() || id == QStringLiteral("usb"))) {
id.clear();
}
return QStringLiteral("frida://") + verb + QStringLiteral("/") + linkName(transport)
+ QStringLiteral("/") + id + QStringLiteral("/") + targetId;
}
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "FridaTypes.h"
class FridaUri
{
public:
static QString listDevices();
static QString listTargets(
FridaTransport transport, FridaListKind kind, const QString &deviceId);
static QString target(
FridaTransport transport,
FridaAction action,
const QString &deviceId,
const QString &targetId);
static QString quote(const QString &text);
static bool targetIsSafe(const QString &targetId);
};
+345
View File
@@ -0,0 +1,345 @@
#include "FridaWidget.h"
#include "FridaConnectDialog.h"
#include "FridaPanels.h"
#include "FridaSession.h"
#include "core/Iaito.h"
#include "core/MainWindow.h"
#include "widgets/HexdumpWidget.h"
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QTabWidget>
#include <QTreeWidget>
#include <QVBoxLayout>
quint64 fridaRuntimeAddress(FridaSession *session, quint64 addr)
{
if (!session) {
return addr;
}
const quint64 runtime = session->toRuntime(addr);
return runtime == RVA_INVALID ? addr : runtime;
}
void fridaSeekRuntime(FridaSession *session, quint64 runtimeVa)
{
if (!session) {
return;
}
const quint64 staticVa = session->toStatic(runtimeVa);
if (staticVa == RVA_INVALID) {
Core()->message(QObject::tr("No static mapping for %1").arg(RAddressString(runtimeVa)));
session->note(session->describe(runtimeVa));
return;
}
Core()->seekAndShow(staticVa);
}
void fridaTrace(FridaSession *session, quint64 addr)
{
if (!session || !session->isAttached()) {
return;
}
const quint64 runtime = fridaRuntimeAddress(session, addr);
const QString command = QStringLiteral("dt ") + RAddressString(runtime);
session->command(command, [session](const QString &output) {
session->note(output.trimmed().isEmpty() ? QObject::tr("trace installed") : output);
});
}
void fridaBreakpoint(FridaSession *session, quint64 addr)
{
if (!session || !session->isAttached()) {
return;
}
const quint64 runtime = fridaRuntimeAddress(session, addr);
const QString command = QStringLiteral("db ") + RAddressString(runtime);
session->command(command, [session](const QString &output) {
session->note(output.trimmed().isEmpty() ? QObject::tr("breakpoint set") : output);
});
}
void fridaShowHookDialog(QWidget *parent, FridaSession *session, quint64 addr)
{
if (!session || !session->isAttached()) {
return;
}
const quint64 runtime = fridaRuntimeAddress(session, addr);
auto *dialog = new QDialog(parent);
dialog->setWindowTitle(QObject::tr("Hook %1").arg(RAddressString(runtime)));
dialog->setAttribute(Qt::WA_DeleteOnClose);
auto *layout = new QVBoxLayout(dialog);
layout->addWidget(new QLabel(session->describe(addr), dialog));
auto *logArgs = new QCheckBox(QObject::tr("Log arguments"), dialog);
auto *logRet = new QCheckBox(QObject::tr("Log return value"), dialog);
auto *backtrace = new QCheckBox(QObject::tr("Backtrace"), dialog);
logArgs->setChecked(true);
logRet->setChecked(true);
layout->addWidget(logArgs);
layout->addWidget(logRet);
layout->addWidget(backtrace);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dialog);
buttons->button(QDialogButtonBox::Ok)->setText(QObject::tr("Install Hook"));
layout->addWidget(buttons);
QObject::connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::reject);
QObject::connect(buttons, &QDialogButtonBox::accepted, dialog, [=]() {
QString body = QStringLiteral("Interceptor.attach(ptr('%1'), {\n")
.arg(RAddressString(runtime));
body += QStringLiteral(" onEnter(args) {\n");
if (logArgs->isChecked()) {
body += QStringLiteral(
" console.log('hook', this.context.pc, args[0], args[1], args[2], args[3]);\n");
}
if (backtrace->isChecked()) {
body += QStringLiteral(
" console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)"
".map(DebugSymbol.fromAddress).join('\\n'));\n");
}
body += QStringLiteral(" },\n onLeave(retval) {\n");
if (logRet->isChecked()) {
body += QStringLiteral(" console.log('return', retval);\n");
}
body += QStringLiteral(" }\n});\n");
const QString path = session->writeScript(body);
if (path.isEmpty()) {
return;
}
session->addHook(runtime, QObject::tr("Interceptor.attach"));
session->command(QStringLiteral(". ") + path, [session](const QString &output) {
session->note(output.trimmed().isEmpty() ? QObject::tr("hook installed") : output);
});
dialog->accept();
});
dialog->open();
}
void fridaShowLiveHex(QWidget *parent, FridaSession *session, quint64 runtimeAddr)
{
if (!session || !session->isAttached() || !parent) {
return;
}
auto *main = qobject_cast<MainWindow *>(parent->window());
if (!main) {
return;
}
HexdumpWidget *hex = nullptr;
const auto dumps = main->findChildren<HexdumpWidget *>();
for (HexdumpWidget *dump : dumps) {
if (!dump->isHidden()) {
hex = dump;
break;
}
}
if (!hex && !dumps.isEmpty()) {
hex = dumps.first();
}
if (!hex) {
return;
}
auto *toggle = hex->findChild<QCheckBox *>(QStringLiteral("fridaLiveToggle"));
if (toggle && !toggle->isChecked()) {
toggle->setChecked(true);
} else if (auto *view = hex->findChild<HexWidget *>(QStringLiteral("hexTextView"))) {
view->refresh();
}
const quint64 staticVa = session->toStatic(runtimeAddr);
hex->show();
hex->raise();
if (IaitoSeekable *seekable = hex->getSeekable()) {
seekable->seek(staticVa == RVA_INVALID ? runtimeAddr : staticVa);
}
}
void fridaShowExports(QWidget *parent, FridaSession *session, const QString &moduleName)
{
const QString command = QStringLiteral("iEj ") + moduleName;
session->command(command, [parent, session, moduleName](const QString &output) {
const QJsonDocument doc = FridaBackend::parseJsonLoose(output);
auto *dialog = new QDialog(parent);
dialog->setWindowTitle(QObject::tr("Exports %1").arg(moduleName));
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->resize(640, 480);
auto *view = new QTreeWidget(dialog);
view->setHeaderLabels(
{QObject::tr("Address"), QObject::tr("Type"), QObject::tr("Name")});
view->setRootIsDecorated(false);
for (const QJsonValue value : doc.array()) {
const QJsonObject exp = value.toObject();
const quint64 runtime
= FridaBackend::parseAddress(exp.value(QStringLiteral("address")));
auto *item = new QTreeWidgetItem(view);
item->setText(0, session->formatRuntime(runtime));
item->setText(1, exp.value(QStringLiteral("type")).toString());
item->setText(2, exp.value(QStringLiteral("name")).toString());
item->setData(0, Qt::UserRole, QVariant::fromValue(runtime));
}
auto *layout = new QVBoxLayout(dialog);
layout->addWidget(view);
QObject::connect(
view, &QTreeWidget::itemActivated, session, [session](QTreeWidgetItem *item, int) {
fridaSeekRuntime(session, item->data(0, Qt::UserRole).toULongLong());
});
dialog->open();
});
}
FridaWidget::FridaWidget(FridaSession *fridaSession, MainWindow *main)
: IaitoDockWidget(main)
, session(fridaSession)
{
setObjectName(QStringLiteral("FridaWidget"));
setWindowTitle(tr("r2frida"));
auto *root = new QWidget(this);
auto *layout = new QVBoxLayout(root);
auto *header = new QHBoxLayout();
statusDot = new QWidget(root);
statusDot->setFixedSize(10, 10);
stateLabel = new QLabel(root);
connectButton = new QPushButton(tr("Connect"), root);
resumeButton = new QPushButton(tr("Resume"), root);
detachButton = new QPushButton(tr("Detach"), root);
detachButton->setStyleSheet(QStringLiteral(
"QPushButton { background-color: #E5534B; color: white; border: none;"
" border-radius: 4px; padding: 4px 12px; }"
"QPushButton:disabled { background-color: palette(mid); color: palette(button-text); }"));
header->addWidget(statusDot, 0, Qt::AlignVCenter);
header->addWidget(stateLabel, 1);
header->addWidget(resumeButton);
header->addWidget(connectButton);
header->addWidget(detachButton);
layout->addLayout(header);
deviceCombo = new QComboBox(root);
targetCombo = new QComboBox(root);
pidLabel = new QLabel(root);
archLabel = new QLabel(root);
sessionLabel = new QLabel(root);
sessionLabel->setWordWrap(true);
sessionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto *targetRow = new QHBoxLayout();
targetRow->addWidget(targetCombo, 1);
targetRow->addWidget(pidLabel);
auto *targetWrap = new QWidget(root);
targetWrap->setLayout(targetRow);
auto *form = new QFormLayout();
form->setContentsMargins(0, 0, 0, 0);
form->addRow(tr("Device"), deviceCombo);
form->addRow(tr("Target"), targetWrap);
form->addRow(tr("Architecture"), archLabel);
form->addRow(tr("Session"), sessionLabel);
layout->addLayout(form);
auto *tabs = new QTabWidget(root);
for (QWidget *page : createFridaPanels(session)) {
tabs->addTab(page, page->windowTitle());
}
layout->addWidget(tabs, 1);
setWidget(root);
connect(session, &FridaSession::changed, this, &FridaWidget::refresh);
connect(session, &FridaSession::errorMessage, this, [this](const QString &message) {
QMessageBox::warning(this, tr("r2frida"), message);
});
connect(connectButton, &QPushButton::clicked, this, &FridaWidget::showConnectDialog);
connect(resumeButton, &QPushButton::clicked, session, &FridaSession::resume);
connect(detachButton, &QPushButton::clicked, session, &FridaSession::detach);
const auto openConnect = [this](int index) {
if (index == 1) {
showConnectDialog();
deviceCombo->setCurrentIndex(0);
targetCombo->setCurrentIndex(0);
}
};
connect(deviceCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, openConnect);
connect(targetCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, openConnect);
refresh();
hide();
}
void FridaWidget::showConnectDialog()
{
auto *dialog = new FridaConnectDialog(session, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->open();
}
void FridaWidget::fillChoice(QComboBox *combo, const QString &current)
{
const bool blocked = combo->blockSignals(true);
combo->clear();
combo->addItem(current.isEmpty() ? tr("Not connected") : current);
combo->addItem(tr("Connect..."));
combo->setCurrentIndex(0);
combo->blockSignals(blocked);
}
void FridaWidget::setLive(bool live)
{
const QString color = live ? QStringLiteral("#3DDC84") : QStringLiteral("#E5534B");
statusDot->setStyleSheet(
QStringLiteral("background-color: %1; border-radius: 5px;").arg(color));
}
void FridaWidget::refresh()
{
const bool attached = session->isAttached();
const bool spawned = attached && session->isSuspended();
QString stateText = tr("Disconnected");
if (session->state() == FridaSessionState::Connecting) {
stateText = tr("Connecting");
} else if (spawned) {
stateText = tr("Spawned");
} else if (attached) {
stateText = tr("Connected");
} else if (session->state() == FridaSessionState::Error) {
stateText = tr("Error");
}
stateLabel->setText(stateText);
setLive(attached);
const FridaProcessInfo info = session->process();
QString device = session->deviceLabel();
if (!session->transportLabel().isEmpty()) {
device = device.isEmpty()
? session->transportLabel()
: session->transportLabel() + QStringLiteral(": ") + device;
}
fillChoice(deviceCombo, attached ? device : QString());
fillChoice(targetCombo, attached ? info.name : QString());
const QString pid = info.pid >= 0 ? QString::number(info.pid) : QStringLiteral("—");
pidLabel->setText(tr("PID: %1").arg(pid));
archLabel->setText(info.arch.isEmpty() ? QStringLiteral("—") : info.arch);
QString sessionText = info.identifier;
if (sessionText.isEmpty()) {
sessionText = session->uri().isEmpty() ? QStringLiteral("—") : session->uri();
}
sessionLabel->setText(sessionText);
sessionLabel->setToolTip(session->uri());
resumeButton->setEnabled(spawned);
detachButton->setEnabled(attached || session->state() == FridaSessionState::Connecting);
if (attached && !wasAttached) {
show();
raise();
}
wasAttached = attached;
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "widgets/IaitoDockWidget.h"
class FridaSession;
class QComboBox;
class QLabel;
class QPushButton;
class QWidget;
class FridaWidget : public IaitoDockWidget
{
Q_OBJECT
public:
explicit FridaWidget(FridaSession *session, MainWindow *main);
void showConnectDialog();
private:
void refresh();
void fillChoice(QComboBox *combo, const QString &current);
void setLive(bool live);
FridaSession *session = nullptr;
QWidget *statusDot = nullptr;
QLabel *stateLabel = nullptr;
QLabel *pidLabel = nullptr;
QLabel *archLabel = nullptr;
QLabel *sessionLabel = nullptr;
QComboBox *deviceCombo = nullptr;
QComboBox *targetCombo = nullptr;
QPushButton *connectButton = nullptr;
QPushButton *resumeButton = nullptr;
QPushButton *detachButton = nullptr;
bool wasAttached = false;
};
quint64 fridaRuntimeAddress(FridaSession *session, quint64 addr);
void fridaSeekRuntime(FridaSession *session, quint64 runtimeVa);
void fridaTrace(FridaSession *session, quint64 addr);
void fridaBreakpoint(FridaSession *session, quint64 addr);
void fridaShowHookDialog(QWidget *parent, FridaSession *session, quint64 addr);
void fridaShowLiveHex(QWidget *parent, FridaSession *session, quint64 runtimeAddr);
void fridaShowExports(QWidget *parent, FridaSession *session, const QString &moduleName);
+298
View File
@@ -0,0 +1,298 @@
#include "IaitoFridaPlugin.h"
#include "FridaSession.h"
#include "FridaWidget.h"
#include "core/Iaito.h"
#include "core/MainWindow.h"
#include "widgets/ConsoleWidget.h"
#include "widgets/HexdumpWidget.h"
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QLabel>
#include <QMenu>
#include <QPointer>
#include <QSignalBlocker>
#include <QStatusBar>
#include <QTimer>
#include <QVBoxLayout>
namespace {
ConsoleWidget *sessionConsole(MainWindow *main)
{
return main ? main->findChild<ConsoleWidget *>() : nullptr;
}
} // namespace
void IaitoFridaPlugin::setupPlugin() {}
void IaitoFridaPlugin::terminate()
{
HexdumpWidget::setCustomizeHook(nullptr);
if (mainWindow) {
if (ConsoleWidget *console = sessionConsole(mainWindow)) {
console->setCommandHandler(nullptr);
}
for (HexdumpWidget *dump : mainWindow->findChildren<HexdumpWidget *>()) {
if (auto *view = dump->findChild<HexWidget *>(QStringLiteral("hexTextView"))) {
view->setDataReader({});
}
}
}
if (session && session->isAttached()) {
session->detach();
}
}
void IaitoFridaPlugin::setupInterface(MainWindow *main)
{
mainWindow = main;
session = new FridaSession(main);
widget = new FridaWidget(session, main);
main->addPluginDockWidget(widget);
widget->hide();
statusButton = new QToolButton(main);
statusButton->setAutoRaise(false);
statusButton->setToolTip(tr("r2frida session"));
statusDetail = new QLabel(main);
main->statusBar()->addPermanentWidget(statusButton);
main->statusBar()->addPermanentWidget(statusDetail);
connect(statusButton, &QToolButton::clicked, this, [this]() {
widget->show();
widget->raise();
});
connect(session, &FridaSession::changed, this, &IaitoFridaPlugin::updateStatus);
if (ConsoleWidget *console = sessionConsole(main)) {
console->setCommandHandler(
[this](const QString &command) { return handleConsoleCommand(command); });
connect(session, &FridaSession::consoleMessage, console, &ConsoleWidget::addOutput);
}
HexdumpWidget::setCustomizeHook(
[this](HexdumpWidget *hexdump) { installLiveHexdump(hexdump); });
QTimer::singleShot(0, this, [this, main]() { buildMenu(main); });
buildContextMenus(main);
updateStatus();
}
bool IaitoFridaPlugin::handleConsoleCommand(const QString &command)
{
const QString line = command.trimmed();
if (!line.startsWith(QLatin1Char(':'))) {
return false;
}
ConsoleWidget *console = sessionConsole(mainWindow);
if (!console) {
return false;
}
const QString frida = line.mid(1);
if (frida.contains(QLatin1Char(';')) || frida.contains(QLatin1Char('|'))
|| frida.contains(QLatin1Char('`'))) {
console->addOutput(tr("That command is not sent to r2frida."));
return true;
}
if (!session->isAttached()) {
console->addOutput(tr("r2frida is not attached."));
return true;
}
QPointer<ConsoleWidget> target = console;
session->command(frida, [target](const QString &output) {
if (target) {
target->addOutput(output);
}
});
return true;
}
QByteArray IaitoFridaPlugin::readLiveBytes(uint64_t addr, int len) const
{
if (!session || !session->isAttached() || len <= 0) {
return QByteArray(qMax(len, 0), '\0');
}
quint64 runtime = RVA_INVALID;
if (session->containsStatic(addr)) {
runtime = session->toRuntime(addr);
} else if (session->containsRuntime(addr)) {
runtime = addr;
}
if (runtime == RVA_INVALID) {
return QByteArray(len, '\0');
}
QByteArray bytes = session->readBytes(runtime, len);
if (bytes.size() < len) {
bytes.append(QByteArray(len - bytes.size(), '\0'));
} else if (bytes.size() > len) {
bytes.truncate(len);
}
return bytes;
}
void IaitoFridaPlugin::installLiveHexdump(HexdumpWidget *hexdump)
{
if (!hexdump || hexdump->findChild<QCheckBox *>(QStringLiteral("fridaLiveToggle"))) {
return;
}
auto *live = new QCheckBox(tr("Live process"), hexdump);
live->setObjectName(QStringLiteral("fridaLiveToggle"));
live->setToolTip(tr("Show bytes from the attached r2frida target"));
if (QLayout *layout = hexdump->widget() ? hexdump->widget()->layout() : nullptr) {
if (auto *box = qobject_cast<QVBoxLayout *>(layout)) {
box->insertWidget(0, live);
}
}
auto *view = hexdump->findChild<HexWidget *>(QStringLiteral("hexTextView"));
connect(live, &QCheckBox::toggled, this, [this, live, view](bool on) {
if (!view) {
return;
}
if (!on) {
view->setDataReader({});
return;
}
if (!session || !session->isAttached()) {
const QSignalBlocker blocker(live);
live->setChecked(false);
if (ConsoleWidget *console = sessionConsole(mainWindow)) {
console->addOutput(tr("r2frida is not attached."));
}
return;
}
view->setDataReader([this](uint64_t addr, int len) { return readLiveBytes(addr, len); });
});
connect(session, &FridaSession::changed, this, [this, live, view]() {
if (!session->isAttached()) {
if (live->isChecked()) {
live->setChecked(false);
}
return;
}
if (live->isChecked() && view) {
view->refresh();
}
});
}
void IaitoFridaPlugin::buildMenu(MainWindow *main)
{
QMenu *debugMenu = main->getMenuByType(MainWindow::MenuType::Debug);
if (!debugMenu) {
return;
}
debugMenu->addSeparator();
auto *menu = debugMenu->addMenu(tr("r2frida"));
connectAction = menu->addAction(tr("Connect..."), widget, &FridaWidget::showConnectDialog);
resumeAction = menu->addAction(tr("Resume"), session, &FridaSession::resume);
detachAction = menu->addAction(tr("Detach"), session, &FridaSession::detach);
menu->addSeparator();
menu->addAction(tr("Show panel"), this, [this]() {
widget->show();
widget->raise();
});
if (!session->checkAvailable()) {
connectAction->setEnabled(false);
connectAction->setToolTip(
tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
}
updateStatus();
}
void IaitoFridaPlugin::addFridaMenu(QMenu *pluginMenu)
{
if (!pluginMenu) {
return;
}
auto *menu = pluginMenu->addMenu(tr("Frida"));
auto *trace = menu->addAction(tr("Trace"));
auto *hook = menu->addAction(tr("Hook"));
auto *breakpoint = menu->addAction(tr("Set breakpoint"));
auto *live = menu->addAction(tr("Read live bytes"));
auto *copy = menu->addAction(tr("Copy runtime address"));
auto *mapping = menu->addAction(tr("Show address mapping"));
const auto enable = [this, menu]() {
const bool on = session->isAttached();
for (QAction *action : menu->actions()) {
action->setEnabled(on);
action->setData(menu->menuAction()->data());
}
};
connect(menu, &QMenu::aboutToShow, this, enable);
connect(trace, &QAction::triggered, this, [this, trace]() {
fridaTrace(session, trace->data().toULongLong());
});
connect(hook, &QAction::triggered, this, [this, hook]() {
fridaShowHookDialog(mainWindow, session, hook->data().toULongLong());
});
connect(breakpoint, &QAction::triggered, this, [this, breakpoint]() {
fridaBreakpoint(session, breakpoint->data().toULongLong());
});
connect(live, &QAction::triggered, this, [this, live]() {
const quint64 addr = live->data().toULongLong();
fridaShowLiveHex(mainWindow, session, fridaRuntimeAddress(session, addr));
});
connect(copy, &QAction::triggered, this, [this, copy]() {
const quint64 runtime = fridaRuntimeAddress(session, copy->data().toULongLong());
QApplication::clipboard()->setText(RAddressString(runtime));
});
connect(mapping, &QAction::triggered, this, [this, mapping]() {
Core()->message(session->describe(mapping->data().toULongLong()));
});
}
void IaitoFridaPlugin::buildContextMenus(MainWindow *main)
{
addFridaMenu(main->getContextMenuExtensions(MainWindow::ContextMenuType::Disassembly));
addFridaMenu(main->getContextMenuExtensions(MainWindow::ContextMenuType::Addressable));
}
void IaitoFridaPlugin::updateStatus()
{
if (!statusButton) {
return;
}
const bool live = session->isAttached();
const QString color = live ? QStringLiteral("#2E7D32") : QStringLiteral("#C62828");
statusButton->setText(tr("FRIDA"));
statusButton->setStyleSheet(QStringLiteral(
"QToolButton { color: white; background-color: %1; border: none;"
" border-radius: 3px; padding: 0 6px; font-weight: 600; }")
.arg(color));
if (!session->checkAvailable()) {
statusButton->setToolTip(
tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
statusDetail->clear();
} else if (live) {
const FridaProcessInfo info = session->process();
QString arch = info.arch;
if (!arch.isEmpty()) {
arch = arch.toUpper();
}
statusDetail->setText(
tr("%1 %2 [%3] %4 %5")
.arg(
session->deviceLabel(),
info.name,
QString::number(info.pid),
arch,
session->transportLabel().toUpper()));
statusButton->setToolTip(tr("%1 | %2 | slide %3")
.arg(
session->transportLabel(),
session->deviceLabel(),
RAddressString(session->slide())));
} else if (session->state() == FridaSessionState::Connecting) {
statusButton->setToolTip(tr("r2frida is connecting"));
statusDetail->setText(tr("connecting"));
} else {
statusButton->setToolTip(tr("r2frida is disconnected"));
statusDetail->clear();
}
if (resumeAction) {
resumeAction->setEnabled(session->isSuspended());
}
if (detachAction) {
detachAction->setEnabled(session->isAttached());
}
}
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include "IaitoPlugin.h"
#include <QByteArray>
#include <QObject>
class FridaSession;
class FridaWidget;
class HexdumpWidget;
class MainWindow;
class QAction;
class QMenu;
class QLabel;
class QToolButton;
class IaitoFridaPlugin : public QObject, public IaitoPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.radare.iaito.plugins.IaitoPlugin")
Q_INTERFACES(IaitoPlugin)
public:
void setupPlugin() override;
void setupInterface(MainWindow *main) override;
void terminate() override;
QString getName() const override { return QStringLiteral("r2frida"); }
QString getAuthor() const override { return QStringLiteral("radareorg"); }
QString getDescription() const override
{
return QStringLiteral("Dynamic instrumentation through r2frida.");
}
QString getVersion() const override { return QStringLiteral("1.0"); }
private:
void buildMenu(MainWindow *main);
void buildContextMenus(MainWindow *main);
void updateStatus();
void addFridaMenu(QMenu *pluginMenu);
void installLiveHexdump(HexdumpWidget *hexdump);
bool handleConsoleCommand(const QString &command);
QByteArray readLiveBytes(uint64_t addr, int len) const;
FridaSession *session = nullptr;
FridaWidget *widget = nullptr;
MainWindow *mainWindow = nullptr;
QToolButton *statusButton = nullptr;
QLabel *statusDetail = nullptr;
QAction *connectAction = nullptr;
QAction *detachAction = nullptr;
QAction *resumeAction = nullptr;
};
+44
View File
@@ -0,0 +1,44 @@
TEMPLATE = lib
CONFIG += plugin
CONFIG -= import_plugins
QT += widgets svg network
TARGET = IaitoFridaPlugin
DESTDIR = $$PWD/../../../build/plugins/native
QMAKE_CXXFLAGS += $$system(pkg-config --cflags r_core)
QMAKE_CXXFLAGS += -std=c++20
INCLUDEPATH += \
$$PWD \
$$PWD/.. \
$$PWD/../.. \
$$PWD/../../common \
$$PWD/../../core \
$$PWD/../../widgets \
$$PWD/../../dialogs \
$$PWD/../../menus \
$$PWD/../../plugins \
$$system(r2 -H R2_INCDIR)
macx: QMAKE_LFLAGS += -undefined dynamic_lookup
unix:!macx: QMAKE_LFLAGS += -Wl,--unresolved-symbols=ignore-all
HEADERS += \
FridaTypes.h \
FridaUri.h \
FridaBackend.h \
FridaSession.h \
FridaConnectDialog.h \
FridaWidget.h \
FridaPanels.h \
IaitoFridaPlugin.h \
../IaitoPlugin.h
SOURCES += \
FridaUri.cpp \
FridaBackend.cpp \
FridaSession.cpp \
FridaConnectDialog.cpp \
FridaWidget.cpp \
FridaPanels.cpp \
IaitoFridaPlugin.cpp
+2
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>
+62 -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);
+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;
+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>
+9 -1
View File
@@ -150,7 +150,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);
@@ -260,6 +261,13 @@ void ConsoleWidget::executeCommand(const QString &command)
QString cmd_line = "[" + RAddressString(Core()->getOffset()) + "]> " + command;
addOutput(cmd_line);
if (commandHandler && commandHandler(command)) {
historyAdd(command);
ui->r2InputLineEdit->setEnabled(true);
ui->r2InputLineEdit->setFocus();
return;
}
RVA oldOffset = Core()->getOffset();
bool refreshAll = consoleCommandRefreshesAll(command);
bool isPiped = command.contains(">");
+8
View File
@@ -10,6 +10,7 @@
#include <QSocketNotifier>
#include <QStringListModel>
#include <functional>
#include <memory>
class QCompleter;
@@ -32,6 +33,12 @@ public:
void setMaxHistoryEntries(int max) { maxHistoryEntries = max; }
// Return true to handle the command instead of sending it to radare2.
void setCommandHandler(const std::function<bool(const QString &command)> &handler)
{
commandHandler = handler;
}
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
QWidget *widgetToFocusOnRaise() override;
@@ -86,6 +93,7 @@ private:
void redirectOutput();
QSharedPointer<CommandTask> commandTask;
std::function<bool(const QString &)> commandHandler;
std::unique_ptr<Ui::ConsoleWidget> ui;
QAction *actionWrapLines;
+679
View File
@@ -0,0 +1,679 @@
#include "DataAnalysisView.h"
#include <QApplication>
#include <QContextMenuEvent>
#include <QGestureEvent>
#include <QKeyEvent>
#include <QLinearGradient>
#include <QMouseEvent>
#include <QNativeGestureEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPinchGesture>
#include <QToolTip>
#include <QWheelEvent>
#include <algorithm>
#include <cmath>
using namespace DataAnalysis;
DataAnalysisView::DataAnalysisView(bool overview, QWidget *parent)
: QWidget(parent)
, overview(overview)
{
setMouseTracking(true);
setFocusPolicy(Qt::StrongFocus);
#ifndef Q_OS_MACOS
// On macOS, handle native trackpad events directly to retain their position.
grabGesture(Qt::PinchGesture);
#endif
setMinimumSize(220, overview ? 70 : 180);
setAccessibleName(overview ? tr("Data overview") : tr("Data analysis graph"));
setAccessibleDescription(
overview
? tr("Drag the window or use the wheel to pan; drag its edges to resize; pinch to zoom."
" Double-click to fit.")
: tr("Click to seek; drag to select bytes; wheel to pan; pinch to zoom."
" Right-click for comments, flags and range actions."));
tooltipTimer.setSingleShot(true);
tooltipTimer.setInterval(650);
connect(&tooltipTimer, &QTimer::timeout, this, &DataAnalysisView::showTooltip);
}
QString DataAnalysisView::metricName(int metric)
{
static const char *names[]
= {QT_TR_NOOP("Entropy"),
QT_TR_NOOP("Printable ASCII"),
QT_TR_NOOP("0x00 bytes"),
QT_TR_NOOP("0xFF bytes"),
QT_TR_NOOP("Unique bytes"),
QT_TR_NOOP("Mean byte"),
QT_TR_NOOP("ASCII letters"),
QT_TR_NOOP("ASCII digits"),
QT_TR_NOOP("ASCII whitespace"),
QT_TR_NOOP("ASCII control"),
QT_TR_NOOP("High bytes")};
return metric >= 0 && metric < Count ? tr(names[metric]) : QString();
}
QString DataAnalysisView::metricValue(int metric, double value)
{
if (metric == Entropy) {
return tr("%1 bits/byte").arg(value * 8, 0, 'f', 3);
}
if (metric == Unique) {
return tr("%1 / 256").arg(qRound(value * 256));
}
if (metric == Mean) {
return QString::number(value * 255, 'f', 2);
}
return tr("%1%").arg(value * 100, 0, 'f', 2);
}
QString DataAnalysisView::metricDescription(int metric)
{
static const char *descriptions[]
= {QT_TR_NOOP(
"Shannon entropy: 0–8 bits per byte. Higher values indicate a more varied byte "
"distribution. Overlaid values are divided by 8."),
QT_TR_NOOP("Percentage of printable ASCII bytes (0x20–0x7e)."),
QT_TR_NOOP("Percentage of zero bytes (0x00), often padding or uninitialized data."),
QT_TR_NOOP("Percentage of 0xff bytes, often padding or erased storage."),
QT_TR_NOOP("Number of distinct byte values observed, out of 256 possible values."),
QT_TR_NOOP("Average byte value (0–255). Overlaid values are divided by 255."),
QT_TR_NOOP("Percentage of ASCII letters (A–Z and a–z)."),
QT_TR_NOOP("Percentage of ASCII decimal digits (0–9)."),
QT_TR_NOOP(
"Percentage of ASCII whitespace (space, tab, newline, vertical tab, form feed and "
"carriage return)."),
QT_TR_NOOP("Percentage of ASCII control bytes (0x00–0x1f and 0x7f)."),
QT_TR_NOOP("Percentage of bytes with the high bit set (0x80–0xff).")};
return metric >= 0 && metric < Count ? tr(descriptions[metric]) : QString();
}
QString DataAnalysisView::markerName(MarkerType type)
{
static const char *names[]
= {QT_TR_NOOP("Section"),
QT_TR_NOOP("Segment"),
QT_TR_NOOP("Map"),
QT_TR_NOOP("Flag"),
QT_TR_NOOP("Comment")};
return tr(names[type]);
}
Range DataAnalysisView::axisRange() const
{
return overview ? domain : visibleRange;
}
QRectF DataAnalysisView::plotRect() const
{
return QRectF(
46, overview ? 5 : 30, qMax(1, width() - 60), qMax(1, height() - (overview ? 26 : 59)));
}
qreal DataAnalysisView::xForAddress(RVA address) const
{
const Range range = axisRange();
if (!range.valid()) {
return plotRect().left();
}
const long double distance = address >= range.start ? (long double) (address - range.start)
: -(long double) (range.start - address);
return plotRect().left() + distance / range.size() * plotRect().width();
}
RVA DataAnalysisView::addressAt(qreal x) const
{
const Range range = axisRange();
if (!range.valid()) {
return RVA_INVALID;
}
const long double fraction = (x - plotRect().left()) / plotRect().width();
if (fraction <= 0) {
return range.start;
}
if (fraction >= 1) {
return range.end - 1;
}
return range.start + qMin<RVA>(RVA(fraction * range.size()), range.size() - 1);
}
void DataAnalysisView::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.fillRect(rect(), palette().color(QPalette::Base));
const QRectF plot = plotRect();
const Range range = axisRange();
const QColor text = palette().color(QPalette::Text);
QColor grid = text;
grid.setAlpha(35);
p.setPen(grid);
p.drawRect(plot);
if (!range.valid()) {
p.setPen(text);
p.drawText(plot, Qt::AlignCenter, tr("Open a file to analyze its data"));
return;
}
const bool split = !overview && separateTracks;
const int tracks = split ? qMax(1, int(metrics.size())) : 1;
for (int track = 0; track < tracks; ++track) {
const qreal top = plot.top() + track * plot.height() / tracks;
const qreal bottom = plot.top() + (track + 1) * plot.height() / tracks;
const qreal graphTop = top + (split ? 19 : 0);
const qreal graphBottom = bottom - (split ? 5 : 0);
const qreal graphHeight = qMax(1.0, graphBottom - graphTop);
if (!overview) {
for (int tick = 0; tick <= 4; ++tick) {
const qreal y = graphBottom - graphHeight * tick / 4;
p.setPen(grid);
p.drawLine(QPointF(plot.left(), y), QPointF(plot.right(), y));
if (tracks == 1 || tick == 0 || tick == 4) {
int maximum = 100;
if ((separateTracks || metrics.size() == 1) && !metrics.isEmpty()) {
const int metric = metrics[track];
maximum = metric == Entropy ? 8
: metric == Mean ? 255
: metric == Unique ? 256
: 100;
}
p.setPen(text);
QString label = QString::number(maximum * tick / 4.0, 'g', 3);
if (maximum == 100 && tick == 4) {
label += '%';
}
p.drawText(
QRectF(0, y - 8, plot.left() - 7, 16),
Qt::AlignRight | Qt::AlignVCenter,
label);
}
}
if (split && !metrics.isEmpty()) {
p.setPen(colors[metrics[track]]);
p.drawText(
QRectF(plot.left() + 5, top, plot.width() - 10, 18),
Qt::AlignLeft | Qt::AlignVCenter,
metricName(metrics[track]));
}
}
p.save();
p.setClipRect(QRectF(plot.left(), graphTop, plot.width(), graphHeight));
const QVector<int> drawn = overview ? QVector<int>{Entropy}
: split && !metrics.isEmpty() ? QVector<int>{metrics[track]}
: metrics;
for (int series = 0; series < drawn.size(); ++series) {
const int metric = drawn[series];
const QColor color = colors[metric];
QPainterPath path;
qreal firstX = 0, lastX = 0;
auto flush = [&]() {
if (path.isEmpty()) {
return;
}
if (overview || style == Areas) {
QPainterPath area = path;
area.lineTo(lastX, graphBottom);
area.lineTo(firstX, graphBottom);
area.closeSubpath();
QLinearGradient gradient(0, graphTop, 0, graphBottom);
QColor fill = color;
fill.setAlpha(overview ? 110 : 85);
gradient.setColorAt(0, fill);
fill.setAlpha(12);
gradient.setColorAt(1, fill);
p.fillPath(area, gradient);
}
p.setPen(QPen(color, overview ? 1 : 1.5));
p.drawPath(path);
path = QPainterPath();
};
for (const auto &sample : samples) {
if (!sample.bytes) {
flush();
continue;
}
const qreal left = xForAddress(sample.range.start);
const qreal right = xForAddress(sample.range.end);
const qreal y = graphBottom - qBound(0.0, sample.values[metric], 1.0) * graphHeight;
if (!overview && style == Bars) {
const qreal barWidth = (right - left) / qMax(1, int(drawn.size()));
QColor fill = color;
fill.setAlpha(190);
p.fillRect(
QRectF(left + series * barWidth, y, qMax(0.6, barWidth), graphBottom - y),
fill);
} else {
if (path.isEmpty()) {
firstX = left;
path.moveTo(left, y);
}
path.lineTo((left + right) / 2, y);
lastX = right;
// Retain single-bucket signals and reach both ends of the range.
path.lineTo(right, y);
}
}
flush();
}
p.restore();
}
// Markers are coalesced by pixel and category to keep dense flag sets cheap to paint.
std::array<int, MarkerCount> lastPixel;
lastPixel.fill(-1000);
qreal labelEnd = plot.left();
for (const auto &marker : markers) {
if (!markerEnabled[marker.type]) {
continue;
}
const QColor color = colors[(marker.type + 4) % Count];
for (RVA address : {marker.range.start, marker.range.end}) {
if (address == marker.range.end && marker.type >= Flag) {
continue;
}
if (address < range.start || address > range.end) {
continue;
}
const qreal x = xForAddress(address);
const int pixel = qRound(x);
if (std::abs(pixel - lastPixel[marker.type]) < 2) {
continue;
}
lastPixel[marker.type] = pixel;
QColor line = color;
line.setAlpha(95);
p.setPen(QPen(line, 1, marker.type >= Flag ? Qt::DotLine : Qt::DashLine));
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
p.setPen(color);
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.top() + 5));
}
if (!overview && marker.type <= Map && intersect(marker.range, range).valid()) {
const qreal left = qMax(plot.left(), xForAddress(marker.range.start));
const qreal right = qMin(plot.right(), xForAddress(marker.range.end));
if (left >= labelEnd && right - left > 38) {
p.setPen(color);
const QString label
= fontMetrics().elidedText(marker.name, Qt::ElideRight, int(right - left - 6));
p.drawText(QRectF(left + 3, 4, right - left - 6, 20), Qt::AlignLeft, label);
labelEnd = left + fontMetrics().horizontalAdvance(label) + 10;
}
}
}
auto highlight = [&](Range selected, bool window) {
selected = intersect(selected, range);
if (!selected.valid()) {
return;
}
const qreal left = xForAddress(selected.start), right = xForAddress(selected.end);
QColor fill = palette().color(QPalette::Highlight);
fill.setAlpha(window ? 35 : 55);
p.fillRect(QRectF(left, plot.top(), qMax(1.0, right - left), plot.height()), fill);
p.setPen(QPen(palette().color(QPalette::Highlight), window ? 2 : 1));
p.drawRect(QRectF(left, plot.top(), qMax(1.0, right - left), plot.height()));
if (window) {
p.fillRect(QRectF(left - 2, plot.center().y() - 7, 4, 14), text);
p.fillRect(QRectF(right - 2, plot.center().y() - 7, 4, 14), text);
}
};
if (overview) {
const QColor shade(0, 0, 0, 65);
const qreal left = xForAddress(visibleRange.start), right = xForAddress(visibleRange.end);
p.fillRect(
QRectF(plot.left(), plot.top(), qMax(0.0, left - plot.left()), plot.height()), shade);
p.fillRect(QRectF(right, plot.top(), qMax(0.0, plot.right() - right), plot.height()), shade);
highlight(visibleRange, true);
}
highlight(drag == Select && dragSelection.valid() ? dragSelection : selection, false);
if (range.contains(seekAddress)) {
const qreal x = xForAddress(seekAddress);
p.setPen(QPen(palette().color(QPalette::Highlight), 2));
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
p.setBrush(palette().color(QPalette::Highlight));
p.drawPolygon(QPolygonF(
QVector<QPointF>{
QPointF(x - 4, plot.top()), QPointF(x + 4, plot.top()), QPointF(x, plot.top() + 6)}));
}
if (!overview && hoverX >= plot.left() && hoverX <= plot.right()) {
p.setPen(QPen(text, 1, Qt::DotLine));
p.drawLine(QPointF(hoverX, plot.top()), QPointF(hoverX, plot.bottom()));
}
p.setPen(text);
const int addressWidth = fontMetrics().horizontalAdvance(RAddressString(range.end - 1));
const int labels = qBound(1, int(plot.width() / (addressWidth + 24)), 4);
for (int i = 0; i <= labels; ++i) {
const qreal x = plot.left() + plot.width() * i / labels;
const QString label = fontMetrics().elidedText(
RAddressString(addressAt(x)), Qt::ElideMiddle, int(plot.width() / (labels + 1)));
const qreal width = fontMetrics().horizontalAdvance(label) + 2;
const qreal left
= qBound(plot.left(), x - width / 2, qMax(plot.left(), plot.right() - width));
p.drawText(QRectF(left, plot.bottom() + 4, width, 20), Qt::AlignLeft, label);
}
const bool hasData = std::any_of(samples.cbegin(), samples.cend(), [](const Sample &sample) {
return sample.bytes > 0;
});
if (!overview && !hasData) {
p.drawText(plot, Qt::AlignCenter, loading ? tr("Analyzing…") : tr("No readable data"));
}
}
bool DataAnalysisView::event(QEvent *event)
{
if (event->type() == QEvent::Gesture) {
auto *gestures = static_cast<QGestureEvent *>(event);
if (auto *pinch = static_cast<QPinchGesture *>(gestures->gesture(Qt::PinchGesture))) {
if (pinch->state() != Qt::GestureFinished && pinch->state() != Qt::GestureCanceled
&& (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged)) {
const QPoint position = mapFromGlobal(pinch->centerPoint().toPoint());
pinchZoom(pinch->scaleFactor(), position.x());
}
gestures->accept(pinch);
return true;
}
} else if (event->type() == QEvent::NativeGesture) {
auto *gesture = static_cast<QNativeGestureEvent *>(event);
if (gesture->gestureType() == Qt::ZoomNativeGesture) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const qreal x = gesture->position().x();
#else
const qreal x = gesture->localPos().x();
#endif
pinchZoom(1.0 + gesture->value(), x);
event->accept();
return true;
}
}
return QWidget::event(event);
}
void DataAnalysisView::pinchZoom(qreal scaleFactor, qreal x)
{
cancelTooltip();
drag = None;
dragSelection = {};
if (std::isfinite(scaleFactor) && scaleFactor > 0 && scaleFactor != 1) {
// Magnifying the graph narrows the address range around the gesture.
zoom(1.0 / scaleFactor, addressAt(x));
}
update();
}
void DataAnalysisView::zoom(double factor, RVA anchor)
{
if (!domain.valid() || !visibleRange.valid() || !std::isfinite(factor) || factor <= 0) {
return;
}
long double scaled = (long double) visibleRange.size() * factor;
if (factor > 1) {
// Small pinch steps must still let the user zoom out from a one-byte range.
scaled = std::ceil(scaled);
}
const RVA size = scaled >= domain.size() ? domain.size() : qMax<RVA>(1, RVA(scaled));
anchor = qBound(visibleRange.start, anchor, visibleRange.end - 1);
const long double ratio = (long double) (anchor - visibleRange.start) / visibleRange.size();
const RVA before = qMin<RVA>(size - 1, RVA(ratio * size));
const RVA start = anchor - qMin(anchor - domain.start, before);
const RVA clamped = qMin(start, domain.end - size);
emit rangeRequested(clamped, clamped + size);
}
void DataAnalysisView::pan(int direction)
{
if (!visibleRange.valid() || !domain.valid()) {
return;
}
const RVA step = qMax<RVA>(1, visibleRange.size() / 4);
const RVA start = direction < 0
? visibleRange.start - qMin(step, visibleRange.start - domain.start)
: visibleRange.start + qMin(step, domain.end - visibleRange.end);
emit rangeRequested(start, start + visibleRange.size());
}
void DataAnalysisView::mousePressEvent(QMouseEvent *event)
{
cancelTooltip();
if (event->button() != Qt::LeftButton || !axisRange().valid()
|| !plotRect().contains(event->pos())) {
return;
}
setFocus();
pressPosition = event->pos();
pressAddress = addressAt(event->pos().x());
pressRange = visibleRange;
dragSelection = {};
if (overview) {
if (std::abs(event->pos().x() - xForAddress(visibleRange.start)) < 7) {
drag = LeftEdge;
} else if (std::abs(event->pos().x() - xForAddress(visibleRange.end)) < 7) {
drag = RightEdge;
} else {
drag = Move;
if (!visibleRange.contains(pressAddress)) {
pressRange = centered(domain, pressAddress, visibleRange.size());
emit rangeRequested(pressRange.start, pressRange.end);
}
}
} else {
drag = Select;
}
event->accept();
}
void DataAnalysisView::updateDrag(QPoint position)
{
const RVA address = addressAt(position.x());
if (drag == Select) {
dragSelection = {qMin(address, pressAddress), qMax(address, pressAddress) + 1};
update();
} else if (drag == LeftEdge) {
emit rangeRequested(qMin(address, pressRange.end - 1), pressRange.end);
} else if (drag == RightEdge) {
emit rangeRequested(pressRange.start, qMax(address + 1, pressRange.start + 1));
} else if (drag == Move) {
const RVA start = address >= pressAddress
? pressRange.start
+ qMin(address - pressAddress, domain.end - pressRange.end)
: pressRange.start
- qMin(pressAddress - address, pressRange.start - domain.start);
emit rangeRequested(start, start + pressRange.size());
}
}
void DataAnalysisView::mouseMoveEvent(QMouseEvent *event)
{
const bool moved = hoverPosition != event->pos();
hoverPosition = event->pos();
hoverX = event->pos().x();
if (drag != None) {
updateDrag(event->pos());
} else if (plotRect().contains(event->pos())) {
if (moved) {
cancelTooltip();
tooltipTimer.start();
}
const bool edge = overview
&& (std::abs(hoverX - xForAddress(visibleRange.start)) < 7
|| std::abs(hoverX - xForAddress(visibleRange.end)) < 7);
setCursor(edge ? Qt::SizeHorCursor : overview ? Qt::OpenHandCursor : Qt::CrossCursor);
} else {
cancelTooltip();
}
update();
}
void DataAnalysisView::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton || drag == None) {
return;
}
updateDrag(event->pos());
if (drag == Select) {
if ((event->pos() - pressPosition).manhattanLength() < QApplication::startDragDistance()) {
emit selectionRequested(0, 0);
emit seekRequested(nearestMarker(event->pos().x()));
} else {
emit selectionRequested(dragSelection.start, dragSelection.end);
}
}
drag = None;
dragSelection = {};
update();
}
void DataAnalysisView::mouseDoubleClickEvent(QMouseEvent *event)
{
cancelTooltip();
if (event->button() == Qt::LeftButton && axisRange().valid()) {
drag = None;
if (overview) {
emit rangeRequested(domain.start, domain.end);
} else {
zoom(0.5, addressAt(event->pos().x()));
}
}
}
void DataAnalysisView::wheelEvent(QWheelEvent *event)
{
cancelTooltip();
const QPoint delta = event->angleDelta();
const int movement = delta.y() ? delta.y() : delta.x();
if (!movement) {
event->ignore();
return;
}
pan(movement > 0 ? -1 : 1);
event->accept();
}
void DataAnalysisView::keyPressEvent(QKeyEvent *event)
{
cancelTooltip();
const RVA center = visibleRange.start + visibleRange.size() / 2;
switch (event->key()) {
case Qt::Key_Plus:
case Qt::Key_Equal:
zoom(0.5, center);
break;
case Qt::Key_Minus:
zoom(2, center);
break;
case Qt::Key_Left:
pan(-1);
break;
case Qt::Key_Right:
pan(1);
break;
case Qt::Key_Home:
emit rangeRequested(domain.start, domain.end);
break;
case Qt::Key_Escape:
drag = None;
emit selectionRequested(0, 0);
update();
break;
default:
QWidget::keyPressEvent(event);
return;
}
event->accept();
}
void DataAnalysisView::leaveEvent(QEvent *)
{
cancelTooltip();
hoverX = -1;
unsetCursor();
QToolTip::hideText();
update();
}
void DataAnalysisView::cancelTooltip()
{
tooltipTimer.stop();
QToolTip::hideText();
}
void DataAnalysisView::hideEvent(QHideEvent *event)
{
cancelTooltip();
QWidget::hideEvent(event);
}
RVA DataAnalysisView::nearestMarker(qreal x) const
{
qreal distance = 5;
RVA result = addressAt(x);
for (const auto &marker : markers) {
if (markerEnabled[marker.type] && axisRange().contains(marker.range.start)) {
const qreal candidate = std::abs(x - xForAddress(marker.range.start));
if (candidate < distance) {
distance = candidate;
result = marker.range.start;
}
}
}
return result;
}
void DataAnalysisView::showTooltip()
{
if (!isVisible() || drag != None || !plotRect().contains(hoverPosition)) {
return;
}
const RVA address = addressAt(hoverPosition.x());
if (address == RVA_INVALID) {
return;
}
QString tip = RAddressString(address);
for (const auto &sample : samples) {
if (!sample.range.contains(address)) {
continue;
}
tip += tr("\nBucket: %1 – %2 (%3 bytes)\nRead %4 of %5 mapped bytes")
.arg(RAddressString(sample.range.start), RAddressString(sample.range.end - 1))
.arg(sample.range.size())
.arg(sample.bytes)
.arg(sample.covered);
if (sample.bytes) {
for (int metric : metrics) {
tip += QString("\n%1: %2")
.arg(metricName(metric), metricValue(metric, sample.values[metric]));
}
} else {
tip += tr("\nNo readable data");
}
break;
}
int found = 0;
for (const auto &marker : markers) {
if (markerEnabled[marker.type]
&& (marker.range.contains(address)
|| std::abs(xForAddress(marker.range.start) - hoverPosition.x()) < 5)) {
tip += QString("\n%1: %2 @ %3")
.arg(
markerName(marker.type),
marker.name.left(160),
RAddressString(marker.range.start));
if (++found == 12) {
break;
}
}
}
QToolTip::showText(mapToGlobal(hoverPosition), tip.toHtmlEscaped().replace('\n', "<br>"), this);
}
void DataAnalysisView::contextMenuEvent(QContextMenuEvent *event)
{
cancelTooltip();
if (axisRange().valid()) {
emit contextRequested(nearestMarker(event->pos().x()), event->globalPos());
event->accept();
}
}
+88
View File
@@ -0,0 +1,88 @@
#ifndef DATAANALYSISVIEW_H
#define DATAANALYSISVIEW_H
#include "common/DataAnalysis.h"
#include <QColor>
#include <QTimer>
#include <QWidget>
class DataAnalysisView : public QWidget
{
Q_OBJECT
public:
enum MarkerType { Section, Segment, Map, Flag, Comment, MarkerCount };
enum Style { Lines, Areas, Bars };
struct Marker
{
DataAnalysis::Range range;
QString name;
MarkerType type;
};
explicit DataAnalysisView(bool overview, QWidget *parent = nullptr);
static QString metricName(int metric);
static QString metricDescription(int metric);
static QString metricValue(int metric, double value);
static QString markerName(MarkerType type);
DataAnalysis::Range domain;
DataAnalysis::Range visibleRange;
DataAnalysis::Range selection;
QVector<DataAnalysis::Sample> samples;
QVector<Marker> markers;
QVector<int> metrics = {DataAnalysis::Entropy, DataAnalysis::Printable, DataAnalysis::Zero};
std::array<QColor, DataAnalysis::Count> colors;
std::array<bool, MarkerCount> markerEnabled = {true, false, false, true, true};
RVA seekAddress = RVA_INVALID;
Style style = Areas;
bool separateTracks = false;
bool loading = false;
QRectF plotRect() const;
RVA addressAt(qreal x) const;
qreal xForAddress(RVA address) const;
void zoom(double factor, RVA anchor);
void pan(int direction);
signals:
void seekRequested(RVA address);
void selectionRequested(RVA start, RVA end); // Half-open.
void rangeRequested(RVA start, RVA end); // Half-open.
void contextRequested(RVA address, QPoint globalPosition);
protected:
bool event(QEvent *) override;
void paintEvent(QPaintEvent *) override;
void mousePressEvent(QMouseEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
void mouseReleaseEvent(QMouseEvent *) override;
void mouseDoubleClickEvent(QMouseEvent *) override;
void wheelEvent(QWheelEvent *) override;
void keyPressEvent(QKeyEvent *) override;
void leaveEvent(QEvent *) override;
void hideEvent(QHideEvent *) override;
void contextMenuEvent(QContextMenuEvent *) override;
private:
bool overview;
enum Drag { None, Select, Move, LeftEdge, RightEdge };
Drag drag = None;
QPoint pressPosition;
RVA pressAddress = 0;
DataAnalysis::Range pressRange;
DataAnalysis::Range dragSelection;
int hoverX = -1;
QTimer tooltipTimer;
QPoint hoverPosition;
DataAnalysis::Range axisRange() const;
RVA nearestMarker(qreal x) const;
void pinchZoom(qreal scaleFactor, qreal x);
void updateDrag(QPoint position);
void showTooltip();
void cancelTooltip();
};
#endif
+966
View File
@@ -0,0 +1,966 @@
#include "DataAnalysisWidget.h"
#include "common/IaitoSeekable.h"
#include "core/MainWindow.h"
#include "dialogs/CommentsDialog.h"
#include "dialogs/FlagDialog.h"
#include "ui_DataAnalysisWidget.h"
#include <QActionGroup>
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QColorDialog>
#include <QDialogButtonBox>
#include <QElapsedTimer>
#include <QFileDialog>
#include <QFormLayout>
#include <QJsonArray>
#include <QJsonObject>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QSaveFile>
#include <QScrollArea>
#include <QSet>
#include <QTextStream>
#include <QToolButton>
#include <algorithm>
using namespace DataAnalysis;
namespace {
RVA number(const QJsonObject &object, const char *key)
{
return object[QLatin1String(key)].toVariant().toULongLong();
}
QByteArray readData(RVA address, int length)
{
auto core = Core()->core();
QByteArray bytes(length, '\0');
int count = 0;
while (count < length) {
// nread distinguishes short/failed reads from actual 0xff bytes. A read
// can end at a submap boundary even when the following map is readable.
const int n = r_io_nread_at(
core->io,
address + count,
reinterpret_cast<ut8 *>(bytes.data()) + count,
length - count);
if (n <= 0) {
break;
}
count += n;
}
bytes.resize(count);
return bytes;
}
} // namespace
DataAnalysisWidget::DataAnalysisWidget(MainWindow *main)
: AddressableDockWidget(main)
, ui(new Ui::DataAnalysisWidget)
{
setObjectName("DataAnalysisWidget");
updateWindowTitle();
auto *contents = new QWidget(this);
ui->setupUi(contents);
ui->legend->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
setWidget(contents);
plot = new DataAnalysisView(false, contents);
plot->setObjectName("analysisPlot");
overview = new DataAnalysisView(true, contents);
overview->setObjectName("analysisOverview");
overview->setFixedHeight(80);
auto *scroll = new QScrollArea(contents);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setWidget(plot);
ui->plotLayout->addWidget(scroll, 1);
ui->plotLayout->addWidget(overview);
ui->verticalLayout->setStretch(3, 1);
setupActions();
updateColors();
refreshDeferrer = createRefreshDeferrer([this]() { reloadTimer.start(); });
reloadTimer.setSingleShot(true);
reloadTimer.setInterval(100);
rangeTimer.setSingleShot(true);
rangeTimer.setInterval(80);
sampleTimer.setInterval(0);
connect(&reloadTimer, &QTimer::timeout, this, &DataAnalysisWidget::reload);
connect(&rangeTimer, &QTimer::timeout, this, [this]() {
if (!samplingOverview) {
beginSampling(false);
}
});
connect(&sampleTimer, &QTimer::timeout, this, &DataAnalysisWidget::sampleBatch);
connect(plot, &DataAnalysisView::seekRequested, seekable, &IaitoSeekable::seek);
connect(plot, &DataAnalysisView::selectionRequested, this, [this](RVA start, RVA end) {
selectRange({start, end}, true);
});
for (auto *view : {plot, overview}) {
connect(view, &DataAnalysisView::rangeRequested, this, [this](RVA start, RVA end) {
setRange({start, end});
});
connect(view, &DataAnalysisView::contextRequested, this, &DataAnalysisWidget::showContextMenu);
}
connect(seekable, &IaitoSeekable::seekableSeekChanged, this, &DataAnalysisWidget::onSeekChanged);
connect(Core(), &IaitoCore::addressRangeSelectionChanged, this, [this](RVA start, RVA end) {
if (!publishingSelection && seekable->isSynchronized()
&& Config()->getAddressRangeSelectionSyncEnabled()) {
selectRange(
start != RVA_INVALID && end != RVA_INVALID && end >= start ? Range{start, end + 1}
: Range{},
false);
}
});
connect(Core(), &IaitoCore::refreshAll, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::codeRebased, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::ioModeChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::ioCacheChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::flagsChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::commentsChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::refreshCodeViews, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::instructionChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::debugTaskStateChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Config(), &Configuration::colorsUpdated, this, &DataAnalysisWidget::updateColors);
connect(this, &QDockWidget::visibilityChanged, this, [this](bool visible) {
if (!visible) {
metadataDirty |= sampleTimer.isActive() || rangeTimer.isActive();
sampleTimer.stop();
rangeTimer.stop();
reloadTimer.stop();
} else if (metadataDirty) {
scheduleReload();
}
});
scheduleReload();
}
DataAnalysisWidget::~DataAnalysisWidget() = default;
QWidget *DataAnalysisWidget::widgetToFocusOnRaise()
{
return plot;
}
void DataAnalysisWidget::setupActions()
{
auto menuButton = [this](const QString &label, QMenu *menu) {
auto *button = new QToolButton(ui->toolbar);
button->setText(label);
button->setPopupMode(QToolButton::InstantPopup);
button->setMenu(menu);
ui->toolbar->addWidget(button);
};
auto *ranges = new QMenu(this);
ranges->addAction(tr("All data"), this, [this]() { setRange(plot->domain); });
ranges->addAction(tr("Current section"), this, [this]() {
fitMarker(DataAnalysisView::Section);
});
ranges->addAction(tr("Current segment"), this, [this]() {
fitMarker(DataAnalysisView::Segment);
});
ranges->addAction(tr("Current map"), this, [this]() { fitMarker(DataAnalysisView::Map); });
ranges->addAction(tr("Current block"), this, [this]() {
const int size = Core()->core()->blocksize;
setRange(sizedRange(seekable->getOffset(), qMax(1, size)));
});
selectionAction
= ranges->addAction(tr("Fit selection"), this, &DataAnalysisWidget::fitSelection);
selectionAction->setEnabled(false);
ranges->addSeparator();
ranges->addAction(tr("Edit range…"), this, &DataAnalysisWidget::editRange);
menuButton(tr("Range"), ranges);
auto *algorithms = new QMenu(this);
for (int i = 0; i < Count; ++i) {
auto *action = algorithms->addAction(DataAnalysisView::metricName(i));
action->setCheckable(true);
action->setChecked(plot->metrics.contains(i));
metricActions[i] = action;
action->setToolTip(DataAnalysisView::metricDescription(i));
auto *entry = ui->legend->addAction(DataAnalysisView::metricName(i));
entry->setToolTip(
DataAnalysisView::metricDescription(i) + tr("\nClick to configure this algorithm."));
legendActions[i] = entry;
connect(entry, &QAction::triggered, this, [this, i]() { configureMetric(i); });
connect(action, &QAction::toggled, this, &DataAnalysisWidget::updateMetrics);
}
menuButton(tr("Algorithms"), algorithms);
ui->toolbar
->addAction(
tr("+"),
this,
[this]() { plot->zoom(0.5, plot->visibleRange.start + plot->visibleRange.size() / 2); })
->setToolTip(tr("Zoom in (+)"));
ui->toolbar
->addAction(
tr("−"),
this,
[this]() { plot->zoom(2, plot->visibleRange.start + plot->visibleRange.size() / 2); })
->setToolTip(tr("Zoom out (-)"));
ui->toolbar->addAction(tr("Fit"), this, [this]() { setRange(plot->domain); })
->setToolTip(tr("Fit all data (Home)"));
followAction = ui->toolbar->addAction(tr("Follow"));
followAction->setCheckable(true);
followAction->setChecked(true);
followAction->setToolTip(tr("Pan to keep the current disassembly or hexdump address visible"));
connect(followAction, &QAction::toggled, this, [this](bool checked) {
if (checked) {
onSeekChanged(seekable->getOffset());
}
});
optionsMenu = new QMenu(this);
tracksAction = optionsMenu->addAction(tr("Separate algorithm tracks"));
tracksAction->setCheckable(true);
connect(tracksAction, &QAction::toggled, this, [this](bool checked) {
plot->separateTracks = checked;
plot->setMinimumHeight(checked ? qMax(180, int(plot->metrics.size()) * 90) : 180);
plot->update();
});
auto *styles = optionsMenu->addMenu(tr("Drawing style"));
auto *styleGroup = new QActionGroup(styles);
const QStringList styleNames = {tr("Lines"), tr("Filled lines"), tr("Vertical bars")};
for (int i = 0; i < styleNames.size(); ++i) {
auto *action = styles->addAction(styleNames[i]);
styleGroup->addAction(action);
action->setCheckable(true);
action->setData(i);
action->setChecked(i == plot->style);
connect(action, &QAction::triggered, this, [this, i]() {
plot->style = static_cast<DataAnalysisView::Style>(i);
plot->update();
});
}
styles->setObjectName("analysisStyles");
auto *colors = optionsMenu->addMenu(tr("Colors"));
auto *colorGroup = new QActionGroup(colors);
const QStringList modes
= {tr("Greyscale"), tr("Rainbow"), tr("Theme single color"), tr("Theme palette")};
for (int i = 0; i < modes.size(); ++i) {
auto *action = colors->addAction(modes[i]);
colorGroup->addAction(action);
action->setCheckable(true);
action->setData(i);
action->setChecked(i == colorMode);
connect(action, &QAction::triggered, this, [this, i]() {
colorMode = i;
updateColors();
});
}
colors->setObjectName("analysisColors");
colors->addSeparator();
auto *custom = colors->addMenu(tr("Customize algorithm colors"));
for (int i = 0; i < Count; ++i) {
custom->addAction(DataAnalysisView::metricName(i), this, [this, i]() {
const QColor color
= QColorDialog::getColor(plot->colors[i], this, DataAnalysisView::metricName(i));
if (color.isValid()) {
customColors[i] = color;
updateColors();
}
});
}
custom->addSeparator();
custom->addAction(tr("Reset custom colors"), this, [this]() {
customColors.fill(QColor());
updateColors();
});
auto *markers = optionsMenu->addMenu(tr("Markers"));
markers->setObjectName("analysisMarkers");
for (int i = 0; i < DataAnalysisView::MarkerCount; ++i) {
auto *action = markers->addAction(
DataAnalysisView::markerName(static_cast<DataAnalysisView::MarkerType>(i)));
action->setCheckable(true);
action->setData(i);
action->setChecked(plot->markerEnabled[i]);
connect(action, &QAction::toggled, this, [this, i](bool checked) {
plot->markerEnabled[i] = overview->markerEnabled[i] = checked;
plot->update();
overview->update();
});
}
auto *samples = optionsMenu->addMenu(tr("Resolution (buckets)"));
samples->setObjectName("analysisResolution");
auto *sampleGroup = new QActionGroup(samples);
for (int n : {32, 64, 128, 256, 512, 1024, 2048, 4096}) {
auto *action = samples->addAction(QString::number(n));
sampleGroup->addAction(action);
action->setCheckable(true);
action->setData(n);
action->setChecked(n == resolution);
connect(action, &QAction::triggered, this, [this, n]() {
resolution = n;
beginSampling(samplingOverview);
});
}
optionsMenu->addSeparator();
optionsMenu->addAction(&syncAction);
optionsMenu->addAction(tr("Find marker…"), this, &DataAnalysisWidget::findMarker);
optionsMenu->addAction(tr("Refresh"), this, &DataAnalysisWidget::scheduleReload);
exportAction = optionsMenu->addAction(
tr("Export visible samples as CSV…"), this, &DataAnalysisWidget::exportCsv);
exportAction->setEnabled(false);
menuButton(tr("Options"), optionsMenu);
ui->legend->setToolTip(
tr("Click a legend entry to configure its color and visibility."
" Overlaid values use a 0–100% scale; entropy is divided by 8."
" Separate tracks use native units."));
updateMetrics();
}
void DataAnalysisWidget::updateMetrics()
{
plot->metrics.clear();
for (int i = 0; i < Count; ++i) {
legendActions[i]->setVisible(metricActions[i]->isChecked());
if (metricActions[i]->isChecked()) {
plot->metrics.append(i);
}
}
overview->metrics = plot->metrics;
plot->setMinimumHeight(plot->separateTracks ? qMax(180, int(plot->metrics.size()) * 90) : 180);
plot->update();
}
void DataAnalysisWidget::updateColors()
{
const char *keys[]
= {"gui.navbar.code",
"gui.navbar.str",
"gui.navbar.data",
"num",
"flag",
"comment",
"flow",
"func_var",
"reg",
"invalid",
"other"};
QColor base = Config()->getColor("gui.navbar.code");
if (!base.isValid()) {
base = QColor(120, 150, 230);
}
QSet<QRgb> used;
for (int i = 0; i < Count; ++i) {
QColor color;
switch (colorMode) {
case 0:
color = QColor::fromHsv(0, 0, 85 + i * 16);
break;
case 1:
color = QColor::fromHsv((270 + i * 137) % 360, 165, 220);
break;
case 2:
color = QColor::fromHsv(qMax(0, base.hue()), 70 + i * 16, 235 - i * 9);
break;
default:
color = Config()->getColor(keys[i]);
if (!color.isValid() || used.contains(color.rgb())
|| color == palette().color(QPalette::Base)) {
color = QColor::fromHsv((270 + i * 137) % 360, 155, 210);
}
break;
}
if (customColors[i].isValid()) {
color = customColors[i];
}
used.insert(color.rgb());
plot->colors[i] = overview->colors[i] = color;
QPixmap swatch(12, 12);
swatch.fill(color);
metricActions[i]->setIcon(QIcon(swatch));
legendActions[i]->setIcon(QIcon(swatch));
}
plot->update();
overview->update();
}
void DataAnalysisWidget::configureMetric(int metric)
{
QDialog dialog(this);
dialog.setObjectName("analysisMetricSettings");
dialog.setWindowTitle(tr("%1 settings").arg(DataAnalysisView::metricName(metric)));
QFormLayout layout(&dialog);
QLabel description(DataAnalysisView::metricDescription(metric));
description.setWordWrap(true);
description.setMaximumWidth(420);
QCheckBox visible(tr("Show this algorithm"));
visible.setObjectName("metricVisible");
visible.setChecked(metricActions[metric]->isChecked());
QColor chosen = customColors[metric];
QPushButton colorButton(tr("Change color…"));
colorButton.setIcon(legendActions[metric]->icon());
QPushButton resetButton(tr("Use palette color"));
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout.addRow(&description);
layout.addRow(tr("Color:"), &colorButton);
layout.addRow(&resetButton);
layout.addRow(&visible);
layout.addRow(&buttons);
connect(&colorButton, &QPushButton::clicked, &dialog, [&]() {
const QColor color = QColorDialog::getColor(
chosen.isValid() ? chosen : plot->colors[metric],
&dialog,
DataAnalysisView::metricName(metric));
if (color.isValid()) {
chosen = color;
QPixmap swatch(12, 12);
swatch.fill(color);
colorButton.setIcon(QIcon(swatch));
colorButton.setText(color.name());
}
});
connect(&resetButton, &QPushButton::clicked, &dialog, [&]() {
chosen = QColor();
colorButton.setIcon(QIcon());
colorButton.setText(tr("Palette default"));
});
connect(&buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (dialog.exec() == QDialog::Accepted) {
customColors[metric] = chosen;
metricActions[metric]->setChecked(visible.isChecked());
updateColors();
}
}
void DataAnalysisWidget::scheduleReload()
{
metadataDirty = true;
if (refreshDeferrer->attemptRefresh(nullptr)) {
sampleTimer.stop();
reloadTimer.start();
}
}
void DataAnalysisWidget::loadMetadata()
{
using View = DataAnalysisView;
virtualAddresses = Core()->getConfigb("io.va");
readable.clear();
plot->markers.clear();
auto addMarker = [this](Range range, const QString &name, View::MarkerType type) {
if (range.valid()) {
plot->markers.append({range, name, type});
}
};
for (const auto &value : Core()->cmdj("omj").array()) {
const QJsonObject map = value.toObject();
const RVA start = number(map, "from"), last = number(map, "to");
const Range range = last >= start ? Range{start, last == RVA_MAX ? RVA_MAX : last + 1}
: Range{};
if (virtualAddresses) {
addMarker(range, map["name"].toString(), View::Map);
if (range.valid() && map["perm"].toString().contains('r')) {
readable.append(range);
}
}
}
if (!virtualAddresses) {
auto core = Core()->core();
const RVA size = r_io_size(core->io);
if (size != RVA_MAX && size) {
readable.append({0, size});
}
}
readable = mergeRanges(readable);
const Range domain = readable.isEmpty() ? Range{}
: Range{readable.first().start, readable.last().end};
const bool changed = domain != plot->domain;
plot->domain = overview->domain = domain;
if (changed || !plot->visibleRange.valid()) {
plot->visibleRange = overview->visibleRange = domain;
}
for (const auto &entry :
{qMakePair(QString("iSj"), View::Section), qMakePair(QString("iSSj"), View::Segment)}) {
const QJsonDocument document = Core()->cmdj(entry.first);
const QJsonArray objects
= document.isArray()
? document.array()
: document.object()[entry.second == View::Section ? "sections" : "segments"]
.toArray();
for (const auto &value : objects) {
const QJsonObject object = value.toObject();
const RVA start = number(object, virtualAddresses ? "vaddr" : "paddr");
const RVA size = number(object, virtualAddresses ? "vsize" : "size");
addMarker(sizedRange(start, size), object["name"].toString(), entry.second);
}
}
// @F temporarily selects all flag spaces without changing the user's space.
for (const auto &value : Core()->cmdj("fj @F:*").array()) {
const QJsonObject flag = value.toObject();
const RVA address = number(flag, flag.contains("offset") ? "offset" : "addr");
addMarker(
sizedRange(address, qMax<RVA>(1, number(flag, "size"))),
flag["name"].toString(),
View::Flag);
}
for (const auto &comment : Core()->getAllComments("CCu")) {
addMarker(sizedRange(comment.offset, 1), comment.name, View::Comment);
}
std::stable_sort(
plot->markers.begin(),
plot->markers.end(),
[](const View::Marker &a, const View::Marker &b) { return a.range.start < b.range.start; });
overview->markers = plot->markers;
metadataDirty = false;
}
void DataAnalysisWidget::reload()
{
if (!refreshDeferrer->attemptRefresh(nullptr) || Core()->isDebugTaskInProgress()) {
return;
}
sampleTimer.stop();
rangeTimer.stop();
loadMetadata();
plot->seekAddress = overview->seekAddress = seekable->getOffset();
if (requestedRange.valid()) {
const Range requested = intersect(requestedRange, plot->domain);
if (requested.valid()) {
plot->visibleRange = overview->visibleRange = requested;
}
requestedRange = {};
}
if (seekable->isSynchronized() && Config()->getAddressRangeSelectionSyncEnabled()
&& Core()->hasAddressRangeSelection()) {
selectRange(
{Core()->getAddressRangeSelectionStart(), Core()->getAddressRangeSelectionEnd() + 1},
false);
}
plot->samples.clear();
overview->samples.clear();
beginSampling(true);
}
void DataAnalysisWidget::beginSampling(bool mini)
{
rangeTimer.stop();
sampleTimer.stop();
if (!isVisibleToUser()) {
metadataDirty = true;
refreshDeferrer->attemptRefresh(nullptr);
return;
}
samplingOverview = mini;
pendingRange = mini ? plot->domain : plot->visibleRange;
pending.clear();
nextBucket = 0;
plot->loading = pendingRange.valid();
exportAction->setEnabled(false);
if (pendingRange.valid()) {
const int count = int(qMin<RVA>(mini ? 256 : resolution, pendingRange.size()));
pending.resize(count);
sampleTimer.start();
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::sampleBatch()
{
if (!isVisibleToUser() || Core()->isDebugTaskInProgress()) {
sampleTimer.stop();
metadataDirty = true;
refreshDeferrer->attemptRefresh(nullptr);
return;
}
QElapsedTimer timer;
timer.start();
// Yield to navigation and painting every few milliseconds. A range change
// cancels the old job; no stale worker result can overwrite the new range.
do {
pending[nextBucket]
= sample(bucket(pendingRange, nextBucket, pending.size()), readable, readData);
++nextBucket;
} while (nextBucket < pending.size() && timer.elapsed() < 6);
if (nextBucket < pending.size()) {
ui->statusLabel->setText(tr("Analyzing %1… %2%")
.arg(samplingOverview ? tr("overview") : tr("visible range"))
.arg(nextBucket * 100 / pending.size()));
return;
}
sampleTimer.stop();
if (samplingOverview) {
overview->samples = pending;
overview->update();
beginSampling(false);
} else {
plot->samples = pending;
plot->loading = false;
exportAction->setEnabled(!plot->samples.isEmpty());
updateLabels();
plot->update();
}
}
void DataAnalysisWidget::setRange(Range range)
{
range = intersect(range, plot->domain);
if (!range.valid() || range == plot->visibleRange) {
return;
}
plot->visibleRange = overview->visibleRange = range;
plot->samples.clear();
plot->loading = true;
exportAction->setEnabled(false);
if (!samplingOverview) {
sampleTimer.stop();
}
rangeTimer.start();
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::showRange(RVA start, RVA end)
{
if (start == RVA_INVALID || end == RVA_INVALID || end < start) {
return;
}
requestedRange = {start, end + 1};
raiseMemoryWidget();
seekable->seek(start);
if (metadataDirty) {
scheduleReload();
} else {
setRange(requestedRange);
requestedRange = {};
}
}
void DataAnalysisWidget::onSeekChanged(RVA address)
{
plot->seekAddress = overview->seekAddress = address;
if (followAction->isChecked() && plot->domain.contains(address)
&& !plot->visibleRange.contains(address)) {
setRange(centered(plot->domain, address, plot->visibleRange.size()));
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::selectRange(Range range, bool publish)
{
plot->selection = overview->selection = range;
selectionAction->setEnabled(range.valid());
if (publish && seekable->isSynchronized() && Config()->getAddressRangeSelectionSyncEnabled()) {
publishingSelection = true;
if (range.valid()) {
Core()->setAddressRangeSelection(range.start, range.end - 1);
} else {
Core()->clearAddressRangeSelection();
}
publishingSelection = false;
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::updateLabels()
{
const Range range = plot->visibleRange;
if (!range.valid()) {
ui->rangeLabel->setText(tr("No readable data in the current address space"));
ui->statusLabel->setText(tr("Open a file or refresh after mapping data."));
return;
}
QString text = tr("%1: %2 – %3 · %4 bytes · Cursor %5")
.arg(
virtualAddresses ? tr("Addresses") : tr("File offsets"),
RAddressString(range.start),
RAddressString(range.end - 1))
.arg(range.size())
.arg(RAddressString(seekable->getOffset()));
if (plot->selection.valid()) {
text += tr("\nSelection: %1 – %2 · %3 bytes")
.arg(
RAddressString(plot->selection.start),
RAddressString(plot->selection.end - 1))
.arg(plot->selection.size());
}
ui->rangeLabel->setText(text);
if (plot->loading) {
ui->statusLabel->setText(tr("Analyzing…"));
} else {
RVA bytes = 0, covered = 0;
for (const auto &sample : plot->samples) {
bytes += sample.bytes;
covered += sample.covered;
}
if (!bytes) {
ui->statusLabel->setText(tr("No readable bytes in the visible range."));
return;
}
ui->statusLabel->setText(
tr("%1 buckets · %2 / %3 mapped bytes read (%4) · Overview: entropy")
.arg(plot->samples.size())
.arg(bytes)
.arg(covered)
.arg(bytes == covered ? tr("complete") : tr("sampled or unreadable")));
ui->statusLabel->setToolTip(
tr("Each bucket reads up to 4096 bytes from centered windows in readable spans."
" Zoom in for complete coverage. Unmapped bytes are excluded."
" Shannon entropy: 0–8 bits/byte. Overlays use normalized percentages."));
}
}
void DataAnalysisWidget::fitSelection()
{
if (plot->selection.valid()) {
setRange(plot->selection);
}
}
void DataAnalysisWidget::fitMarker(DataAnalysisView::MarkerType type)
{
Range best;
for (const auto &marker : plot->markers) {
if (marker.type == type && marker.range.contains(seekable->getOffset())
&& (!best.valid() || marker.range.size() < best.size())) {
best = marker.range;
}
}
if (best.valid()) {
setRange(best);
} else {
ui->statusLabel->setText(
tr("No %1 contains the current address.").arg(DataAnalysisView::markerName(type)));
}
}
void DataAnalysisWidget::editRange()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Data analysis range"));
QFormLayout layout(&dialog);
QLineEdit start(RAddressString(plot->visibleRange.start));
QLineEdit end(RAddressString(plot->visibleRange.valid() ? plot->visibleRange.end - 1 : 0));
QLabel hint(tr("Enter addresses or radare2 expressions. Both endpoints are inclusive."));
hint.setWordWrap(true);
QLabel error;
error.setWordWrap(true);
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout.addRow(&hint);
layout.addRow(tr("Start:"), &start);
layout.addRow(tr("End (inclusive):"), &end);
layout.addRow(&error);
layout.addRow(&buttons);
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
connect(&buttons, &QDialogButtonBox::accepted, &dialog, [&]() {
RVA first, last;
if (!Core()->tryMath(start.text(), first) || !Core()->tryMath(end.text(), last)
|| first == RVA_INVALID || last == RVA_INVALID || first > last) {
error.setText(tr("Enter a valid start and an end greater than or equal to it."));
return;
}
const Range requested{first, last + 1};
if (intersect(requested, plot->domain) != requested) {
error.setText(
tr("The range must be within %1 – %2.")
.arg(RAddressString(plot->domain.start), RAddressString(plot->domain.end - 1)));
return;
}
setRange(requested);
dialog.accept();
});
dialog.exec();
}
void DataAnalysisWidget::findMarker()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Find section, map, flag or comment"));
dialog.resize(620, 400);
QVBoxLayout layout(&dialog);
QLineEdit filter;
filter.setPlaceholderText(tr("Filter by name, comment, type or hexadecimal address…"));
QListWidget list;
QLabel hint(
tr("Activate a result to seek. Section, segment and map results also fit the range."
" Showing up to 500 matches."));
hint.setWordWrap(true);
QDialogButtonBox buttons(QDialogButtonBox::Close);
layout.addWidget(&filter);
layout.addWidget(&list);
layout.addWidget(&hint);
layout.addWidget(&buttons);
// Keep result indices stable while the modal loop receives metadata updates.
const auto markers = plot->markers;
auto fill = [&]() {
list.clear();
for (int i = 0; i < markers.size(); ++i) {
const auto &marker = markers[i];
const QString label = QString("%1 · %2 · %3")
.arg(
RAddressString(marker.range.start),
DataAnalysisView::markerName(marker.type),
marker.name);
if (label.contains(filter.text(), Qt::CaseInsensitive)) {
auto *item = new QListWidgetItem(label, &list);
item->setData(Qt::UserRole, i);
if (list.count() >= 500) {
break;
}
}
}
};
connect(&filter, &QLineEdit::textChanged, &dialog, fill);
connect(&list, &QListWidget::itemActivated, &dialog, [&](QListWidgetItem *item) {
const int index = item->data(Qt::UserRole).toInt();
if (index < 0 || index >= markers.size()) {
return;
}
const auto marker = markers[index];
seekable->seek(marker.range.start);
if (marker.type <= DataAnalysisView::Map) {
setRange(marker.range);
} else if (!plot->visibleRange.contains(marker.range.start)) {
setRange(centered(plot->domain, marker.range.start, plot->visibleRange.size()));
}
dialog.accept();
});
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
fill();
dialog.exec();
}
void DataAnalysisWidget::showContextMenu(RVA address, QPoint position)
{
QMenu menu(this);
menu.addAction(RAddressString(address))->setEnabled(false);
menu.addAction(tr("Seek here"), this, [this, address]() { seekable->seek(address); });
if (mainWindow) {
auto *showIn = mainWindow->createShowInMenu(&menu, address);
showIn->setTitle(tr("Show in"));
menu.addMenu(showIn);
}
menu.addAction(tr("Copy address"), this, [address]() {
QApplication::clipboard()->setText(RAddressString(address));
});
menu.addAction(tr("Add or edit comment…"), this, [this, address]() {
CommentsDialog::addOrEditComment(address, this);
});
const Range selection = plot->selection;
const RVA flagSize = selection.contains(address) ? selection.end - address : 1;
menu.addAction(tr("Add or edit flag…"), this, [this, address, flagSize]() {
FlagDialog dialog(address, flagSize, this);
dialog.exec();
});
menu.addSeparator();
menu.addAction(selectionAction);
menu.addAction(tr("Clear selection"), this, [this]() { selectRange({}, true); });
menu.addAction(tr("Edit range…"), this, &DataAnalysisWidget::editRange);
menu.addAction(tr("Find marker…"), this, &DataAnalysisWidget::findMarker);
menu.addAction(tr("Fit all data"), this, [this]() { setRange(plot->domain); });
menu.addSeparator();
menu.addAction(&syncAction);
menu.exec(position);
}
void DataAnalysisWidget::exportCsv()
{
const auto samples = plot->samples;
const auto metrics = plot->metrics;
const QString path = QFileDialog::getSaveFileName(
this, tr("Export data analysis"), QString(), tr("CSV files (*.csv)"));
if (path.isEmpty()) {
return;
}
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, tr("Export failed"), file.errorString());
return;
}
QTextStream out(&file);
out << "start,end_exclusive,bytes_read,mapped_bytes";
for (int metric : metrics) {
QString name = DataAnalysisView::metricName(metric);
name.replace('"', "\"\"");
out << ",\"" << name << " (normalized 0-1)\"";
}
out << '\n';
for (const auto &sample : samples) {
out << RAddressString(sample.range.start) << ',' << RAddressString(sample.range.end) << ','
<< sample.bytes << ',' << sample.covered;
for (int metric : metrics) {
out << ',';
if (sample.bytes) {
out << QString::number(sample.values[metric], 'g', 12);
}
}
out << '\n';
}
out.flush();
if (!file.commit()) {
QMessageBox::warning(this, tr("Export failed"), file.errorString());
}
}
QVariantMap DataAnalysisWidget::serializeViewProprties()
{
auto properties = AddressableDockWidget::serializeViewProprties();
QVariantList metrics, colors, markers;
for (int i = 0; i < Count; ++i) {
if (metricActions[i]->isChecked()) {
metrics.append(i);
}
colors.append(customColors[i].isValid() ? customColors[i].name() : QString());
}
for (bool enabled : plot->markerEnabled) {
markers.append(enabled);
}
properties["metrics"] = metrics;
properties["colors"] = colors;
properties["markers"] = markers;
properties["style"] = int(plot->style);
properties["colorMode"] = colorMode;
properties["resolution"] = resolution;
properties["tracks"] = tracksAction->isChecked();
properties["follow"] = followAction->isChecked();
return properties;
}
void DataAnalysisWidget::deserializeViewProperties(const QVariantMap &properties)
{
AddressableDockWidget::deserializeViewProperties(properties);
const QVariantList metrics
= properties.value("metrics", QVariantList{Entropy, Printable, Zero}).toList();
const QVariantList colors = properties.value("colors").toList();
for (int i = 0; i < Count; ++i) {
metricActions[i]->setChecked(metrics.contains(i));
customColors[i] = i < colors.size() ? QColor(colors[i].toString()) : QColor();
}
auto restoreMenu = [this](const char *name, int selected) {
for (auto *action : optionsMenu->findChild<QMenu *>(name)->actions()) {
if (action->isCheckable() && action->data().toInt() == selected) {
action->trigger();
}
}
};
restoreMenu("analysisStyles", qBound(0, properties.value("style", 1).toInt(), 2));
restoreMenu("analysisColors", qBound(0, properties.value("colorMode", 3).toInt(), 3));
int count = properties.value("resolution", 1024).toInt();
if (!QList<int>{32, 64, 128, 256, 512, 1024, 2048, 4096}.contains(count)) {
count = 1024;
}
restoreMenu("analysisResolution", count);
const QVariantList markers
= properties.value("markers", QVariantList{true, false, false, true, true}).toList();
for (auto *action : optionsMenu->findChild<QMenu *>("analysisMarkers")->actions()) {
const int index = action->data().toInt();
action->setChecked(index < markers.size() && markers[index].toBool());
}
tracksAction->setChecked(properties.value("tracks", false).toBool());
followAction->setChecked(properties.value("follow", true).toBool());
updateColors();
}
+81
View File
@@ -0,0 +1,81 @@
#ifndef DATAANALYSISWIDGET_H
#define DATAANALYSISWIDGET_H
#include "AddressableDockWidget.h"
#include "DataAnalysisView.h"
#include <memory>
#include <QTimer>
namespace Ui {
class DataAnalysisWidget;
}
class QMenu;
class DataAnalysisWidget : public AddressableDockWidget
{
Q_OBJECT
public:
explicit DataAnalysisWidget(MainWindow *main);
~DataAnalysisWidget() override;
QVariantMap serializeViewProprties() override;
void deserializeViewProperties(const QVariantMap &properties) override;
// Reusable entry point for other widgets. Both endpoints are inclusive.
void showRange(RVA start, RVA end);
protected:
QString getWindowTitle() const override { return tr("Data Analysis"); }
QWidget *widgetToFocusOnRaise() override;
private:
std::unique_ptr<Ui::DataAnalysisWidget> ui;
DataAnalysisView *plot;
DataAnalysisView *overview;
RefreshDeferrer *refreshDeferrer;
QTimer reloadTimer;
QTimer sampleTimer;
QTimer rangeTimer;
QVector<DataAnalysis::Range> readable;
QVector<DataAnalysis::Sample> pending;
DataAnalysis::Range pendingRange;
DataAnalysis::Range requestedRange;
int nextBucket = 0;
int resolution = 1024;
int colorMode = 3;
bool samplingOverview = false;
bool virtualAddresses = true;
bool metadataDirty = true;
bool publishingSelection = false;
QAction *followAction;
QAction *tracksAction;
QAction *selectionAction;
QAction *exportAction;
QMenu *optionsMenu;
std::array<QAction *, DataAnalysis::Count> metricActions;
std::array<QAction *, DataAnalysis::Count> legendActions;
std::array<QColor, DataAnalysis::Count> customColors;
void setupActions();
void scheduleReload();
void reload();
void loadMetadata();
void beginSampling(bool mini);
void sampleBatch();
void setRange(DataAnalysis::Range range);
void updateLabels();
void updateColors();
void updateMetrics();
void configureMetric(int metric);
void onSeekChanged(RVA address);
void selectRange(DataAnalysis::Range range, bool publish);
void fitSelection();
void editRange();
void fitMarker(DataAnalysisView::MarkerType type);
void findMarker();
void showContextMenu(RVA address, QPoint position);
void exportCsv();
};
#endif
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DataAnalysisWidget</class>
<widget class="QWidget" name="DataAnalysisWidget">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing"><number>4</number></property>
<property name="leftMargin"><number>4</number></property>
<property name="topMargin"><number>4</number></property>
<property name="rightMargin"><number>4</number></property>
<property name="bottomMargin"><number>4</number></property>
<item><widget class="QToolBar" name="toolbar"><property name="movable"><bool>false</bool></property></widget></item>
<item><widget class="QLabel" name="rangeLabel">
<property name="text"><string>Open a file to analyze its data</string></property>
<property name="wordWrap"><bool>true</bool></property>
<property name="textInteractionFlags"><set>Qt::TextSelectableByMouse</set></property>
</widget></item>
<item><widget class="QToolBar" name="legend"><property name="movable"><bool>false</bool></property></widget></item>
<item><layout class="QVBoxLayout" name="plotLayout"/></item>
<item><widget class="QLabel" name="statusLabel">
<property name="wordWrap"><bool>true</bool></property>
<property name="text"><string>Click to seek · Drag to select · Wheel or pinch to zoom · Shift+wheel to pan</string></property>
<property name="textInteractionFlags"><set>Qt::TextSelectableByMouse</set></property>
</widget></item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+13
View File
@@ -167,10 +167,23 @@ DecompilerWidget::DecompilerWidget(MainWindow *main)
seekPrevAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
addAction(seekPrevAction);
connect(seekPrevAction, &QAction::triggered, seekable, &IaitoSeekable::seekPrev);
QAction *switchAction = new QAction(this);
ShortcutMgr()->bindAction("decompiler.switchToDisassembly", switchAction);
switchAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
addAction(switchAction);
connect(switchAction, &QAction::triggered, this, [this] {
mainWindow->showMemoryWidget(MemoryWidgetType::Disassembly);
});
}
DecompilerWidget::~DecompilerWidget() = default;
QWidget *DecompilerWidget::widgetToFocusOnRaise()
{
return ui->textEdit;
}
QString DecompilerWidget::getWidgetType()
{
return "DecompilerWidget";
+1
View File
@@ -183,6 +183,7 @@ private:
void updateCursorPosition();
QString getWindowTitle() const override;
QWidget *widgetToFocusOnRaise() override;
/**
* @brief Event filter that intercept the following events:
+222 -9
View File
@@ -11,6 +11,7 @@
#include "common/TempConfig.h"
#include "core/Iaito.h"
#include "core/MainWindow.h"
#include "widgets/ExceptionBar.h"
#include <QAction>
#include <QActionGroup>
@@ -117,6 +118,11 @@ DisassemblerGraphView::DisassemblerGraphView(
{
highlight_token = nullptr;
auto *layout = new QVBoxLayout(this);
exceptionBar = new ExceptionBar(this);
layout->addWidget(exceptionBar);
connect(exceptionBar, &ExceptionBar::seekRequested, this, [this](quint64 address) {
this->seekable->seek(address);
});
// Signals that require a refresh all
connect(Core(), &IaitoCore::refreshAll, this, &DisassemblerGraphView::refreshView);
connect(Core(), &IaitoCore::commentsChanged, this, &DisassemblerGraphView::refreshView);
@@ -153,6 +159,13 @@ DisassemblerGraphView::DisassemblerGraphView(
contextMenu->addAction(&actionExportGraph);
contextMenu->addMenu(layoutMenu);
addBasicBlockContentMenu();
showExceptionsAction = contextMenu->addAction(tr("Show exception regions"));
showExceptionsAction->setCheckable(true);
showExceptionsAction->setChecked(true);
showExceptionsAction->setToolTip(tr(
"Colored bubbles mark protected blocks. Dashed arrows show possible exception transfers."));
connect(showExceptionsAction, &QAction::toggled, this, &DisassemblerGraphView::refreshView);
exceptionMenu = blockMenu->addMenu(tr("Exception handlers"));
if (mainWindow) {
if (QAction *overviewAction = mainWindow->getOverviewAction()) {
contextMenu->addAction(overviewAction);
@@ -338,6 +351,12 @@ void DisassemblerGraphView::loadCurrentGraph()
disassembly_blocks.clear();
blocks.clear();
outgoingXRefsCache.clear();
exceptionEdges.clear();
exceptionShapes.clear();
exceptionRegions = fcn ? Core()->getExceptionRegions(currentFcnAddr) : QList<ExceptionRegion>();
exceptionBar->setRegions(exceptionRegions, seekable->getOffset());
setViewportMargins(0, exceptionRegions.isEmpty() ? 0 : exceptionBar->sizeHint().height(), 0, 0);
showExceptionsAction->setVisible(!exceptionRegions.isEmpty());
if (highlight_token) {
delete highlight_token;
@@ -426,6 +445,9 @@ void DisassemblerGraphView::loadCurrentGraph()
addBlock(gb);
}
if (showExceptionsAction->isChecked()) {
addExceptionBlocks();
}
cleanupEdges(blocks);
if (!func["blocks"].toArray().isEmpty()) {
@@ -433,6 +455,146 @@ void DisassemblerGraphView::loadCurrentGraph()
}
}
void DisassemblerGraphView::addExceptionBlocks()
{
for (const auto &region : exceptionRegions) {
if (!blockForAddress(region.handler)) {
// The handler may be in another function or not analyzed yet. Show
// a navigation node without creating or changing analysis blocks.
DisassemblyBlock db;
db.entry = region.handler;
db.size = 1;
db.externalHandler = true;
appendBlockContentLine(db, tr("Handler outside this function"), region.handler, 1);
appendBlockContentLine(db, tr("Double-click to open"), region.handler, 1);
disassembly_blocks[db.entry] = db;
GraphBlock block;
block.entry = db.entry;
addBlock(block);
}
}
for (auto &item : disassembly_blocks) {
auto &db = item.second;
QStringList tags;
for (const auto &region : exceptionRegions) {
if (!db.externalHandler && region.protects(db.entry, db.size)) {
tags << tr("TRY #%1").arg(region.index);
const auto *handler = blockForAddress(region.handler);
auto &edges = blocks.at(db.entry).edges;
if (handler && handler->entry != db.entry
&& std::none_of(edges.begin(), edges.end(), [handler](const GraphEdge &edge) {
return edge.target == handler->entry;
})) {
edges.emplace_back(handler->entry);
exceptionEdges[{db.entry, handler->entry}] = region.color();
}
}
if (region.handler >= db.entry && region.handler - db.entry < db.size) {
tags << tr("HANDLER #%1").arg(region.index);
}
}
if (!tags.isEmpty()) {
db.header_text = Text(
QStringLiteral("[%1] %2")
.arg(RAddressString(db.entry), tags.join(QStringLiteral(" · "))),
ConfigColor(ADDR),
Qt::transparent);
}
prepareGraphNode(blocks.at(db.entry));
}
}
void DisassemblerGraphView::graphLayoutChanged()
{
exceptionShapes.clear();
if (exceptionRegions.isEmpty() || !showExceptionsAction->isChecked()) {
return;
}
// Keep the bubbles inside image export and scrollable graph bounds.
for (auto &item : blocks) {
item.second.x += 12;
item.second.y += 12;
for (auto &edge : item.second.edges) {
edge.polyline.translate(12, 12);
}
}
width += 24;
height += 24;
QSet<QPair<quint64, quint64>> seen;
for (const auto &region : exceptionRegions) {
const auto range = qMakePair(region.from, region.to);
if (seen.contains(range)) {
continue;
}
seen.insert(range);
QRectF bounds;
for (const auto &item : blocks) {
const auto &block = item.second;
const auto &db = disassembly_blocks.at(block.entry);
if (db.externalHandler || !region.protects(db.entry, db.size)) {
continue;
}
bounds = bounds.united(QRectF(block.x, block.y, block.width, block.height));
}
if (bounds.isEmpty()) {
continue;
}
// One envelope covers every protected block, including disconnected
// blocks and the space between them. Membership comes from the entire
// half-open metadata range, not just its boundary instructions.
QPainterPath shape;
shape.addRoundedRect(bounds.adjusted(-8, -8, 8, 8), 12, 12);
for (const auto &item : blocks) {
const auto &block = item.second;
const auto &db = disassembly_blocks.at(block.entry);
if (db.externalHandler || !region.protects(db.entry, db.size)) {
QPainterPath excluded;
excluded.addRect(
QRectF(block.x - 2, block.y - 2, block.width + 4, block.height + 4));
shape = shape.subtracted(excluded);
}
}
exceptionShapes.append({region, shape});
}
// Keep inner regions readable over their enclosing regions.
std::stable_sort(
exceptionShapes.begin(),
exceptionShapes.end(),
[](const ExceptionShape &a, const ExceptionShape &b) {
const QRectF ar = a.path.boundingRect();
const QRectF br = b.path.boundingRect();
return ar.width() * ar.height() > br.width() * br.height();
});
}
void DisassemblerGraphView::drawBackground(QPainter &p, bool interactive)
{
p.setRenderHint(QPainter::Antialiasing);
for (const auto &shape : exceptionShapes) {
const bool active = interactive && shape.region.contains(seekable->getOffset());
QColor fill = shape.region.color();
fill.setAlpha(active ? 48 : 22);
p.setBrush(fill);
QPen outline(shape.region.color(), active ? 2 : 1);
outline.setStyle(Qt::DashLine);
p.setPen(outline);
p.drawPath(shape.path);
}
}
QString DisassemblerGraphView::exceptionTooltip(RVA address, RVA size) const
{
QStringList details;
for (const auto &region : exceptionRegions) {
if (region.touches(address, size)) {
details << region.description();
}
}
return details.join(QStringLiteral("\n\n"))
.toHtmlEscaped()
.replace(QLatin1Char('\n'), QStringLiteral("<br>"));
}
DisassemblerGraphView::EdgeConfigurationMapping DisassemblerGraphView::getEdgeConfigurations()
{
EdgeConfigurationMapping result;
@@ -478,8 +640,7 @@ DisassemblerGraphView::MinimapBars DisassemblerGraphView::getMinimapBars()
}
}
};
const bool hasTitleBar = Config()->getGraphBlockEntryOffset()
&& !db.header_text.lines.empty();
const bool hasTitleBar = !db.header_text.lines.empty();
if (hasTitleBar) {
y += int(db.header_text.lines.size()) * charHeight;
} else {
@@ -682,7 +843,7 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block,
free(s);
}
const bool hasTitleBar = Config()->getGraphBlockEntryOffset() && !db.header_text.lines.empty();
const bool hasTitleBar = !db.header_text.lines.empty();
// Draw basic block body with the default background.
p.drawRect(blockRect);
@@ -733,7 +894,7 @@ void DisassemblerGraphView::drawBlock(QPainter &p, GraphView::GraphBlock &block,
const int luma = (titleBg.red() * 299 + titleBg.green() * 587 + titleBg.blue() * 114)
/ 1000;
const QColor titleTextColor = (luma > 128) ? QColor(Qt::black) : QColor(Qt::white);
const QString titleText = "[" + RAddressString(db.entry) + "]";
const QString titleText = db.header_text.ToQString();
QFont titleFont = Config()->getFont();
titleFont.setBold(true);
p.setPen(titleTextColor);
@@ -907,7 +1068,11 @@ GraphView::EdgeConfiguration DisassemblerGraphView::edgeConfiguration(
{
EdgeConfiguration ec;
DisassemblyBlock &db = disassembly_blocks[from.entry];
if (to->entry == db.true_path) {
const auto exception = exceptionEdges.find({from.entry, to->entry});
if (exception != exceptionEdges.end()) {
ec.color = exception->second;
ec.lineStyle = Qt::DashLine;
} else if (to->entry == db.true_path) {
ec.color = brtrueColor;
} else if (to->entry == db.false_path) {
ec.color = brfalseColor;
@@ -1144,6 +1309,7 @@ void DisassemblerGraphView::applyAddressRangeSelection(RVA start, RVA end)
void DisassemblerGraphView::onSeekChanged(RVA addr)
{
exceptionBar->setAddress(addr);
blockMenu->setOffset(addr);
DisassemblyBlock *db = blockForAddress(addr);
bool switchFunction = false;
@@ -1172,8 +1338,13 @@ void DisassemblerGraphView::takeTrue()
if (db->true_path != RVA_INVALID) {
seekable->seek(db->true_path);
} else if (!blocks[db->entry].edges.empty()) {
seekable->seek(blocks[db->entry].edges[0].target);
} else {
for (const auto &edge : blocks[db->entry].edges) {
if (!exceptionEdges.count({db->entry, edge.target})) {
seekable->seek(edge.target);
break;
}
}
}
}
@@ -1186,8 +1357,13 @@ void DisassemblerGraphView::takeFalse()
if (db->false_path != RVA_INVALID) {
seekable->seek(db->false_path);
} else if (!blocks[db->entry].edges.empty()) {
seekable->seek(blocks[db->entry].edges[0].target);
} else {
for (const auto &edge : blocks[db->entry].edges) {
if (!exceptionEdges.count({db->entry, edge.target})) {
seekable->seek(edge.target);
break;
}
}
}
}
@@ -1538,6 +1714,7 @@ void DisassemblerGraphView::blockContextMenuRequested(
? offset
: static_cast<RVA>(block.entry);
blockMenu->setOffset(menuOffset);
exceptionBar->populateMenu(exceptionMenu, block.entry, disassembly_blocks.at(block.entry).size);
actionUnhighlight.setVisible(Core()->getBBHighlighter()->getBasicBlock(block.entry));
actionUnhighlightInstruction.setVisible(
instrForAddress(menuOffset) && Core()->getBIHighlighter()->getBasicInstruction(menuOffset));
@@ -1574,6 +1751,11 @@ void DisassemblerGraphView::blockDoubleClicked(
GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos)
{
Q_UNUSED(event);
if (disassembly_blocks.at(block.entry).externalHandler) {
seekable->seek(block.entry);
refreshView();
return;
}
seekable->seekToReference(getAddrForMouseEvent(block, &pos));
}
@@ -1581,8 +1763,15 @@ void DisassemblerGraphView::blockHelpEvent(
GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos)
{
Instr *instr = getInstrForMouseEvent(block, &pos);
const auto &db = disassembly_blocks.at(block.entry);
const QString exceptions = exceptionTooltip(
instr ? instr->addr : db.entry, instr ? qMax<ut64>(instr->size, 1) : db.size);
if (!instr || instr->fullText.lines.empty()) {
if (!instr) {
if (!exceptions.isEmpty()) {
QToolTip::showText(event->globalPos(), exceptions, viewport());
return;
}
QToolTip::hideText();
event->ignore();
return;
@@ -1604,6 +1793,10 @@ void DisassemblerGraphView::blockHelpEvent(
}
}
if (!exceptions.isEmpty()) {
QToolTip::showText(event->globalPos(), exceptions, viewport());
return;
}
if (instr->fullText.lines.empty()) {
QToolTip::hideText();
event->ignore();
@@ -1616,6 +1809,26 @@ void DisassemblerGraphView::blockHelpEvent(
bool DisassemblerGraphView::helpEvent(QHelpEvent *event)
{
if (!GraphView::helpEvent(event)) {
const QPoint position = viewToLogicalCoordinates(event->pos());
QStringList details;
for (const auto &shape : exceptionShapes) {
if (shape.path.contains(position)) {
for (const auto &region : exceptionRegions) {
if (region.from == shape.region.from && region.to == shape.region.to) {
details << region.description();
}
}
}
}
if (!details.isEmpty()) {
QToolTip::showText(
event->globalPos(),
details.join(QStringLiteral("\n\n"))
.toHtmlEscaped()
.replace(QLatin1Char('\n'), QStringLiteral("<br>")),
viewport());
return true;
}
QToolTip::hideText();
event->ignore();
}
+18
View File
@@ -6,6 +6,7 @@
#include <QHash>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
#include <QShortcut>
#include <QStringList>
#include <QWidget>
@@ -18,6 +19,7 @@
class QTextEdit;
class FallbackSyntaxHighlighter;
class QJsonArray;
class ExceptionBar;
class DisassemblerGraphView : public IaitoGraphView
{
@@ -90,6 +92,7 @@ class DisassemblerGraphView : public IaitoGraphView
ut64 false_path = 0;
bool terminal = false;
bool indirectcall = false;
bool externalHandler = false;
};
enum class BasicBlockContent {
@@ -159,6 +162,8 @@ public slots:
void applyAddressRangeSelection(RVA start, RVA end);
protected:
void drawBackground(QPainter &p, bool interactive) override;
void graphLayoutChanged() override;
void paintEvent(QPaintEvent *event) override;
void blockContextMenuRequested(
GraphView::GraphBlock &block, QContextMenuEvent *event, QPoint pos) override;
@@ -188,6 +193,19 @@ private:
QMenu *contextMenu;
BasicBlockContent basicBlockContent = BasicBlockContent::Disassembly;
QHash<RVA, QList<XrefDescription>> outgoingXRefsCache;
QList<ExceptionRegion> exceptionRegions;
struct ExceptionShape
{
ExceptionRegion region;
QPainterPath path;
};
QList<ExceptionShape> exceptionShapes;
std::map<std::pair<ut64, ut64>, QColor> exceptionEdges;
ExceptionBar *exceptionBar;
QMenu *exceptionMenu;
QAction *showExceptionsAction;
void addExceptionBlocks();
QString exceptionTooltip(RVA address, RVA size) const;
void connectSeekChanged(bool disconnect);
+256 -83
View File
@@ -12,6 +12,7 @@
#include "dialogs/ShortcutKeysDialog.h"
#include "menus/DisassemblyContextMenu.h"
#include "widgets/ExceptionBar.h"
#include <algorithm>
#include <cmath>
#include <limits>
@@ -29,6 +30,7 @@
#include <QSplitter>
#include <QTextBlock>
#include <QTextBlockUserData>
#include <QTextLayout>
#include <QToolTip>
#include <QVBoxLayout>
#include <QWheelEvent>
@@ -67,22 +69,29 @@ static int getMaxVisibleDisassemblyLines(QPlainTextEdit *textEdit)
return qMax(1, int(std::ceil(availableHeight / lineHeight)));
}
static QString tokenAtCursor(QTextCursor cursor)
static QTextCursor tokenRangeAtCursor(QTextCursor cursor)
{
const int clickedCharPos = cursor.positionInBlock();
cursor.select(QTextCursor::BlockUnderCursor);
const QString line = cursor.selectedText().replace("\xc2\xa0", " ");
const QString line = cursor.block().text().replace("\xc2\xa0", " ");
static const QRegularExpression tokenRegExp(R"(\b(?<!\.)([^\s]+)\b(?!\.))");
QRegularExpressionMatchIterator i = tokenRegExp.globalMatch(line);
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
if (match.capturedStart() <= clickedCharPos && match.capturedEnd() > clickedCharPos) {
return match.captured();
const int start = cursor.block().position() + match.capturedStart();
cursor.setPosition(start);
cursor.setPosition(start + match.capturedLength(), QTextCursor::KeepAnchor);
return cursor;
}
}
return QString();
return {};
}
static QString tokenAtCursor(const QTextCursor &cursor)
{
return tokenRangeAtCursor(cursor).selectedText();
}
static QString wordAtCursor(QTextCursor cursor)
@@ -176,7 +185,17 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
auto *container = new QWidget;
container->setMinimumHeight(0);
container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Ignored);
auto *containerLayout = new QHBoxLayout(container);
auto *outerLayout = new QVBoxLayout(container);
outerLayout->setContentsMargins(0, 0, 0, 0);
outerLayout->setSpacing(0);
exceptionBar = new ExceptionBar(container);
outerLayout->addWidget(exceptionBar);
connect(exceptionBar, &ExceptionBar::seekRequested, this, [this](quint64 address) {
seekable->seek(address);
});
exceptionMenu = mCtxMenu->addMenu(tr("Exception handlers"));
auto *containerLayout = new QHBoxLayout;
outerLayout->addLayout(containerLayout, 1);
containerLayout->setContentsMargins(0, 0, 0, 0);
containerLayout->setSpacing(0);
containerLayout->addWidget(splitter);
@@ -221,6 +240,11 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
// Event filter to intercept double clicks in the textbox
// and showing tooltips when hovering above those offsets
mDisasTextEdit->viewport()->installEventFilter(this);
connect(
mDisasTextEdit->horizontalScrollBar(),
&QScrollBar::valueChanged,
this,
&DisassemblyWidget::refreshDisassemblyHover);
// Set Disas context menu
mDisasTextEdit->setContextMenuPolicy(Qt::CustomContextMenu);
@@ -298,6 +322,10 @@ DisassemblyWidget::DisassemblyWidget(MainWindow *main)
mainWindow->showMemoryWidget(MemoryWidgetType::Graph);
})
ADD_ACTION("disasm.switchToDecompiler", Qt::WidgetWithChildrenShortcut, [this] {
mainWindow->showMemoryWidget(MemoryWidgetType::Decompiler);
})
ADD_ACTION("disasm.seekPrev", Qt::WidgetWithChildrenShortcut, &DisassemblyWidget::seekPrev)
ADD_ACTION("disasm.cursorDown", Qt::WidgetWithChildrenShortcut, [this]() {
@@ -323,7 +351,8 @@ void DisassemblyWidget::setPreviewMode(bool previewMode)
action->setEnabled(!previewMode);
}
for (auto action : actions()) {
if (action->shortcut() == Qt::Key_Space || action->shortcut() == Qt::Key_Escape) {
if (action->shortcut() == Qt::Key_Space || action->shortcut() == Qt::Key_Tab
|| action->shortcut() == Qt::Key_Escape) {
action->setEnabled(!previewMode);
}
}
@@ -498,6 +527,7 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
return;
}
setHoveredToken({});
topOffset = Core()->alignInstructionAddress(topOffset);
if (maxLines <= 0) {
@@ -511,6 +541,9 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
breakpoints = Core()->getBreakpointsAddresses();
// Retrieve disassembly lines
if (!cached) {
exceptionRegions = Core()->getExceptionRegions();
}
outgoingXRefsCache.clear();
const QList<DisassemblyLine> oldLines = lines;
if (cached) {
@@ -566,6 +599,7 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
}
}
updateExceptionBar(!cached);
if (!cached || !updateDocumentIncrementally(oldLines, newVisible)) {
rebuildDocument(newVisible);
}
@@ -573,6 +607,24 @@ void DisassemblyWidget::refreshDisasm(RVA offset)
addressScrollBar->setViewport(maxLines, bottomOffset > topOffset ? bottomOffset - topOffset : 1);
addressScrollBar->setAddress(topOffset);
addressScrollBar->setSeekAddress(seekable->getOffset());
refreshDisassemblyHover();
}
void DisassemblyWidget::updateExceptionBar(bool force)
{
const RVA address = seekable->getOffset();
const RVA function = Core()->getFunctionStart(address);
if (force || exceptionFunction != function) {
exceptionFunction = function;
QList<ExceptionRegion> regions;
if (function != RVA_INVALID && !exceptionRegions.isEmpty()) {
const auto blocks = Core()->cmdj("afbj @ " + RAddressString(function)).array();
regions = ExceptionRegion::forBlocks(exceptionRegions, blocks);
}
exceptionBar->setRegions(regions, address);
} else {
exceptionBar->setAddress(address);
}
}
void DisassemblyWidget::decorateBlock(
@@ -580,6 +632,14 @@ void DisassemblyWidget::decorateBlock(
{
QTextCursor cursor(block);
QTextBlockFormat f;
for (const auto &region : exceptionRegions) {
if (region.contains(line.offset) || region.handler == line.offset) {
QColor color = region.color();
color.setAlpha(region.handler == line.offset ? 65 : 26);
f.setBackground(color);
break;
}
}
if (Core()->isBreakpoint(breakpoints, line.offset)) {
f.setBackground(ConfigColor("gui.breakpoint_background"));
} else {
@@ -644,7 +704,7 @@ bool DisassemblyWidget::documentMatchesLines(const QList<DisassemblyLine> &ref,
return true;
}
// Scrolls by editing only the rows that changed and blitting the rest of the view
// Scrolls by editing only the rows that changed, retaining the other text layouts.
bool DisassemblyWidget::updateDocumentIncrementally(
const QList<DisassemblyLine> &oldLines, int newVisible)
{
@@ -682,19 +742,15 @@ bool DisassemblyWidget::updateDocumentIncrementally(
return false;
}
QAbstractTextDocumentLayout *layout = doc->documentLayout();
const bool layoutSignalsBlocked = layout->blockSignals(true);
connectCursorPositionChanged(true);
mDisasTextEdit->setLockScroll(true);
mDisasTextEdit->setUpdatesEnabled(false);
QTextCursor cursor(doc);
const QTextBlockFormat regular = cursor.blockFormat();
BasicBlockColor bbColor;
qreal delta = 0;
cursor.beginEditBlock();
if (shift > 0) {
delta = mDisasTextEdit->blockTop(doc->findBlockByNumber(shift))
- mDisasTextEdit->blockTop(doc->firstBlock());
cursor.movePosition(QTextCursor::Start);
cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, shift);
cursor.removeSelectedText();
@@ -735,29 +791,18 @@ bool DisassemblyWidget::updateDocumentIncrementally(
doc->findBlockByNumber(i).clearLayout();
}
doc->lastBlock().clearLayout();
if (shift < 0) {
delta = -(
mDisasTextEdit->blockTop(doc->findBlockByNumber(-shift))
- mDisasTextEdit->blockTop(doc->firstBlock()));
}
layout->blockSignals(layoutSignalsBlocked);
mDisasTextEdit->setLockScroll(false);
connectCursorPositionChanged(false);
// Editing the document also invalidates layouts, selections and dirty regions.
// Repaint from the updated document instead of moving stale viewport pixels.
mDisasTextEdit->setUpdatesEnabled(true);
visibleLines = newVisible;
if (!documentMatchesLines(lines, newVisible)) {
return false; // the full rebuild repairs whatever went wrong
}
// Blit everything below the document margin so only the new rows get painted
QWidget *viewport = mDisasTextEdit->viewport();
const int margin = qMax(0, qCeil(mDisasTextEdit->textOffset()));
if (qAbs(delta - qRound(delta)) > 0.01) {
viewport->update();
} else {
viewport->scroll(
0, -qRound(delta), QRect(0, margin, viewport->width(), viewport->height() - margin));
}
updateCursorPosition();
if (readCurrentDisassemblyOffset() == seekable->getOffset()) {
// The cursor did not move, extend the highlights to the new rows
@@ -987,28 +1032,13 @@ void DisassemblyWidget::clearBasicBlockColorCache()
void DisassemblyWidget::highlightCurrentLine()
{
updateExceptionBar();
QList<QTextEdit::ExtraSelection> extraSelections;
QColor highlightColor = ConfigColor("lineHighlight");
// Highlight the current word
QTextCursor cursor = mDisasTextEdit->textCursor();
auto clickedCharPos = cursor.positionInBlock();
// Select the line (BlockUnderCursor matches a line with current
// implementation)
cursor.select(QTextCursor::BlockUnderCursor);
// Remove any non-breakable space from the current line
QString searchString = cursor.selectedText().replace("\xc2\xa0", " ");
// Cut the line in "tokens" that can be highlighted
static const QRegularExpression tokenRegExp(R"(\b(?<!\.)([^\s]+)\b(?!\.))");
QRegularExpressionMatchIterator i = tokenRegExp.globalMatch(searchString);
while (i.hasNext()) {
QRegularExpressionMatch match = i.next();
// Current token is under our cursor, select this one
if (match.capturedStart() <= clickedCharPos && match.capturedEnd() > clickedCharPos) {
curHighlightedWord = match.captured();
break;
}
}
curHighlightedWord = tokenAtCursor(cursor);
// Highlight the current line
QTextEdit::ExtraSelection highlightSelection;
@@ -1039,7 +1069,7 @@ void DisassemblyWidget::highlightCurrentLine()
QString scrHighlight = Core()->getConfig("scr.highlight");
extraSelections.append(createHighlightSelections(mDisasTextEdit, scrHighlight));
mDisasTextEdit->setExtraSelections(extraSelections);
setDisassemblySelections(extraSelections);
}
void DisassemblyWidget::highlightPCLine()
@@ -1076,7 +1106,7 @@ void DisassemblyWidget::highlightPCLine()
QList<QTextEdit::ExtraSelection> currentSelections = mDisasTextEdit->extraSelections();
currentSelections.append(pcSelections);
mDisasTextEdit->setExtraSelections(currentSelections);
setDisassemblySelections(currentSelections);
}
void DisassemblyWidget::showDisasContextMenu(const QPoint &pt)
@@ -1259,8 +1289,7 @@ void DisassemblyWidget::updateCursorPosition()
if (parked.position() != 0 || parked.hasSelection()) {
mDisasTextEdit->moveCursor(QTextCursor::Start);
}
mDisasTextEdit->setExtraSelections(
createSameWordsSelections(mDisasTextEdit, curHighlightedWord));
setDisassemblySelections(createSameWordsSelections(mDisasTextEdit, curHighlightedWord));
} else {
RVA currentCursorOffset = readCurrentDisassemblyOffset();
QTextCursor originalCursor = mDisasTextEdit->textCursor();
@@ -1285,7 +1314,7 @@ void DisassemblyWidget::updateCursorPosition()
break;
} else if (lineOffset != RVA_INVALID && lineOffset > offset) {
mDisasTextEdit->moveCursor(QTextCursor::Start);
mDisasTextEdit->setExtraSelections({});
setDisassemblySelections({});
break;
}
@@ -1354,6 +1383,7 @@ void DisassemblyWidget::cursorPositionChanged()
void DisassemblyWidget::updateContextMenuState()
{
exceptionBar->populateMenu(exceptionMenu, readCurrentDisassemblyOffset());
QTextCursor cursor = mDisasTextEdit->textCursor();
bool hasSelection = cursor.hasSelection();
@@ -1449,8 +1479,12 @@ void DisassemblyWidget::copyBytes()
void DisassemblyWidget::jumpToOffsetUnderCursor(const QTextCursor &cursor)
{
RVA offset = readDisassemblyOffset(cursor);
seekable->seekToReference(offset);
const RVA target = tokenTarget(cursor);
if (target != RVA_INVALID) {
seekable->seek(target);
} else {
seekable->seekToReference(readDisassemblyOffset(cursor));
}
}
QList<XrefDescription> DisassemblyWidget::getOutgoingXRefs(RVA offset)
@@ -1490,36 +1524,109 @@ RVA DisassemblyWidget::xrefTargetForToken(RVA offset, const QString &token)
return RVA_INVALID;
}
bool DisassemblyWidget::isActionableTokenAt(const QPoint &pos)
RVA DisassemblyWidget::tokenTarget(const QTextCursor &cursor)
{
QTextCursor cursor = mDisasTextEdit->cursorForPosition(pos);
const RVA offset = readDisassemblyOffset(cursor);
if (offset == RVA_INVALID) {
return false;
return RVA_INVALID;
}
const QString token = tokenAtCursor(cursor);
if (token.trimmed().isEmpty()) {
return false;
return RVA_INVALID;
}
return xrefTargetForToken(offset, token) != RVA_INVALID;
if (token.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
bool isAddress = false;
const RVA address = token.toULongLong(&isAddress, 16);
if (isAddress) {
return address;
}
}
{
RCoreLocked core = Core()->core();
const QByteArray name = token.toUtf8();
if (RFlagItem *flag = r_flag_get(core->flags, name.constData())) {
return flag->addr;
}
if (RAnalFunction *function = r_anal_get_function_byname(core->anal, name.constData())) {
return function->addr;
}
}
return xrefTargetForToken(offset, token);
}
QString DisassemblyWidget::deepLinkAt(const QPoint &pos)
{
const QTextCursor cursor = mDisasTextEdit->cursorForPosition(pos);
const QTextCursor cursor = mDisasTextEdit->cursorOnCharacter(pos);
return DeepLink::linkAt(cursor.block().text(), cursor.positionInBlock());
}
void DisassemblyWidget::updateDisassemblyCursor(const QPoint &pos, Qt::MouseButtons buttons)
void DisassemblyWidget::setDisassemblySelections(QList<QTextEdit::ExtraSelection> selections)
{
constexpr int hoverProperty = QTextFormat::UserProperty;
selections.erase(
std::remove_if(
selections.begin(),
selections.end(),
[](const QTextEdit::ExtraSelection &s) { return s.format.boolProperty(hoverProperty); }),
selections.end());
if (hoveredToken.hasSelection()) {
QTextEdit::ExtraSelection hover;
hover.cursor = hoveredToken;
hover.format.setBackground(ConfigColor("wordHighlight"));
hover.format.setProperty(hoverProperty, true);
selections.append(hover);
}
mDisasTextEdit->setExtraSelections(selections);
}
void DisassemblyWidget::setHoveredToken(const QTextCursor &cursor)
{
if (hoveredToken == cursor) {
return;
}
hoveredToken = cursor;
setDisassemblySelections(mDisasTextEdit->extraSelections());
}
void DisassemblyWidget::updateDisassemblyHover(const QPoint &pos, Qt::MouseButtons buttons)
{
QTextCursor hover;
Qt::CursorShape shape = Qt::ArrowCursor;
if (buttons & Qt::LeftButton) {
mDisasTextEdit->viewport()->setCursor(Qt::IBeamCursor);
} else if (isActionableTokenAt(pos) || !deepLinkAt(pos).isEmpty()) {
mDisasTextEdit->viewport()->setCursor(Qt::PointingHandCursor);
shape = Qt::IBeamCursor;
} else {
QTextCursor cursor = mDisasTextEdit->cursorOnCharacter(pos);
const QString text = cursor.block().text();
const QString link = DeepLink::linkAt(text, cursor.positionInBlock());
if (!link.isEmpty()) {
const int start = cursor.block().position()
+ text.lastIndexOf(link, cursor.positionInBlock());
cursor.setPosition(start);
cursor.setPosition(start + link.size(), QTextCursor::KeepAnchor);
hover = cursor;
} else if (tokenTarget(cursor) != RVA_INVALID) {
hover = tokenRangeAtCursor(cursor);
}
if (hover.hasSelection()) {
shape = Qt::PointingHandCursor;
}
}
mDisasTextEdit->viewport()->setCursor(shape);
setHoveredToken(hover);
}
void DisassemblyWidget::refreshDisassemblyHover()
{
if (mDisasTextEdit->viewport()->underMouse()) {
updateDisassemblyHover(
mDisasTextEdit->viewport()->mapFromGlobal(QCursor::pos()), QApplication::mouseButtons());
} else {
mDisasTextEdit->viewport()->setCursor(Qt::ArrowCursor);
setHoveredToken({});
}
}
@@ -1527,40 +1634,68 @@ bool DisassemblyWidget::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::Resize && obj == mDisasTextEdit->viewport()) {
QMetaObject::invokeMethod(this, [this]() { updateMaxLines(); }, Qt::QueuedConnection);
} else if (event->type() == QEvent::MouseButtonPress && obj == mDisasTextEdit->viewport()) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
pressedTokenTarget = RVA_INVALID;
followedTokenOnClick = false;
if (mouseEvent->button() == Qt::LeftButton && mouseEvent->modifiers() == Qt::NoModifier) {
tokenPressPosition = mouseEvent->pos();
pressedTokenTarget = tokenTarget(mDisasTextEdit->cursorOnCharacter(tokenPressPosition));
}
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
} else if (event->type() == QEvent::MouseMove && obj == mDisasTextEdit->viewport()) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
updateDisassemblyCursor(mouseEvent->pos(), mouseEvent->buttons());
if ((mouseEvent->buttons() & Qt::LeftButton)
&& (mouseEvent->pos() - tokenPressPosition).manhattanLength()
>= QApplication::startDragDistance()) {
pressedTokenTarget = RVA_INVALID;
}
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
} else if (event->type() == QEvent::MouseButtonRelease && obj == mDisasTextEdit->viewport()) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
const RVA target = pressedTokenTarget;
pressedTokenTarget = RVA_INVALID;
if (mouseEvent->button() == Qt::LeftButton && !mDisasTextEdit->textCursor().hasSelection()) {
const QString link = deepLinkAt(mouseEvent->pos());
if (!link.isEmpty()) {
DeepLink::handle(mainWindow, link);
return true;
}
if (target != RVA_INVALID
&& (mouseEvent->pos() - tokenPressPosition).manhattanLength()
< QApplication::startDragDistance()
&& target == tokenTarget(mDisasTextEdit->cursorOnCharacter(mouseEvent->pos()))) {
followedTokenOnClick = true;
seekable->seek(target);
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
return true;
}
}
updateDisassemblyCursor(mouseEvent->pos(), mouseEvent->buttons());
updateDisassemblyHover(mouseEvent->pos(), mouseEvent->buttons());
} else if (event->type() == QEvent::Leave && obj == mDisasTextEdit->viewport()) {
mDisasTextEdit->viewport()->setCursor(Qt::ArrowCursor);
setHoveredToken({});
} else if (
event->type() == QEvent::MouseButtonDblClick
&& (obj == mDisasTextEdit || obj == mDisasTextEdit->viewport())) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
pressedTokenTarget = RVA_INVALID;
if (mouseEvent->button() != Qt::LeftButton || followedTokenOnClick) {
return true;
}
const QTextCursor &cursor = mDisasTextEdit->cursorForPosition(
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
mouseEvent->position().toPoint()
#else
QPoint(mouseEvent->x(), mouseEvent->y())
#endif
);
const QTextCursor cursor = mDisasTextEdit->cursorOnCharacter(mouseEvent->pos());
jumpToOffsetUnderCursor(cursor);
return true;
} else if (event->type() == QEvent::ToolTip && obj == mDisasTextEdit->viewport()) {
QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
QTextCursor cursorForWord = mDisasTextEdit->cursorForPosition(helpEvent->pos());
QTextCursor cursorForWord = mDisasTextEdit->cursorOnCharacter(helpEvent->pos());
if (cursorForWord.isNull()) {
QToolTip::hideText();
return true;
}
const QString registerTooltip = PreviewTooltip::buildRegisterPreview(
wordAtCursor(cursorForWord));
if (!registerTooltip.isEmpty()) {
@@ -1568,9 +1703,8 @@ bool DisassemblyWidget::eventFilter(QObject *obj, QEvent *event)
return true;
}
const QString token = tokenAtCursor(cursorForWord);
RVA offsetFrom = readDisassemblyOffset(cursorForWord);
RVA offsetTo = xrefTargetForToken(offsetFrom, token);
RVA offsetTo = tokenTarget(cursorForWord);
// Only if the offset we point *to* is different from the one the
// cursor is currently on *and* the former is a valid offset, we are
@@ -1582,6 +1716,22 @@ bool DisassemblyWidget::eventFilter(QObject *obj, QEvent *event)
}
}
if (offsetTo == RVA_INVALID || offsetTo == offsetFrom) {
QStringList details;
for (const auto &region : exceptionRegions) {
if (region.contains(offsetFrom) || region.handler == offsetFrom) {
details << region.description();
}
}
if (!details.isEmpty()) {
QToolTip::showText(
helpEvent->globalPos(),
details.join(QStringLiteral("\n\n"))
.toHtmlEscaped()
.replace(QLatin1Char('\n'), QStringLiteral("<br>")),
this);
}
}
return true;
}
@@ -1654,6 +1804,7 @@ void DisassemblyWidget::colorsUpdatedSlot()
{
PreviewTooltip::applyStyleSheet(this);
setupColors();
setDisassemblySelections(mDisasTextEdit->extraSelections());
// Skip the expensive refreshDisasm() (which re-runs disassembleLines)
// when only the interface palette changed; the cached line HTML still
// carries valid radare2 colors, so a repaint is enough.
@@ -1682,9 +1833,36 @@ void DisassemblyWidget::setupColors()
.arg(ConfigColor("btext").name()));
}
qreal DisassemblyTextEdit::blockTop(const QTextBlock &block) const
// Mouse hits must land on text, rather than snap to a nearby caret position.
QTextCursor DisassemblyTextEdit::cursorOnCharacter(const QPoint &pos) const
{
return blockBoundingGeometry(block).top();
if (!viewport()->rect().contains(pos)) {
return {};
}
const QTextBlock block = cursorForPosition(pos).block();
if (!block.isValid()) {
return {};
}
const QPointF localPos = QPointF(pos)
- blockBoundingGeometry(block).translated(contentOffset()).topLeft();
const QTextLayout *layout = block.layout();
for (int i = 0; i < layout->lineCount(); ++i) {
const QTextLine line = layout->lineAt(i);
const QRectF rect = line.naturalTextRect();
if (localPos.x() < rect.left() || localPos.x() >= rect.right() || localPos.y() < rect.top()
|| localPos.y() >= rect.bottom()) {
continue;
}
const int column = line.xToCursor(localPos.x(), QTextLine::CursorOnCharacter);
const QString text = block.text();
if (column >= text.size() || text.at(column).isSpace()) {
return {};
}
QTextCursor cursor(block);
cursor.setPosition(block.position() + column);
return cursor;
}
return {};
}
// Viewport rectangles of every block, in document order
@@ -1698,11 +1876,6 @@ QList<QRectF> DisassemblyTextEdit::blockRects() const
return rects;
}
qreal DisassemblyTextEdit::textOffset() const
{
return (blockBoundingGeometry(document()->begin()).topLeft() + contentOffset()).y();
}
bool DisassemblyTextEdit::viewportEvent(QEvent *event)
{
switch (event->type()) {
@@ -1755,7 +1928,7 @@ void DisassemblyTextEdit::mousePressEvent(QMouseEvent *event)
void DisassemblyTextEdit::paintEvent(QPaintEvent *event)
{
// The viewport is opaque so scrolling can blit it, paint its background
// The viewport is opaque, so every damaged area needs its background painted.
QPainter(viewport()).fillRect(event->rect(), backgroundColor);
QPlainTextEdit::paintEvent(event);
@@ -1794,7 +1967,7 @@ void DisassemblyTextEdit::paintEvent(QPaintEvent *event)
return;
}
// Endpoints only depend on visible rows so blitted lines match a fresh paint
// Derive endpoints from the visible rows.
const auto &functionRanges = disas->getFunctionRanges();
for (auto range = functionRanges.cbegin(); range != functionRanges.cend(); ++range) {
auto firstLine = rowSpan.lowerBound(range.key());
+16 -4
View File
@@ -22,6 +22,7 @@ class DisassemblyContextMenu;
class DisassemblyLeftPanel;
class QWheelEvent;
class AddressScrollBar;
class ExceptionBar;
class DisassemblyWidget : public MemoryDockWidget
{
@@ -93,6 +94,7 @@ private:
qreal wheelRemainder;
QString curHighlightedWord;
QTextCursor hoveredToken;
struct BasicBlockColor
{
@@ -103,6 +105,11 @@ private:
QList<BasicBlockColor> basicBlockColorCache;
QHash<RVA, QList<XrefDescription>> outgoingXRefsCache;
QList<ExceptionRegion> exceptionRegions;
RVA exceptionFunction = RVA_INVALID;
ExceptionBar *exceptionBar;
QMenu *exceptionMenu;
void updateExceptionBar(bool force = false);
/**
* offset of lines below the first line of the current seek
@@ -110,6 +117,9 @@ private:
int cursorLineOffset;
int cursorCharOffset;
bool seekFromCursor;
QPoint tokenPressPosition;
RVA pressedTokenTarget = RVA_INVALID;
bool followedTokenOnClick = false;
bool applyingAddressRangeSelection = false;
bool publishingAddressRangeSelection = false;
@@ -148,8 +158,11 @@ private:
void jumpToOffsetUnderCursor(const QTextCursor &);
QList<XrefDescription> getOutgoingXRefs(RVA offset);
RVA xrefTargetForToken(RVA offset, const QString &token);
bool isActionableTokenAt(const QPoint &pos);
void updateDisassemblyCursor(const QPoint &pos, Qt::MouseButtons buttons);
RVA tokenTarget(const QTextCursor &cursor);
void setDisassemblySelections(QList<QTextEdit::ExtraSelection> selections);
void setHoveredToken(const QTextCursor &cursor);
void updateDisassemblyHover(const QPoint &pos, Qt::MouseButtons buttons);
void refreshDisassemblyHover();
QString deepLinkAt(const QPoint &pos);
};
@@ -167,8 +180,7 @@ public:
void setLockScroll(bool lock) { this->lockScroll = lock; }
void setBackgroundColor(const QColor &color) { backgroundColor = color; }
qreal textOffset() const;
qreal blockTop(const QTextBlock &block) const;
QTextCursor cursorOnCharacter(const QPoint &pos) const;
QList<QRectF> blockRects() const;
public:
+78
View File
@@ -0,0 +1,78 @@
#include "ExceptionBar.h"
#include <QHBoxLayout>
#include <QMenu>
#include <QToolButton>
ExceptionBar::ExceptionBar(QWidget *parent)
: QWidget(parent)
{
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
auto *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
navigator = new QToolButton(this);
navigator->setAutoRaise(true);
navigator->setPopupMode(QToolButton::InstantPopup);
navigator->setMenu(new QMenu(navigator));
navigator->setToolTip(tr("Navigate protected ranges and exception handlers (read-only)"));
layout->addWidget(navigator);
layout->addStretch();
hide();
}
void ExceptionBar::addRegion(QMenu *menu, const ExceptionRegion &region)
{
auto *entry = menu->addMenu(QStringLiteral("#%1 · %2").arg(region.index).arg(region.label()));
entry->setToolTipsVisible(true);
entry->menuAction()->setToolTip(region.description());
const auto add = [this, entry](const QString &title, quint64 address) {
auto *action = entry->addAction(QStringLiteral("%1 · 0x%2").arg(title).arg(address, 0, 16));
connect(action, &QAction::triggered, this, [this, address]() {
emit seekRequested(address);
});
};
add(tr("Go to handler"), region.handler);
add(tr("Go to try start"), region.from);
add(tr("Go to try end (exclusive)"), region.to);
// In PE metadata filter values 0 and 1 are constants, not code addresses.
if (region.filter > 1 && region.filter != ExceptionRegion::Invalid) {
add(tr("Go to filter"), region.filter);
}
}
void ExceptionBar::setRegions(const QList<ExceptionRegion> &value, quint64 address)
{
regions = value;
navigator->menu()->clear();
for (const auto &region : regions) {
addRegion(navigator->menu(), region);
}
navigator->setText(tr("Exceptions (%1)").arg(regions.size()));
setVisible(!regions.isEmpty());
setAddress(address);
}
void ExceptionBar::setAddress(quint64 address)
{
QStringList details;
details << tr("Navigate this function's protected ranges and exception handlers (read-only)");
for (const auto &region : regions) {
if (region.contains(address) || region.handler == address) {
details << region.description();
}
}
navigator->setToolTip(details.join(QStringLiteral("\n\n"))
.toHtmlEscaped()
.replace(QLatin1Char('\n'), QStringLiteral("<br>")));
}
void ExceptionBar::populateMenu(QMenu *menu, quint64 address, quint64 size)
{
menu->clear();
for (const auto &region : regions) {
if (region.touches(address, size)) {
addRegion(menu, region);
}
}
menu->menuAction()->setVisible(!menu->isEmpty());
}
+29
View File
@@ -0,0 +1,29 @@
#ifndef EXCEPTIONBAR_H
#define EXCEPTIONBAR_H
#include "common/ExceptionInfo.h"
#include <QWidget>
class QToolButton;
class QMenu;
// Shared, read-only exception navigator for code views.
class ExceptionBar : public QWidget
{
Q_OBJECT
public:
explicit ExceptionBar(QWidget *parent = nullptr);
void setRegions(const QList<ExceptionRegion> &regions, quint64 address);
void setAddress(quint64 address);
void populateMenu(QMenu *menu, quint64 address, quint64 size = 1);
signals:
void seekRequested(quint64 address);
private:
QList<ExceptionRegion> regions;
QToolButton *navigator;
void addRegion(QMenu *menu, const ExceptionRegion &region);
};
#endif
+4 -1
View File
@@ -95,6 +95,9 @@ ExportsProxyModel::ExportsProxyModel(ExportsModel *source_model, QObject *parent
bool ExportsProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
if (!filterAcceptsType(row, parent)) {
return false;
}
QModelIndex index = sourceModel()->index(row, 0, parent);
auto exp = index.data(ExportsModel::ExportDescriptionRole).value<ExportDescription>();
return exp.name.contains(FILTER_REGEX);
@@ -138,7 +141,7 @@ ExportsWidget::ExportsWidget(MainWindow *main)
exportsModel = new ExportsModel(&exports, this);
exportsProxyModel = new ExportsProxyModel(exportsModel, this);
setModels(exportsProxyModel);
setModels(exportsProxyModel, ExportsModel::NameColumn, ExportsModel::TypeColumn);
ui->treeView->sortByColumn(ExportsModel::OffsetColumn, Qt::AscendingOrder);
QShortcut *toggle_shortcut = ShortcutMgr()->registerShortcut("widget.toggleExports", main);
+36 -16
View File
@@ -1,4 +1,5 @@
#include "FlagsWidget.h"
#include "NameListView.h"
#include "common/Helpers.h"
#include "common/ShortcutManager.h"
#include "core/MainWindow.h"
@@ -161,33 +162,37 @@ FlagsWidget::FlagsWidget(MainWindow *main)
flags_model = new FlagsModel(&flags, this);
flags_proxy_model = new FlagsSortFilterProxyModel(flags_model, this);
connect(
ui->filterLineEdit,
&QLineEdit::textChanged,
ui->quickFilterView,
&QuickFilterView::filterTextChanged,
flags_proxy_model,
&QSortFilterProxyModel::setFilterWildcard);
&AddressableFilterProxyModel::setFilterWildcard);
ui->flagsTreeView->setMainWindow(mainWindow);
ui->flagsTreeView->setModel(static_cast<QAbstractItemModel *>(flags_proxy_model));
auto names = new NameListView(
ui->flagsTreeView,
ui->flagsTreeView->getItemContextMenu(),
flags_proxy_model,
FlagsModel::NAME,
objectName());
ui->quickFilterView->addActionButton(names->toggleAction());
ui->flagsTreeView->sortByColumn(FlagsModel::OFFSET, Qt::AscendingOrder);
// Ctrl-F to move the focus to the Filter search box
QShortcut *searchShortcut = ShortcutMgr()->registerShortcut("list.showFilter", this);
connect(searchShortcut, &QShortcut::activated, ui->filterLineEdit, [this]() {
ui->filterLineEdit->setFocus();
});
connect(searchShortcut, &QShortcut::activated, ui->quickFilterView, &QuickFilterView::showFilter);
searchShortcut->setContext(Qt::WidgetWithChildrenShortcut);
// Esc to clear the filter entry
QShortcut *clearShortcut = ShortcutMgr()->registerShortcut("list.clearFilter", this);
connect(clearShortcut, &QShortcut::activated, [this] {
if (ui->filterLineEdit->text().isEmpty()) {
ui->flagsTreeView->setFocus();
} else {
ui->filterLineEdit->setText("");
}
});
connect(clearShortcut, &QShortcut::activated, [this] { ui->quickFilterView->clearFilter(); });
connect(
ui->quickFilterView,
&QuickFilterView::filterClosed,
ui->flagsTreeView,
static_cast<void (QWidget::*)()>(&QWidget::setFocus));
clearShortcut->setContext(Qt::WidgetWithChildrenShortcut);
connect(ui->filterLineEdit, &QLineEdit::textChanged, this, [this] {
connect(ui->quickFilterView, &QuickFilterView::filterTextChanged, this, [this] {
tree->showItemsNumber(flags_proxy_model->rowCount());
});
@@ -204,6 +209,21 @@ FlagsWidget::FlagsWidget(MainWindow *main)
menu->addSeparator();
menu->addAction(ui->actionRename);
menu->addAction(ui->actionDelete);
connect(menu, &QMenu::aboutToShow, this, [this] {
const bool hasFlag = ui->flagsTreeView->itemAddress(ui->flagsTreeView->currentIndex())
!= RVA_INVALID;
ui->actionRename->setEnabled(hasFlag);
ui->actionDelete->setEnabled(hasFlag);
});
connect(
ui->flagsTreeView->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
[this](const QModelIndex &index) {
const bool hasFlag = ui->flagsTreeView->itemAddress(index) != RVA_INVALID;
ui->actionRename->setEnabled(hasFlag);
ui->actionDelete->setEnabled(hasFlag);
});
addAction(ui->actionRename);
addAction(ui->actionDelete);
}
@@ -224,7 +244,7 @@ void FlagsWidget::on_flagspaceCombo_currentTextChanged(const QString &arg1)
void FlagsWidget::on_actionRename_triggered()
{
const QModelIndex index = ui->flagsTreeView->selectionModel()->currentIndex();
if (!index.isValid()) {
if (!index.isValid() || !index.data(FlagsModel::FlagDescriptionRole).isValid()) {
return;
}
FlagDescription flag = index.data(FlagsModel::FlagDescriptionRole).value<FlagDescription>();
@@ -245,7 +265,7 @@ void FlagsWidget::on_actionRename_triggered()
void FlagsWidget::on_actionDelete_triggered()
{
const QModelIndex index = ui->flagsTreeView->selectionModel()->currentIndex();
if (!index.isValid()) {
if (!index.isValid() || !index.data(FlagsModel::FlagDescriptionRole).isValid()) {
return;
}
FlagDescription flag = index.data(FlagsModel::FlagDescriptionRole).value<FlagDescription>();
+7 -8
View File
@@ -71,14 +71,7 @@
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="filterLineEdit">
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Quick Filter</string>
</property>
</widget>
<widget class="QuickFilterView" name="quickFilterView" native="true"/>
</item>
<item>
<widget class="QLabel" name="flagspaceLabel">
@@ -118,6 +111,12 @@
</action>
</widget>
<customwidgets>
<customwidget>
<class>QuickFilterView</class>
<extends>QWidget</extends>
<header>widgets/QuickFilterView.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>AddressableItemList&lt;&gt;</class>
<extends>QTreeView</extends>
+20 -26
View File
@@ -530,7 +530,7 @@ FunctionsWidget::FunctionsWidget(MainWindow *main)
functionModel = new FunctionModel(
&functions, &importAddresses, &mainAdress, false, default_font, highlight_font, this);
functionProxyModel = new FunctionSortFilterProxyModel(functionModel, this);
setModels(functionProxyModel);
setModels(functionProxyModel, FunctionModel::NameColumn);
ui->treeView->sortByColumn(FunctionModel::NameColumn, Qt::AscendingOrder);
connect(Config(), &Configuration::fontsUpdated, this, [this]() {
@@ -593,6 +593,21 @@ FunctionsWidget::FunctionsWidget(MainWindow *main)
itemConextMenu->setActionIcon(
actionSetPinMenu, AddressableItemContextMenu::MenuIcon::Pin, QColor(103, 80, 164));
itemConextMenu->setWholeFunction(true);
connect(itemConextMenu, &QMenu::aboutToShow, this, [this] {
const bool hasFunction = ui->treeView->itemAddress(ui->treeView->currentIndex())
!= RVA_INVALID;
actionRename.setEnabled(hasFunction);
actionUndefine.setEnabled(hasFunction);
actionSetColorMenu->setEnabled(hasFunction);
actionSetPinMenu->setEnabled(hasFunction);
});
connect(
ui->treeView->selectionModel(),
&QItemSelectionModel::currentChanged,
this,
[this](const QModelIndex &index) {
actionRename.setEnabled(ui->treeView->itemAddress(index) != RVA_INVALID);
});
addActions(itemConextMenu->actions());
@@ -685,7 +700,7 @@ void FunctionsWidget::onActionFunctionsRenameTriggered()
{
// Get selected item in functions tree view
const QModelIndex index = ui->treeView->selectionModel()->currentIndex();
if (!index.isValid()) {
if (!index.isValid() || !index.data(FunctionModel::FunctionDescriptionRole).isValid()) {
return;
}
FunctionDescription function
@@ -712,12 +727,7 @@ void FunctionsWidget::onActionFunctionsRenameTriggered()
void FunctionsWidget::onActionFunctionsUndefineTriggered()
{
const auto selection = ui->treeView->selectionModel()->selection().indexes();
std::vector<RVA> offsets;
offsets.reserve(selection.size());
for (const auto &index : selection) {
offsets.push_back(functionProxyModel->address(index));
}
const auto offsets = ui->treeView->selectedAddresses();
for (RVA offset : offsets) {
Core()->delFunction(offset);
}
@@ -725,15 +735,7 @@ void FunctionsWidget::onActionFunctionsUndefineTriggered()
void FunctionsWidget::onActionFunctionColorPicked(const QString &r2Color)
{
const auto selection = ui->treeView->selectionModel()->selection().indexes();
std::vector<RVA> offsets;
offsets.reserve(selection.size());
for (const auto &index : selection) {
RVA off = functionProxyModel->address(index);
if (std::find(offsets.begin(), offsets.end(), off) == offsets.end()) {
offsets.push_back(off);
}
}
const auto offsets = ui->treeView->selectedAddresses();
bool affectsView = false;
RVA seek = Core()->getOffset();
for (RVA offset : offsets) {
@@ -755,15 +757,7 @@ void FunctionsWidget::onActionFunctionColorPicked(const QString &r2Color)
void FunctionsWidget::onActionFunctionPinPicked(const QString &emoji)
{
const auto selection = ui->treeView->selectionModel()->selection().indexes();
std::vector<RVA> offsets;
offsets.reserve(selection.size());
for (const auto &index : selection) {
RVA off = functionProxyModel->address(index);
if (std::find(offsets.begin(), offsets.end(), off) == offsets.end()) {
offsets.push_back(off);
}
}
const auto offsets = ui->treeView->selectedAddresses();
bool affectsView = false;
RVA seek = Core()->getOffset();
for (RVA offset : offsets) {
+5
View File
@@ -144,6 +144,7 @@ void GraphView::contextMenuEvent(QContextMenuEvent *event)
void GraphView::computeGraphPlacement()
{
graphLayoutSystem->CalculateLayout(blocks, entry, width, height);
graphLayoutChanged();
setCacheDirty();
clampViewOffset();
viewport()->update();
@@ -371,6 +372,10 @@ void GraphView::paint(QPainter &p, QPoint offset, QRect viewport, qreal scale, b
p.setWindow(window);
QRectF windowF(window.x(), window.y(), window.width(), window.height());
p.save();
drawBackground(p, interactive);
p.restore();
for (auto &blockIt : blocks) {
GraphBlock &block = blockIt.second;
+2
View File
@@ -131,6 +131,8 @@ protected:
* @param interactive - can be used for disabling elemnts during export
*/
virtual void drawBlock(QPainter &p, GraphView::GraphBlock &block, bool interactive = true) = 0;
virtual void drawBackground(QPainter &, bool) {}
virtual void graphLayoutChanged() {}
virtual void blockClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos);
virtual void blockDoubleClicked(GraphView::GraphBlock &block, QMouseEvent *event, QPoint pos);
virtual void blockHelpEvent(GraphView::GraphBlock &block, QHelpEvent *event, QPoint pos);
+263
View File
@@ -0,0 +1,263 @@
#include "HexPatWidget.h"
#include "ui_HexPatWidget.h"
#include "common/Configuration.h"
#include "common/Helpers.h"
#include "core/Iaito.h"
#include <QAction>
#include <QApplication>
#include <QClipboard>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QHeaderView>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMenu>
#include <QRegularExpression>
namespace {
enum Column { Name, Type, Address, Size, Value, ColumnCount };
bool isPluginPath(const QString &path)
{
// hexpat splits arguments on whitespace and passes paths unquoted to r2's
// test/cat commands. Quoting the outer command alone cannot protect them.
static const QRegularExpression unsafe(QStringLiteral("[^\\p{L}\\p{N}_./:-]"));
return !path.contains(unsafe);
}
bool filterItem(QTreeWidgetItem *item, const QString &text, bool parentMatches = false)
{
bool matches = parentMatches || text.isEmpty();
for (int column = 0; column < ColumnCount; ++column) {
matches |= item->text(column).contains(text, Qt::CaseInsensitive);
}
bool visible = matches;
for (int i = 0; i < item->childCount(); ++i) {
visible |= filterItem(item->child(i), text, matches);
}
item->setHidden(!visible);
if (visible && !text.isEmpty() && item->childCount()) {
item->setExpanded(true);
}
return visible;
}
} // namespace
HexPatWidget::HexPatWidget(MainWindow *main)
: IaitoDockWidget(main)
, ui(new Ui::HexPatWidget)
{
ui->setupUi(this);
ui->patternsTree->setFont(Config()->getFont());
qhelpers::setVerticalScrollMode(ui->patternsTree);
ui->patternsTree->header()->setStretchLastSection(true);
connect(ui->openButton, &QPushButton::clicked, this, &HexPatWidget::openPattern);
connect(ui->reloadButton, &QPushButton::clicked, this, &HexPatWidget::evaluatePattern);
connect(ui->fileEdit, &QLineEdit::returnPressed, this, &HexPatWidget::evaluatePattern);
connect(ui->includeEdit, &QLineEdit::returnPressed, this, &HexPatWidget::evaluatePattern);
connect(ui->includeButton, &QPushButton::clicked, this, [this]() {
const QString path = QFileDialog::getExistingDirectory(
this, tr("Pattern include directory"), ui->includeEdit->text());
if (!path.isEmpty()) {
ui->includeEdit->setText(QDir::fromNativeSeparators(path));
}
});
connect(ui->filterEdit, &QLineEdit::textChanged, this, &HexPatWidget::filterPatterns);
connect(ui->patternsTree, &QTreeWidget::itemActivated, this, [this](QTreeWidgetItem *item) {
seekToPattern(item);
});
connect(ui->patternsTree, &QTreeWidget::customContextMenuRequested, this, [this](QPoint pos) {
auto *item = ui->patternsTree->itemAt(pos);
if (!item) {
return;
}
QMenu menu(this);
auto *seek = menu.addAction(tr("Seek to address"), this, [this, item]() {
seekToPattern(item);
});
seek->setEnabled(item->data(Address, Qt::UserRole).isValid());
menu.addAction(tr("Copy value"), this, [item]() {
QApplication::clipboard()->setText(item->text(Value));
});
menu.addAction(tr("Copy row"), this, [item]() {
QStringList fields;
for (int column = 0; column < ColumnCount; ++column) {
fields.append(item->text(column));
}
QApplication::clipboard()->setText(fields.join(QLatin1Char('\t')));
});
menu.exec(ui->patternsTree->viewport()->mapToGlobal(pos));
});
connect(Config(), &Configuration::fontsUpdated, this, [this]() {
ui->patternsTree->setFont(Config()->getFont());
});
connect(Core(), &IaitoCore::refreshAll, this, &HexPatWidget::updateAvailability);
updateAvailability();
}
HexPatWidget::~HexPatWidget() = default;
void HexPatWidget::updateAvailability()
{
bool found = false;
for (const QJsonValue &entry : Core()->cmdj("Lcj").array()) {
if (entry.toObject().value("name").toString() == QLatin1String("hexpat")) {
found = true;
break;
}
}
if (!found) {
ui->patternsTree->clear();
ui->statusLabel->setText(tr("Install the hexpat r2js plugin to load ImHex patterns."));
} else if (!available) {
ui->statusLabel->setText(
tr("Open a .hexpat file to evaluate it against the current binary."));
}
available = found;
ui->dockWidgetContents->setEnabled(available);
toggleViewAction()->setEnabled(available);
const QString hint = available ? tr("Load and explore ImHex pattern files")
: tr("Requires the hexpat r2js plugin (r2hexpat)");
toggleViewAction()->setToolTip(hint);
toggleViewAction()->setStatusTip(hint);
}
void HexPatWidget::openPattern()
{
const QString path = QFileDialog::getOpenFileName(
this,
tr("Open ImHex pattern"),
ui->fileEdit->text(),
tr("ImHex patterns (*.hexpat *.pat);;All files (*)"));
if (!path.isEmpty()) {
ui->fileEdit->setText(QDir::fromNativeSeparators(path));
evaluatePattern();
}
}
void HexPatWidget::evaluatePattern()
{
updateAvailability();
if (!available) {
return;
}
// Drop the previous result before validation so a failed reload never looks successful.
ui->patternsTree->clear();
const QFileInfo file(ui->fileEdit->text());
if (!file.isFile() || !file.isReadable()) {
ui->statusLabel->setText(tr("Choose a readable ImHex pattern file."));
return;
}
const QString path = QDir::fromNativeSeparators(file.absoluteFilePath());
const QString include = ui->includeEdit->text();
const QString includePath = include.isEmpty() ? QString()
: QDir::fromNativeSeparators(
QFileInfo(include).absoluteFilePath());
if (!isPluginPath(path) || !isPluginPath(includePath)) {
ui->statusLabel->setText(
tr("The hexpat plugin requires paths without spaces or command metacharacters. "
"Use paths containing only letters, numbers, /, :, ., _, or -."));
return;
}
if (!includePath.isEmpty() && !QFileInfo(includePath).isDir()) {
ui->statusLabel->setText(tr("The include directory does not exist."));
return;
}
ui->fileEdit->setText(path);
QString command = QStringLiteral("hexpatj ");
if (!includePath.isEmpty()) {
command += QStringLiteral("-I %1 ").arg(includePath);
}
command += path;
// r2js shares the core's JavaScript runtime; execute on the core/UI thread.
const QJsonDocument result = QJsonDocument::fromJson(Core()->cmd(command).toUtf8());
if (!result.isArray()) {
ui->statusLabel->setText(
tr("Pattern evaluation failed. See the Console for hexpat error details."));
return;
}
const QJsonArray patterns = result.array();
for (const QJsonValue &pattern : patterns) {
addPattern(ui->patternsTree->invisibleRootItem(), pattern.toObject());
}
ui->patternsTree->expandToDepth(0);
for (int column = 0; column < Value; ++column) {
ui->patternsTree->resizeColumnToContents(column);
}
filterPatterns();
ui->statusLabel->setText(
patterns.isEmpty()
? tr("Evaluation completed without placed patterns.")
: tr("%n top-level pattern(s). Double-click a field to seek to its address.",
nullptr,
patterns.size()));
}
void HexPatWidget::addPattern(QTreeWidgetItem *parent, const QJsonObject &pattern)
{
auto *item = new QTreeWidgetItem(parent);
item->setText(Name, pattern.value("name").toString());
item->setText(Type, pattern.value("type").toString());
item->setText(Size, pattern.value("size").toVariant().toString());
const QVariant address = pattern.value("addr").toVariant();
bool validAddress = false;
const RVA addr = address.toULongLong(&validAddress);
if (validAddress) {
item->setText(Address, RAddressString(addr));
// Heap/local patterns and custom sections do not refer to the binary's address space.
if (!pattern.value("local").toBool() && pattern.value("section").toInt() == 0) {
item->setData(Address, Qt::UserRole, QVariant::fromValue(addr));
}
}
item->setText(
Value,
pattern.contains("repr") ? pattern.value("repr").toString()
: pattern.value("value").toVariant().toString());
if (pattern.contains("bits")) {
item->setText(Size, tr("%1 bits").arg(pattern.value("bits").toInt()));
}
QString detail
= tr("Kind: %1\nOffset: %2")
.arg(pattern.value("kind").toString(), pattern.value("offset").toVariant().toString());
if (pattern.contains("bitOffset")) {
detail += tr("\nBit offset: %1").arg(pattern.value("bitOffset").toInt());
}
if (pattern.value("local").toBool()) {
detail += tr("\nLocal pattern (no binary address)");
} else if (pattern.value("section").toInt() != 0) {
detail += tr("\nSection: %1 (no binary address)")
.arg(pattern.value("section").toVariant().toString());
}
for (int column = 0; column < ColumnCount; ++column) {
item->setToolTip(column, item->text(column) + QLatin1Char('\n') + detail);
}
for (const char *key : {"children", "entries"}) {
for (const QJsonValue &child : pattern.value(key).toArray()) {
addPattern(item, child.toObject());
}
}
if (pattern.value("pointee").isObject()) {
addPattern(item, pattern.value("pointee").toObject());
}
}
void HexPatWidget::seekToPattern(QTreeWidgetItem *item)
{
if (item && item->data(Address, Qt::UserRole).isValid()) {
Core()->seekAndShow(item->data(Address, Qt::UserRole).toULongLong());
}
}
void HexPatWidget::filterPatterns()
{
const QString text = ui->filterEdit->text();
for (int i = 0; i < ui->patternsTree->topLevelItemCount(); ++i) {
filterItem(ui->patternsTree->topLevelItem(i), text);
}
}
+39
View File
@@ -0,0 +1,39 @@
#ifndef HEXPATWIDGET_H
#define HEXPATWIDGET_H
#include "IaitoDockWidget.h"
#include <memory>
class QJsonObject;
class QTreeWidgetItem;
namespace Ui {
class HexPatWidget;
}
class HexPatWidget : public IaitoDockWidget
{
Q_OBJECT
public:
explicit HexPatWidget(MainWindow *main);
~HexPatWidget() override;
public slots:
void updateAvailability();
private slots:
void openPattern();
void evaluatePattern();
void filterPatterns();
private:
std::unique_ptr<Ui::HexPatWidget> ui;
bool available = false;
void addPattern(QTreeWidgetItem *parent, const QJsonObject &pattern);
void seekToPattern(QTreeWidgetItem *item);
};
#endif // HEXPATWIDGET_H
+81
View File
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>HexPatWidget</class>
<widget class="QDockWidget" name="hexPatWidget">
<property name="windowTitle">
<string>HexPat</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="fileLayout">
<item>
<widget class="QLineEdit" name="fileEdit">
<property name="placeholderText"><string>ImHex pattern file (.hexpat)</string></property>
<property name="toolTip"><string>Pattern to evaluate against the current binary</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="openButton">
<property name="text"><string>Open…</string></property>
<property name="toolTip"><string>Open and evaluate an ImHex pattern file</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="reloadButton">
<property name="text"><string>Reload</string></property>
<property name="toolTip"><string>Evaluate the file again using the current binary data</string></property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="includeLayout">
<item>
<widget class="QLineEdit" name="includeEdit">
<property name="placeholderText"><string>Include directory (optional)</string></property>
<property name="toolTip"><string>Additional search directory for #include and import</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="includeButton">
<property name="text"><string>Browse…</string></property>
<property name="toolTip"><string>Choose an include directory</string></property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLineEdit" name="filterEdit">
<property name="placeholderText"><string>Filter fields, types, addresses, or values…</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="patternsTree">
<property name="contextMenuPolicy"><enum>Qt::CustomContextMenu</enum></property>
<property name="editTriggers"><set>QAbstractItemView::NoEditTriggers</set></property>
<property name="alternatingRowColors"><bool>true</bool></property>
<property name="uniformRowHeights"><bool>true</bool></property>
<property name="expandsOnDoubleClick"><bool>false</bool></property>
<column><property name="text"><string>Name</string></property></column>
<column><property name="text"><string>Type</string></property></column>
<column><property name="text"><string>Address</string></property></column>
<column><property name="text"><string>Size</string></property></column>
<column><property name="text"><string>Value</string></property></column>
</widget>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text"><string>Open a .hexpat file to evaluate it against the current binary.</string></property>
<property name="textFormat"><enum>Qt::PlainText</enum></property>
<property name="wordWrap"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
+572 -73
View File
@@ -25,6 +25,7 @@
#include <QJsonObject>
#include <QKeyEvent>
#include <QMenu>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPaintEvent>
#include <QPainter>
@@ -99,22 +100,60 @@ struct GlyphRow
};
} // namespace
void HexWidget::drawFlagsBackground(QPainter &painter, bool ascii)
void HexWidget::drawBackgroundRanges(QPainter &painter, bool ascii)
{
if (flagBackgroundRanges.isEmpty()) {
if (flagBackgroundRanges.isEmpty() && basicBlockBackgroundRanges.isEmpty()) {
return;
}
painter.save();
painter.setPen(Qt::NoPen);
for (const auto &range : flagBackgroundRanges) {
painter.setBrush(range.color);
auto polys = rangePolygons(range.start, range.end, ascii);
for (const auto &poly : polys) {
painter.drawPolygon(poly);
const auto drawRanges = [&](const auto &ranges) {
for (const auto &range : ranges) {
painter.setBrush(range.color);
auto polys = rangePolygons(range.start, range.end, ascii);
for (const auto &poly : polys) {
painter.drawPolygon(poly);
}
}
};
drawRanges(flagBackgroundRanges);
drawRanges(basicBlockBackgroundRanges);
painter.restore();
}
void HexWidget::updateBasicBlockBackgroundRanges()
{
basicBlockBackgroundRanges.clear();
// The analysis interval API uses an exclusive end address.
const uint64_t end = std::min<uint64_t>(lastVisibleAddr(), UINT64_MAX - 1);
if (visibleLines <= 0 || end < startAddress) {
return;
}
auto core = Core()->core();
RList *blocks = r_anal_get_blocks_intersect(core->anal, startAddress, end - startAddress + 1);
RListIter *it;
RAnalBlock *block;
IaitoRListForeach(blocks, it, RAnalBlock, block)
{
if (!block->size) {
continue;
}
QColor color;
if (auto *highlight = Core()->getBBHighlighter()->getBasicBlock(block->addr)) {
color = highlight->color;
} else if (block->color.r || block->color.g || block->color.b) {
color = QColor(block->color.r, block->color.g, block->color.b);
}
if (color.isValid()) {
color.setAlphaF(0.3);
const uint64_t last = block->addr
+ std::min<uint64_t>(block->size - 1, UINT64_MAX - block->addr);
basicBlockBackgroundRanges.append({block->addr, last, color});
}
}
painter.restore();
r_list_free(blocks);
}
// Scans [startAddr, lastAddr] for flags. Ranges found by earlier paints are
@@ -217,6 +256,9 @@ static constexpr uint64_t MAX_COPY_SIZE = 128 * 1024 * 1024;
static constexpr int MAX_LINE_WIDTH_PRESET = 32;
static constexpr int MAX_LINE_WIDTH_BYTES = 128 * 1024;
static constexpr int RELATIVE_ADDRESS_CHAR_LEN = 32;
// Bound sparse reads/scans even for an empty 64-bit address space. Cache both
// sides of the viewport; painting and cursor blinking never scan or fetch.
static constexpr int SPARSE_LOOKAHEAD_BYTES = 256 * 1024;
static int relativeAddressType(const QString &relto)
{
@@ -290,6 +332,7 @@ HexWidget::HexWidget(QWidget *parent)
setMonospaceFont(Config()->getFont());
});
connect(Core(), &IaitoCore::asmOptionsChanged, this, [this]() { refresh(); });
connect(Core(), &IaitoCore::functionsChanged, this, &HexWidget::refresh);
auto sizeActionGroup = new QActionGroup(this);
for (int i = 1; i <= 8; i *= 2) {
@@ -349,6 +392,11 @@ HexWidget::HexWidget(QWidget *parent)
actionHexPairs->setCheckable(true);
connect(actionHexPairs, &QAction::triggered, this, &HexWidget::onHexPairsModeEnabled);
actionSparse = new QAction(tr("Collapse zero / FF regions"), this);
actionSparse->setCheckable(true);
actionSparse->setToolTip(tr("Collapse runs of identical 00 or FF bytes longer than two rows"));
connect(actionSparse, &QAction::toggled, this, &HexWidget::setSparse);
actionCopy = new QAction(tr("Copy"), this);
addAction(actionCopy);
actionCopy->setShortcutContext(Qt::ShortcutContext::WidgetWithChildrenShortcut);
@@ -658,6 +706,187 @@ void HexWidget::setColumnMode(ColumnMode mode)
viewport()->update();
}
void HexWidget::setSparse(bool enabled)
{
if (sparse == enabled) {
return;
}
sparse = enabled;
actionSparse->setChecked(enabled);
hoverAddress = UINT64_MAX;
flagBackgroundRanges.clear();
fetchData(true);
updateCursorMeta();
syncScrollBar();
viewport()->update();
}
void HexWidget::rebuildSparseRows()
{
const auto hasGaps = [this]() {
return std::any_of(sparseRows.begin(), sparseRows.end(), [](const SparseHexLayout::Row &row) {
return row.fill >= 0;
});
};
repaintCollapseButtons = hasGaps();
sparseRows.clear();
if (!isDataAvailable(startAddress, 1)) {
return;
}
const int scanSize = sparse ? std::max(SPARSE_LOOKAHEAD_BYTES, bytesPerScreen())
: bytesPerScreen() + itemRowByteLen() * 2 + 1;
const int length = int(std::min<uint64_t>(scanSize - 1, data->maxIndex() - startAddress)) + 1;
QByteArray bytes(length, Qt::Uninitialized);
if (data->copy(bytes.data(), startAddress, length)) {
sparseRows = SparseHexLayout::build(
reinterpret_cast<const unsigned char *>(bytes.constData()),
length,
itemRowByteLen(),
itemByteLen,
visibleLines + (sparse ? 0 : 1));
if (data->maxIndex() == UINT64_MAX && uint64_t(length - 1) == UINT64_MAX - startAddress
&& !sparseRows.empty()) {
sparseRows.back().continues = false;
}
}
repaintCollapseButtons |= hasGaps();
}
uint64_t HexWidget::rowAddress(int row) const
{
if (sparse && row >= 0 && row < int(sparseRows.size())) {
return startAddress + sparseRows[row].offset;
}
BasicCursor address(startAddress);
address += int64_t(row) * itemRowByteLen();
return address.address;
}
int HexWidget::rowByteCount(int row) const
{
if (sparse) {
return row >= 0 && row < int(sparseRows.size()) ? sparseRows[row].size : 0;
}
return itemRowByteLen();
}
int HexWidget::screenRow(uint64_t address) const
{
if (address < startAddress || address > lastVisibleAddr()) {
return -1;
}
if (sparse) {
const int offset = int(address - startAddress);
auto it = std::upper_bound(
sparseRows.begin(),
sparseRows.end(),
offset,
[](int value, const SparseHexLayout::Row &row) { return value < row.offset; });
return it == sparseRows.begin() ? -1 : int(it - sparseRows.begin()) - 1;
}
return int((address - startAddress) / itemRowByteLen());
}
uint64_t HexWidget::lastVisibleAddr() const
{
int length = bytesPerScreen();
if (sparse) {
length = sparseRows.empty() ? 0 : sparseRows.back().offset + sparseRows.back().size;
}
BasicCursor last(startAddress);
last += std::max(0, length - 1);
return last.address;
}
bool HexWidget::isSparseRow(int row) const
{
return sparse && row >= 0 && row < int(sparseRows.size()) && sparseRows[row].fill >= 0;
}
QRectF HexWidget::sparseRowRect(int row) const
{
return QRectF(
itemArea.left(),
itemArea.top() + row * lineHeight,
(showAscii ? asciiArea.right() : itemArea.right()) - itemArea.left(),
lineHeight);
}
int HexWidget::sparseRowAt(const QPoint &point) const
{
if (point.y() < itemArea.top()) {
return -1;
}
const int row = int((point.y() - itemArea.top()) / lineHeight);
return isSparseRow(row) && sparseRowRect(row).contains(point) ? row : -1;
}
QRectF HexWidget::collapseButtonRect(const SparseHexLayout::Row &gap) const
{
if (sparse || gap.fill < 0) {
return {};
}
const int firstRow = (gap.offset + itemRowByteLen() - 1) / itemRowByteLen();
const int row = std::min(firstRow + 1, visibleLines - 1);
if (row < firstRow || (row + 1) * itemRowByteLen() > gap.offset + gap.size) {
return {};
}
const uint64_t address = rowAddress(row);
if ((cursorEnabled && cursor.address >= address
&& cursor.address - address < uint64_t(itemRowByteLen()))
|| selection.intersects(address, address + itemRowByteLen() - 1)) {
return {};
}
const qreal right = std::min(
showAscii ? asciiArea.right() : itemArea.right(),
qreal(horizontalScrollBar()->value() + viewport()->width()) - charWidth);
const qreal width
= QFontMetricsF(monospaceFont)
.horizontalAdvance(
tr("Collapse %1").arg(gap.fill ? QStringLiteral("FF") : QStringLiteral("00")))
+ charWidth * 3;
const qreal left = std::max(itemArea.left(), right - width);
return QRectF(
left,
itemArea.top() + row * lineHeight + 1,
std::max(qreal(0), right - left),
lineHeight - 2);
}
int HexWidget::collapseButtonAt(const QPoint &point) const
{
if (!sparse) {
for (int i = 0; i < int(sparseRows.size()); ++i) {
if (collapseButtonRect(sparseRows[i]).contains(point)) {
return i;
}
}
}
return -1;
}
BasicCursor HexWidget::rowPosition(int row, int column) const
{
if (!sparse) {
BasicCursor result(startAddress);
result += int64_t(row) * itemRowByteLen() + column;
return result;
}
if (row < 0) {
BasicCursor result(startAddress);
result += int64_t(row) * itemRowByteLen();
return result;
}
if (row >= int(sparseRows.size())) {
BasicCursor result(lastVisibleAddr());
result += 1;
return result;
}
BasicCursor result(rowAddress(row));
result += isSparseRow(row) ? 0 : std::clamp(column, 0, rowByteCount(row));
return result;
}
void HexWidget::selectRange(RVA start, RVA end)
{
BasicCursor endCursor(end);
@@ -690,10 +919,22 @@ void HexWidget::refresh()
hoverAddress = UINT64_MAX;
updateMetrics();
fetchData(true);
updateCursorMeta();
syncScrollBar();
viewport()->update();
}
void HexWidget::setDataReader(const std::function<QByteArray(uint64_t, int)> &reader)
{
if (auto *memory = dynamic_cast<MemoryData *>(data.get())) {
memory->setReader(reader);
}
if (auto *memory = dynamic_cast<MemoryData *>(oldData.get())) {
memory->setReader(reader);
}
refresh();
}
void HexWidget::setItemEndianess(bool bigEndian)
{
itemBigEndian = bigEndian;
@@ -715,6 +956,7 @@ void HexWidget::updateColors()
addrColor = Config()->getColor("func_var_addr");
diffColor = Config()->getColor("graph.diff.unmatch");
updateBasicBlockBackgroundRanges();
updateCursorMeta();
viewport()->update();
}
@@ -728,7 +970,7 @@ void HexWidget::paintEvent(QPaintEvent *event)
if (xOffset > 0)
painter.translate(QPoint(-xOffset, 0));
if (event->rect() == cursor.screenPos.toAlignedRect()) {
if (cursorEnabled && event->rect() == cursor.screenPos.toAlignedRect()) {
/* Cursor blink */
drawCursor(painter);
return;
@@ -747,14 +989,26 @@ void HexWidget::paintEvent(QPaintEvent *event)
return;
}
relativeAddrType = relativeAddressType(Core()->getConfig("asm.addr.relto"));
const uint64_t rowLen = itemRowByteLen();
updateFlagBackgroundRanges(
startAddress + firstRow * rowLen,
startAddress + (lastRow + 1) * rowLen - 1,
firstRow == 0 && lastRow == visibleLines - 1);
if (sparse) {
// Never walk the bytes hidden by a gap when looking up flags.
updateFlagBackgroundRanges(1, 0, true);
for (int row = firstRow; row <= lastRow; ++row) {
if (!isSparseRow(row) && rowByteCount(row) > 0) {
updateFlagBackgroundRanges(
rowAddress(row), rowAddress(row) + rowByteCount(row) - 1, false);
}
}
} else {
updateFlagBackgroundRanges(
rowAddress(firstRow),
lastVisibleAddr() - (visibleLines - lastRow - 1) * itemRowByteLen(),
firstRow == 0 && lastRow == visibleLines - 1);
}
drawAddrArea(painter, firstRow, lastRow);
drawItemArea(painter, firstRow, lastRow);
drawAsciiArea(painter, firstRow, lastRow);
drawSparseRows(painter, firstRow, lastRow);
drawCollapseButtons(painter);
if (!cursorEnabled)
return;
@@ -790,10 +1044,45 @@ void HexWidget::resizeEvent(QResizeEvent *event)
viewport()->update();
}
void HexWidget::updateMouseCursor(const QPoint &pos)
{
Qt::CursorShape shape = Qt::ArrowCursor;
if (updatingSelection) {
shape = Qt::IBeamCursor;
} else if (sparseRowAt(pos) >= 0 || collapseButtonAt(pos) >= 0) {
shape = Qt::PointingHandCursor;
} else if (itemArea.contains(pos) || (showAscii && asciiArea.contains(pos))) {
RCoreLocked core = Core()->core();
const bool hasFlag = core->flags && r_flag_get_in(core->flags, mousePosToAddr(pos).address);
shape = hasFlag ? Qt::PointingHandCursor : Qt::IBeamCursor;
}
viewport()->setCursor(shape);
}
void HexWidget::mouseMoveEvent(QMouseEvent *event)
{
QPoint pos = event->pos();
pos.rx() += horizontalScrollBar()->value();
updateMouseCursor(pos);
const int gap = sparseRowAt(pos);
const int collapse = collapseButtonAt(pos);
if ((gap >= 0 || collapse >= 0) && !updatingSelection) {
const auto &row = sparseRows[gap >= 0 ? gap : collapse];
QString tip = tr("%1 – %2\n%3 bytes of %4")
.arg(
RAddressString(startAddress + row.offset),
RAddressString(startAddress + row.offset + row.size - 1),
QString::number(row.size),
row.fill ? QStringLiteral("FF") : QStringLiteral("00"));
if (row.continues) {
tip += tr("\nRegion continues beyond this view.");
}
tip += gap >= 0 ? tr("\nClick to show all bytes.")
: tr("\nClick to collapse repeated regions.");
QToolTip::showText(viewport()->mapToGlobal(event->pos()), tip, this);
return;
}
auto mouseAddr = mousePosToAddr(pos).address;
@@ -813,10 +1102,6 @@ void HexWidget::mouseMoveEvent(QMouseEvent *event)
}
if (!updatingSelection) {
if (itemArea.contains(pos) || asciiArea.contains(pos))
setCursor(Qt::IBeamCursor);
else
setCursor(Qt::ArrowCursor);
return;
}
@@ -840,6 +1125,18 @@ void HexWidget::mousePressEvent(QMouseEvent *event)
pos.rx() += horizontalScrollBar()->value();
if (event->button() == Qt::LeftButton) {
if (collapseButtonAt(pos) >= 0) {
setSparse(true);
return;
}
const int gap = sparseRowAt(pos);
if (gap >= 0) {
const uint64_t address = rowAddress(gap);
setSparse(false);
scrollTo(address);
seek(address);
return;
}
bool selectingData = itemArea.contains(pos);
bool selecting = selectingData || asciiArea.contains(pos);
if (selecting) {
@@ -849,6 +1146,7 @@ void HexWidget::mousePressEvent(QMouseEvent *event)
setCursorAddr(cursorPosition, event->modifiers() == Qt::ShiftModifier);
viewport()->update();
}
updateMouseCursor(pos);
}
}
@@ -860,6 +1158,9 @@ void HexWidget::mouseReleaseEvent(QMouseEvent *event)
cursorEnabled = true;
}
updatingSelection = false;
QPoint pos = event->pos();
pos.rx() += horizontalScrollBar()->value();
updateMouseCursor(pos);
}
}
@@ -885,6 +1186,36 @@ void HexWidget::wheelEvent(QWheelEvent *event)
void HexWidget::scrollRows(int rows)
{
if (sparse && rows != 0) {
if (rows > 0) {
BasicCursor target(lastVisibleAddr());
target += 1;
scrollTo(rows < int(sparseRows.size()) ? rowAddress(rows) : target.address);
} else if (startAddress > 0) {
// Build the preceding visual rows in reverse, with the same scan cap.
const int length = int(std::min<uint64_t>(startAddress, SPARSE_LOOKAHEAD_BYTES));
const uint64_t from = startAddress - length;
if (!isDataAvailable(from, length)) {
data.swap(oldData);
data->fetch(from, length + std::max(bytesPerScreen(), SPARSE_LOOKAHEAD_BYTES));
}
QByteArray bytes(length, Qt::Uninitialized);
if (data->copy(bytes.data(), from, length)) {
std::reverse(bytes.begin(), bytes.end());
const auto preceding = SparseHexLayout::build(
reinterpret_cast<const unsigned char *>(bytes.constData()),
length,
itemRowByteLen(),
itemByteLen,
-rows);
if (!preceding.empty()) {
const auto &last = preceding.back();
scrollTo(startAddress - last.offset - last.size);
}
}
}
return;
}
const int64_t delta = int64_t(rows) * itemRowByteLen();
if (delta == 0) {
return;
@@ -903,7 +1234,7 @@ void HexWidget::scrollTo(uint64_t address)
const uint64_t oldStart = startAddress;
startAddress = address;
const bool refetched = fetchData();
const uint64_t screen = static_cast<uint64_t>(bytesPerScreen());
const uint64_t screen = sparse ? 1 : static_cast<uint64_t>(bytesPerScreen());
if (data->maxIndex() < screen) {
startAddress = 0;
} else if (startAddress > (data->maxIndex() - screen) + 1) {
@@ -921,7 +1252,7 @@ void HexWidget::scrollTo(uint64_t address)
void HexWidget::syncScrollBar()
{
addressScrollBar->setViewport(visibleLines, bytesPerScreen());
addressScrollBar->setViewport(visibleLines, lastVisibleAddr() - startAddress + 1);
addressScrollBar->setAddress(startAddress);
addressScrollBar->setSeekAddress(cursor.address);
}
@@ -933,7 +1264,8 @@ void HexWidget::scrollViewport(uint64_t oldStart, bool refetched)
const int64_t bytes = int64_t(startAddress - oldStart);
const int rowLen = itemRowByteLen();
const int64_t rows = bytes / rowLen;
if (refetched || rowLen <= 0 || bytes % rowLen != 0 || rows == 0 || qAbs(rows) >= visibleLines) {
if (sparse || repaintCollapseButtons || refetched || rowLen <= 0 || bytes % rowLen != 0
|| rows == 0 || qAbs(rows) >= visibleLines) {
viewport()->update();
return;
}
@@ -981,10 +1313,10 @@ void HexWidget::keyPressEvent(QKeyEvent *event)
}
};
if (moveOrSelect(QKeySequence::MoveToNextLine, QKeySequence::SelectNextLine)) {
moveCursor(itemRowByteLen(), select);
moveCursorRows(1, select);
extendSel();
} else if (moveOrSelect(QKeySequence::MoveToPreviousLine, QKeySequence::SelectPreviousLine)) {
moveCursor(-itemRowByteLen(), select);
moveCursorRows(-1, select);
extendSel();
} else if (moveOrSelect(QKeySequence::MoveToNextChar, QKeySequence::SelectNextChar)) {
moveCursor(cursorOnAscii ? 1 : itemByteLen, select);
@@ -993,18 +1325,19 @@ void HexWidget::keyPressEvent(QKeyEvent *event)
moveCursor(cursorOnAscii ? -1 : -itemByteLen, select);
extendSel();
} else if (moveOrSelect(QKeySequence::MoveToNextPage, QKeySequence::SelectNextPage)) {
moveCursor(bytesPerScreen(), select);
moveCursorRows(visibleLines, select);
extendSel();
} else if (moveOrSelect(QKeySequence::MoveToPreviousPage, QKeySequence::SelectPreviousPage)) {
moveCursor(-bytesPerScreen(), select);
moveCursorRows(-visibleLines, select);
extendSel();
} else if (moveOrSelect(QKeySequence::MoveToStartOfLine, QKeySequence::SelectStartOfLine)) {
int linePos = int((cursor.address % itemRowByteLen()) - (startAddress % itemRowByteLen()));
moveCursor(-linePos, select);
setCursorAddr(rowAddress(screenRow(cursor.address)), select);
extendSel();
} else if (moveOrSelect(QKeySequence::MoveToEndOfLine, QKeySequence::SelectEndOfLine)) {
int linePos = int((cursor.address % itemRowByteLen()) - (startAddress % itemRowByteLen()));
moveCursor(itemRowByteLen() - linePos, select);
const int row = screenRow(cursor.address);
BasicCursor end(rowAddress(row));
end += rowByteCount(row);
setCursorAddr(end, select);
extendSel();
}
// viewport()->update();
@@ -1286,6 +1619,7 @@ QMenu *HexWidget::buildViewFormatMenu(QMenu *parent)
m->addMenu(rowSizeMenu);
m->addSeparator();
m->addAction(actionSparse);
setActionIcon(actionHexPairs, MenuIcon::Pairs, viewColor);
m->addAction(actionHexPairs);
setActionIcon(actionItemBigEndian, MenuIcon::Endian, viewColor);
@@ -1364,6 +1698,7 @@ void HexWidget::addSyncOffsetActions(QMenu *parent)
void HexWidget::contextMenuEvent(QContextMenuEvent *event)
{
QPoint pt = event->pos();
pt.rx() += horizontalScrollBar()->value();
if (event->reason() == QContextMenuEvent::Mouse) {
auto mouseAddr = mousePosToAddr(pt).address;
if (asciiArea.contains(pt)) {
@@ -1386,7 +1721,12 @@ void HexWidget::contextMenuEvent(QContextMenuEvent *event)
buildInsertMenu(menu);
addSyncOffsetActions(menu);
menu->exec(mapToGlobal(pt));
if (ctx.effectiveAddress != RVA_INVALID && ctx.effectiveSize) {
const RVA size = qMin<RVA>(ctx.effectiveSize, RVA_MAX - ctx.effectiveAddress);
emit contextMenuAboutToShow(menu, ctx.effectiveAddress, ctx.effectiveAddress + size - 1);
}
menu->exec(event->globalPos());
actionCopy->setEnabled(!selection.isEmpty());
actionCopyAddress->setEnabled(true);
actionCopyAsCString->setEnabled(true);
@@ -1395,7 +1735,7 @@ void HexWidget::contextMenuEvent(QContextMenuEvent *event)
void HexWidget::onCursorBlinked()
{
if (!cursorEnabled)
if (!cursorEnabled || isSparseRow(screenRow(cursor.address)))
return;
cursor.blink();
QRect cursorRect = cursor.screenPos.toAlignedRect();
@@ -1777,6 +2117,9 @@ void HexWidget::drawHeader(QPainter &painter)
void HexWidget::drawCursor(QPainter &painter, bool shadow)
{
if (screenRow(cursor.address) < 0 || isSparseRow(screenRow(cursor.address))) {
return;
}
if (shadow) {
shadowCursor.screenPos.setWidth(cursorOnAscii ? itemWidth() : charWidth);
QColor hi = palette().color(QPalette::Highlight);
@@ -1811,10 +2154,12 @@ void HexWidget::drawAddrArea(QPainter &painter, int firstRow, int lastRow)
GlyphRow text(painter, rawFont, glyphIndexes, charWidth);
const QColor rowHi = palette().color(QPalette::Highlight);
const int rowLen = itemRowByteLen();
uint64_t offset = startAddress + uint64_t(firstRow) * rowLen;
for (int line = firstRow; line <= lastRow && offset <= data->maxIndex();
++line, strRect.translate(0, lineHeight), offset += rowLen) {
for (int line = firstRow; line <= lastRow; ++line, strRect.translate(0, lineHeight)) {
const uint64_t offset = rowAddress(line);
const int rowLen = rowByteCount(line);
if (rowLen <= 0 || offset > data->maxIndex()) {
break;
}
addrString = formatAddress(offset);
QColor color = addrColor;
if (!selection.isEmpty() && selection.intersects(offset, offset + rowLen - 1)) {
@@ -1845,18 +2190,23 @@ void HexWidget::drawItemArea(QPainter &painter, int firstRow, int lastRow)
QString itemString;
GlyphRow text(painter, rawFont, glyphIndexes, charWidth);
// Draw flag-backed highlights before any selection
drawFlagsBackground(painter, false);
// Keep selections visible above flag and basic block colors.
drawBackgroundRanges(painter, false);
fillSelectionBackground(painter);
const QColor selectedColor = palette().highlightedText().color();
uint64_t itemAddr = startAddress + uint64_t(firstRow) * itemRowByteLen();
for (int line = firstRow; line <= lastRow; ++line) {
for (int line = firstRow; line <= lastRow; ++line, itemRect.translate(0, lineHeight)) {
if (isSparseRow(line) || rowByteCount(line) <= 0) {
continue;
}
uint64_t itemAddr = rowAddress(line);
int remaining = rowByteCount(line);
itemRect.moveLeft(itemArea.left());
text.baseline = itemRect.top() + textBaseline;
for (int j = 0; j < itemColumns; ++j) {
for (int k = 0; k < itemGroupSize && itemAddr <= data->maxIndex();
++k, itemAddr += itemByteLen) {
for (int j = 0; j < itemColumns && remaining >= itemByteLen; ++j) {
for (int k = 0;
k < itemGroupSize && remaining >= itemByteLen && itemAddr <= data->maxIndex();
++k, itemAddr += itemByteLen, remaining -= itemByteLen) {
itemString = renderItem(itemAddr - startAddress, &itemColor);
if (selection.contains(itemAddr)) {
itemColor = selectedColor;
@@ -1875,7 +2225,6 @@ void HexWidget::drawItemArea(QPainter &painter, int firstRow, int lastRow)
itemRect.translate(columnSpacingWidth(), 0);
}
text.flush();
itemRect.translate(0, lineHeight);
}
painter.setPen(borderColor);
@@ -1890,18 +2239,21 @@ void HexWidget::drawAsciiArea(QPainter &painter, int firstRow, int lastRow)
charRect.translate(0, firstRow * lineHeight);
GlyphRow text(painter, rawFont, glyphIndexes, charWidth);
// Draw flag-backed highlights before any selection in ASCII area
drawFlagsBackground(painter, true);
// Keep selections visible above flag and basic block colors.
drawBackgroundRanges(painter, true);
fillSelectionBackground(painter, true);
const QColor selectedColor = palette().highlightedText().color();
uint64_t address = startAddress + uint64_t(firstRow) * itemRowByteLen();
QChar ascii;
QColor color;
for (int line = firstRow; line <= lastRow; ++line, charRect.translate(0, lineHeight)) {
if (isSparseRow(line) || rowByteCount(line) <= 0) {
continue;
}
uint64_t address = rowAddress(line);
charRect.moveLeft(asciiArea.left());
text.baseline = charRect.top() + textBaseline;
for (int j = 0; j < itemRowByteLen() && address <= data->maxIndex(); ++j, ++address) {
for (int j = 0; j < rowByteCount(line) && address <= data->maxIndex(); ++j, ++address) {
ascii = renderAscii(address - startAddress, &color);
if (selection.contains(address)) {
color = selectedColor;
@@ -1930,6 +2282,88 @@ void HexWidget::drawAsciiArea(QPainter &painter, int firstRow, int lastRow)
}
}
void HexWidget::drawSparseRows(QPainter &painter, int firstRow, int lastRow)
{
if (!sparse) {
return;
}
painter.save();
painter.setRenderHint(QPainter::Antialiasing);
const QColor accent = palette().color(QPalette::Highlight);
const QFontMetricsF metrics(monospaceFont);
for (int row = firstRow; row <= lastRow; ++row) {
if (!isSparseRow(row)) {
continue;
}
const auto &gap = sparseRows[row];
const uint64_t address = rowAddress(row);
const bool selected = selection.intersects(address, address + gap.size - 1);
QRectF rect = sparseRowRect(row).adjusted(0, 1, 0, -1);
// Cover any flag/selection fills, then use a subtle theme-colored pill.
painter.fillRect(sparseRowRect(row), backgroundColor);
QColor fill = accent;
fill.setAlpha(selected ? 64 : 24);
painter.setBrush(fill);
painter.setPen(Qt::NoPen);
painter.drawRoundedRect(rect, 3, 3);
painter.setPen(accent);
const qreal x = rect.left() + charWidth;
const qreal y = rect.center().y();
painter.drawLine(QPointF(x - 2, y - 3), QPointF(x + 1, y));
painter.drawLine(QPointF(x + 1, y), QPointF(x - 2, y + 3));
const QString label = tr("%1 · %2 bytes%3 · click to expand")
.arg(
gap.fill ? QStringLiteral("FF") : QStringLiteral("00"),
QString::number(gap.size),
gap.continues ? QStringLiteral("+") : QString());
const QRectF textRect = rect.adjusted(charWidth * 2, 0, -charWidth, 0);
painter.setPen(defColor);
painter.drawText(
textRect,
Qt::AlignVCenter | Qt::AlignLeft,
metrics.elidedText(label, Qt::ElideRight, textRect.width()));
if (cursor.address >= address && cursor.address - address < uint64_t(gap.size)) {
painter.setBrush(Qt::NoBrush);
painter.setPen(accent);
painter.drawRoundedRect(rect.adjusted(0.5, 0.5, -0.5, -0.5), 3, 3);
}
}
painter.restore();
}
void HexWidget::drawCollapseButtons(QPainter &painter)
{
if (sparse) {
return;
}
painter.save();
painter.setRenderHint(QPainter::Antialiasing);
const QColor accent = palette().color(QPalette::Highlight);
const QFontMetricsF metrics(monospaceFont);
for (const auto &gap : sparseRows) {
const QRectF rect = collapseButtonRect(gap);
if (rect.isEmpty()) {
continue;
}
painter.setBrush(backgroundColor);
painter.setPen(accent);
painter.drawRoundedRect(rect.adjusted(0.5, 0.5, -0.5, -0.5), 3, 3);
const qreal x = rect.left() + charWidth;
const qreal y = rect.center().y();
painter.drawLine(QPointF(x - 3, y - 1), QPointF(x, y + 2));
painter.drawLine(QPointF(x, y + 2), QPointF(x + 3, y - 1));
const QString label
= tr("Collapse %1").arg(gap.fill ? QStringLiteral("FF") : QStringLiteral("00"));
const QRectF textRect = rect.adjusted(charWidth * 2, 0, -charWidth / 2, 0);
painter.setPen(defColor);
painter.drawText(
textRect,
Qt::AlignVCenter | Qt::AlignLeft,
metrics.elidedText(label, Qt::ElideRight, textRect.width()));
}
painter.restore();
}
void HexWidget::fillSelectionBackground(QPainter &painter, bool ascii)
{
if (selection.isEmpty()) {
@@ -1950,6 +2384,31 @@ QVector<QPolygonF> HexWidget::rangePolygons(RVA start, RVA last, bool ascii)
return {};
}
if (sparse) {
QVector<QPolygonF> parts;
for (int row = 0; row < int(sparseRows.size()); ++row) {
const uint64_t address = rowAddress(row);
const uint64_t end = address + rowByteCount(row) - 1;
if (isSparseRow(row) || end < start || address > last) {
continue;
}
const int firstOffset = int(std::max<uint64_t>(start, address) - startAddress);
const int lastOffset = int(std::min<uint64_t>(last, end) - startAddress);
QRectF first = ascii ? asciiRectangle(firstOffset) : itemRectangle(firstOffset);
QRectF final = ascii ? asciiRectangle(lastOffset) : itemRectangle(lastOffset);
if (!ascii) {
first.setLeft(
first.left() + (firstOffset % itemByteLen) * itemWidth() / itemByteLen);
final.setRight(
final.right()
- (itemByteLen - 1 - lastOffset % itemByteLen) * itemWidth() / itemByteLen);
}
first.setRight(final.right());
parts.append(QPolygonF(first));
}
return parts;
}
QRectF rect;
const QRectF area = QRectF(ascii ? asciiArea : itemArea);
@@ -2078,7 +2537,8 @@ void HexWidget::updateAreasPosition()
void HexWidget::updateAreasHeight()
{
visibleLines = static_cast<int>((viewport()->height() - itemArea.top()) / lineHeight);
visibleLines
= std::max(0, static_cast<int>((viewport()->height() - itemArea.top()) / lineHeight));
qreal height = visibleLines * lineHeight;
addrArea.setHeight(height);
@@ -2090,12 +2550,42 @@ void HexWidget::moveCursor(int offset, bool select)
{
BasicCursor addr = cursor.address;
addr += offset;
if (sparse) {
const int row = screenRow(cursor.address);
if (isSparseRow(row) && offset > 0) {
addr = rowAddress(row);
addr += rowByteCount(row);
} else if (isSparseRow(row) && offset < 0) {
addr = rowAddress(row);
addr += offset;
}
}
if (addr.address > data->maxIndex()) {
addr.address = data->maxIndex();
}
setCursorAddr(addr, select);
}
void HexWidget::moveCursorRows(int rows, bool select)
{
if (!sparse) {
moveCursor(rows * itemRowByteLen(), select);
return;
}
const int currentRow = screenRow(cursor.address);
const int column = currentRow < 0 || isSparseRow(currentRow)
? 0
: int(cursor.address - rowAddress(currentRow));
const int targetRow = currentRow + rows;
if (targetRow >= 0 && targetRow < int(sparseRows.size())) {
setCursorAddr(rowPosition(targetRow, std::min(column, rowByteCount(targetRow) - 1)), select);
return;
}
scrollRows(rows);
const int row = std::clamp(currentRow, 0, std::max(0, int(sparseRows.size()) - 1));
setCursorAddr(rowPosition(row, std::min(column, std::max(0, rowByteCount(row) - 1))), select);
}
void HexWidget::setCursorAddr(BasicCursor addr, bool select)
{
bool addressChanged = cursor.address != addr.address;
@@ -2131,7 +2621,7 @@ void HexWidget::setCursorAddr(BasicCursor addr, bool select)
addressValue -= (addressValue % itemRowByteLen());
/* FIXME: handling Page Up/Down */
if (addressValue == startAddress + bytesPerScreen()) {
if (!sparse && addressValue == startAddress + bytesPerScreen()) {
startAddress += itemRowByteLen();
} else {
startAddress = addressValue;
@@ -2139,7 +2629,7 @@ void HexWidget::setCursorAddr(BasicCursor addr, bool select)
refetched = fetchData();
if (startAddress > (data->maxIndex() - bytesPerScreen()) + 1) {
if (!sparse && startAddress > (data->maxIndex() - bytesPerScreen()) + 1) {
startAddress = (data->maxIndex() - bytesPerScreen()) + 1;
}
}
@@ -2152,7 +2642,7 @@ void HexWidget::setCursorAddr(BasicCursor addr, bool select)
viewport()->update();
} else {
scrollViewport(oldStart, refetched);
updateRow((previousAddress - startAddress) / itemRowByteLen());
updateRow(screenRow(previousAddress));
updateRow(cursorScreenRow());
}
@@ -2163,7 +2653,7 @@ void HexWidget::setCursorAddr(BasicCursor addr, bool select)
// Row of the cursor address relative to the current start, may be off screen
uint64_t HexWidget::cursorScreenRow() const
{
return (cursor.address - startAddress) / itemRowByteLen();
return screenRow(cursor.address);
}
void HexWidget::updateRow(uint64_t row)
@@ -2181,12 +2671,18 @@ void HexWidget::updateCursorMeta()
QPointF point;
QPointF pointAscii;
int offset = cursor.address - startAddress;
const int row = screenRow(cursor.address);
if (row < 0 || isSparseRow(row)) {
cursor.screenPos.moveTop(-lineHeight * 2);
shadowCursor.screenPos.moveTop(-lineHeight * 2);
return;
}
int offset = cursor.address - rowAddress(row);
int itemOffset = offset;
int asciiOffset;
/* Calc common Y coordinate */
point.ry() = (itemOffset / itemRowByteLen()) * lineHeight;
point.ry() = row * lineHeight;
pointAscii.setY(point.y());
itemOffset %= itemRowByteLen();
asciiOffset = itemOffset;
@@ -2261,7 +2757,7 @@ QVariant HexWidget::readItem(int offset, QColor *color)
float float32;
double float64;
quint8 bytes[sizeof(uint64_t)];
quint8 bytes[sizeof(uint64_t)] = {};
data->copy(bytes, startAddress + offset, static_cast<size_t>(itemByteLen));
const bool signedItem = itemFormat == ItemFormatSignedDec;
@@ -2463,13 +2959,19 @@ bool HexWidget::isDataAvailable(uint64_t address, int length)
bool HexWidget::fetchData(bool force)
{
if (!force && isDataAvailable(startAddress, bytesPerScreen())) {
return false;
bool fetched = force || !isDataAvailable(startAddress, bytesPerScreen());
if (fetched) {
data.swap(oldData);
const int before = sparse ? int(std::min<uint64_t>(startAddress, SPARSE_LOOKAHEAD_BYTES))
: 0;
data->fetch(
startAddress - before,
before
+ (sparse ? std::max(bytesPerScreen(), SPARSE_LOOKAHEAD_BYTES) : bytesPerScreen()));
}
data.swap(oldData);
data->fetch(startAddress, bytesPerScreen());
return true;
rebuildSparseRows();
updateBasicBlockBackgroundRanges();
return fetched;
}
BasicCursor HexWidget::screenPosToAddr(const QPoint &point, bool middle) const
@@ -2478,15 +2980,12 @@ BasicCursor HexWidget::screenPosToAddr(const QPoint &point, bool middle) const
int relativeAddress = 0;
int line = static_cast<int>(pt.y() / lineHeight);
relativeAddress += line * itemRowByteLen();
int column = static_cast<int>(pt.x() / columnExWidth());
relativeAddress += column * itemGroupByteLen();
pt.rx() -= column * columnExWidth();
auto roundingOffset = middle ? itemWidth() / 2 : 0;
relativeAddress += static_cast<int>((pt.x() + roundingOffset) / itemWidth()) * itemByteLen;
BasicCursor result(startAddress);
result += relativeAddress;
return result;
return rowPosition(line, relativeAddress);
}
BasicCursor HexWidget::asciiPosToAddr(const QPoint &point, bool middle) const
@@ -2494,12 +2993,10 @@ BasicCursor HexWidget::asciiPosToAddr(const QPoint &point, bool middle) const
QPointF pt = point - asciiArea.topLeft();
int relativeAddress = 0;
relativeAddress += static_cast<int>(pt.y() / lineHeight) * itemRowByteLen();
const int line = static_cast<int>(pt.y() / lineHeight);
auto roundingOffset = middle ? (charWidth / 2) : 0;
relativeAddress += static_cast<int>((pt.x() + (roundingOffset)) / charWidth);
BasicCursor result(startAddress);
result += relativeAddress;
return result;
return rowPosition(line, relativeAddress);
}
BasicCursor HexWidget::currentAreaPosToAddr(const QPoint &point, bool middle) const
@@ -2519,8 +3016,9 @@ QRectF HexWidget::itemRectangle(int offset)
qreal y;
qreal width = itemWidth();
y = (offset / itemRowByteLen()) * lineHeight;
offset %= itemRowByteLen();
const int row = screenRow(startAddress + offset);
y = row * lineHeight;
offset -= int(rowAddress(row) - startAddress);
x = (offset / itemGroupByteLen()) * columnExWidth();
offset %= itemGroupByteLen();
@@ -2543,8 +3041,9 @@ QRectF HexWidget::asciiRectangle(int offset)
{
QPointF p;
p.ry() = (offset / itemRowByteLen()) * lineHeight;
offset %= itemRowByteLen();
const int row = screenRow(startAddress + offset);
p.ry() = row * lineHeight;
offset -= int(rowAddress(row) - startAddress);
p.rx() = offset * charWidth;
+56 -13
View File
@@ -3,8 +3,10 @@
#include "Iaito.h"
#include "common/IOModesController.h"
#include "common/SparseHexLayout.h"
#include "dialogs/HexdumpRangeDialog.h"
#include <functional>
#include <memory>
#include <QColor>
#include <QIcon>
@@ -150,10 +152,19 @@ public:
m_blocks.clear();
uint64_t addr = alignedAddr;
for (ut64 i = 0; i < len / blockSize; ++i, addr += blockSize) {
m_blocks.append(Core()->ioRead(addr, blockSize));
QByteArray block = reader ? reader(addr, int(blockSize))
: Core()->ioRead(addr, blockSize);
if (block.size() < int(blockSize)) {
block.append(QByteArray(int(blockSize) - block.size(), '\0'));
} else if (block.size() > int(blockSize)) {
block.truncate(int(blockSize));
}
m_blocks.append(block);
}
}
void setReader(const std::function<QByteArray(uint64_t, int)> &next) { reader = next; }
bool copy(void *out, uint64_t addr, size_t len) override
{
if (addr < m_firstBlockAddr || addr > m_lastValidAddr
@@ -166,15 +177,14 @@ public:
int totalOffset = addr - m_firstBlockAddr;
int blockId = totalOffset / BLOCK_SIZE;
int blockOffset = totalOffset % BLOCK_SIZE;
size_t first_part = BLOCK_SIZE - blockOffset;
if (first_part >= len) {
memcpy(out, m_blocks.at(blockId).constData() + blockOffset, len);
} else {
memcpy(out, m_blocks.at(blockId).constData() + blockOffset, first_part);
memcpy(
static_cast<char *>(out) + first_part,
m_blocks.at(blockId + 1).constData(),
len - first_part);
char *dst = static_cast<char *>(out);
while (len > 0) {
const size_t part = std::min(len, BLOCK_SIZE - blockOffset);
memcpy(dst, m_blocks.at(blockId).constData() + blockOffset, part);
dst += part;
len -= part;
++blockId;
blockOffset = 0;
}
return true;
}
@@ -187,6 +197,7 @@ private:
QVector<QByteArray> m_blocks;
uint64_t m_firstBlockAddr = 0;
uint64_t m_lastValidAddr = 0;
std::function<QByteArray(uint64_t, int)> reader;
};
class HexSelection
@@ -306,6 +317,7 @@ public:
*/
void setFixedLineSize(int bytes);
void setColumnMode(ColumnMode mode);
void setSparse(bool enabled);
/**
* @brief Select non empty inclusive range [start; end]
@@ -323,6 +335,9 @@ public:
// TODO: add currentAddress
};
Selection getSelection();
// When set, displayed bytes come from this reader instead of the open file.
void setDataReader(const std::function<QByteArray(uint64_t, int)> &reader);
public slots:
void seek(uint64_t address);
void refresh();
@@ -330,6 +345,7 @@ public slots:
signals:
void selectionChanged(Selection selection);
void positionChanged(RVA start);
void contextMenuAboutToShow(QMenu *menu, RVA start, RVA end);
protected:
void paintEvent(QPaintEvent *event) override;
@@ -364,6 +380,13 @@ private slots:
void w_writeCString();
private:
struct BasicBlockBackgroundRange
{
uint64_t start;
uint64_t end;
QColor color;
};
struct FlagBackgroundRange
{
uint64_t start;
@@ -379,11 +402,25 @@ private:
void drawAddrArea(QPainter &painter, int firstRow, int lastRow);
void drawItemArea(QPainter &painter, int firstRow, int lastRow);
void drawAsciiArea(QPainter &painter, int firstRow, int lastRow);
void drawSparseRows(QPainter &painter, int firstRow, int lastRow);
void rebuildSparseRows();
uint64_t rowAddress(int row) const;
int rowByteCount(int row) const;
int screenRow(uint64_t address) const;
bool isSparseRow(int row) const;
int sparseRowAt(const QPoint &point) const;
QRectF sparseRowRect(int row) const;
QRectF collapseButtonRect(const SparseHexLayout::Row &gap) const;
int collapseButtonAt(const QPoint &point) const;
void drawCollapseButtons(QPainter &painter);
BasicCursor rowPosition(int row, int column) const;
void moveCursorRows(int rows, bool select);
/** Draw the status bar displaying offset and fd command output */
void drawStatusBar(QPainter &painter);
// Draw background color for flags across item/ascii areas
void drawFlagsBackground(QPainter &painter, bool ascii);
// Draw flag and basic block colors across item/ascii areas.
void drawBackgroundRanges(QPainter &painter, bool ascii);
void updateFlagBackgroundRanges(uint64_t startAddr, uint64_t lastAddr, bool fullScreen);
void updateBasicBlockBackgroundRanges();
void updateGlyphCache();
void scrollViewport(uint64_t oldStart, bool refetched);
/// Scroll by whole rows, positive moves the view down
@@ -400,6 +437,7 @@ private:
void moveCursor(int offset, bool select = false);
void setCursorAddr(BasicCursor addr, bool select = false);
void updateCursorMeta();
void updateMouseCursor(const QPoint &pos);
void setCursorOnAscii(bool ascii);
bool isItemDifferentAt(uint64_t address);
const QColor itemColor(uint8_t byte);
@@ -471,7 +509,7 @@ private:
inline qreal areaSpacingWidth() const { return areaSpacing * charWidth; }
inline uint64_t lastVisibleAddr() const { return (startAddress - 1) + bytesPerScreen(); }
uint64_t lastVisibleAddr() const;
const QRectF &currentArea() const { return cursorOnAscii ? asciiArea : itemArea; }
@@ -545,6 +583,7 @@ private:
QList<QAction *> actionsItemFormat;
QAction *actionItemBigEndian;
QAction *actionHexPairs;
QAction *actionSparse;
QAction *actionCopy;
QAction *actionCopyAddress;
QAction *actionCopyAsCString;
@@ -613,6 +652,10 @@ private:
void writeNumber(int byteCount);
QString statusBarText;
QVector<FlagBackgroundRange> flagBackgroundRanges;
QVector<BasicBlockBackgroundRange> basicBlockBackgroundRanges;
bool sparse = false;
bool repaintCollapseButtons = false;
std::vector<SparseHexLayout::Row> sparseRows;
};
#endif // HEXWIDGET_H
+33
View File
@@ -9,6 +9,7 @@
#include "core/MainWindow.h"
#include <cstring>
#include <functional>
#include <QByteArray>
#include <QClipboard>
#include <QCoreApplication>
@@ -28,6 +29,15 @@
#include <QTextOption>
#include <QtEndian>
namespace {
std::function<void(HexdumpWidget *)> hexdumpCustomize;
}
void HexdumpWidget::setCustomizeHook(const std::function<void(HexdumpWidget *)> &hook)
{
hexdumpCustomize = hook;
}
HexdumpWidget::HexdumpWidget(MainWindow *main)
: MemoryDockWidget(MemoryWidgetType::Hexdump, main)
, ui(new Ui::HexdumpWidget)
@@ -166,6 +176,26 @@ HexdumpWidget::HexdumpWidget(MainWindow *main)
this->ui->hexTextView->addAction(&syncAction);
connect(
ui->hexTextView,
&HexWidget::contextMenuAboutToShow,
this,
[this](QMenu *menu, RVA start, RVA end) {
menu->addSeparator();
menu->addAction(
start == end ? tr("Analyze data here") : tr("Analyze selected data"),
this,
[this, start, end]() {
const RVA last = start == end ? start
+ qMin<RVA>(
qMax<RVA>(1, Core()->core()->blocksize),
RVA_MAX - start)
- 1
: end;
mainWindow->showDataAnalysis(start, last);
});
});
connect(Config(), &Configuration::fontsUpdated, this, &HexdumpWidget::fontsUpdated);
connect(Core(), &IaitoCore::refreshAll, this, [this]() { refresh(); });
connect(Core(), &IaitoCore::refreshCodeViews, this, [this]() { refresh(); });
@@ -240,6 +270,9 @@ HexdumpWidget::HexdumpWidget(MainWindow *main)
make_nav("hex.navUpSelect", Qt::Key_Up, Qt::ShiftModifier);
make_nav("hex.navRightSelect", Qt::Key_Right, Qt::ShiftModifier);
}
if (hexdumpCustomize) {
hexdumpCustomize(this);
}
}
// Event filter to catch resize events on the side tabs and refresh values
+2
View File
@@ -7,6 +7,7 @@
#include <QTextEdit>
#include <array>
#include <functional>
#include <memory>
#include "HexWidget.h"
@@ -37,6 +38,7 @@ public:
Highlighter *highlighter;
static QString getWidgetType();
static void setCustomizeHook(const std::function<void(HexdumpWidget *)> &hook);
public slots:
void initParsing();
+4 -1
View File
@@ -128,6 +128,9 @@ ImportsProxyModel::ImportsProxyModel(ImportsModel *sourceModel, QObject *parent)
bool ImportsProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
if (!filterAcceptsType(row, parent)) {
return false;
}
QModelIndex index = sourceModel()->index(row, 0, parent);
auto import = index.data(ImportsModel::ImportDescriptionRole).value<ImportDescription>();
return import.name.contains(FILTER_REGEX);
@@ -199,7 +202,7 @@ ImportsWidget::ImportsWidget(MainWindow *main)
importsModel = new ImportsModel(&imports, this);
importsProxyModel = new ImportsProxyModel(importsModel, this);
setModels(importsProxyModel);
setModels(importsProxyModel, ImportsModel::NameColumn, ImportsModel::TypeColumn);
// Sort by library name by default to create a solid context per each group
// of imports
ui->treeView->sortByColumn(ImportsModel::LibraryColumn, Qt::AscendingOrder);
+20 -4
View File
@@ -1,4 +1,5 @@
#include "ListDockWidget.h"
#include "NameListView.h"
#include "common/Helpers.h"
#include "common/ShortcutManager.h"
#include "core/MainWindow.h"
@@ -69,27 +70,42 @@ void ListDockWidget::setStatusBarSizeGripEnabled(bool enabled)
tree->setStatusBarSizeGripEnabled(enabled);
}
void ListDockWidget::setModels(AddressableFilterProxyModel *objectFilterProxyModel)
void ListDockWidget::setModels(
AddressableFilterProxyModel *objectFilterProxyModel, int nameColumn, int typeColumn)
{
this->objectFilterProxyModel = objectFilterProxyModel;
if (typeColumn >= 0) {
ui->quickFilterView->setTypeFilter(objectFilterProxyModel, typeColumn);
}
// AddressableFilterProxyModel inherits both AddressableItemModelI and
// QSortFilterProxyModel, so pin the QAbstractItemModel overload
// explicitly against AddressableItemList::setModel(AddressableItemModelI *).
ui->treeView->setModel(static_cast<QAbstractItemModel *>(objectFilterProxyModel));
if (nameColumn >= 0) {
auto names = new NameListView(
ui->treeView,
ui->treeView->getItemContextMenu(),
objectFilterProxyModel,
nameColumn,
objectName());
ui->quickFilterView->addActionButton(names->toggleAction());
}
connect(
ui->quickFilterView,
&QuickFilterView::filterTextChanged,
objectFilterProxyModel,
&QSortFilterProxyModel::setFilterWildcard);
&AddressableFilterProxyModel::setFilterWildcard);
connect(
ui->quickFilterView,
&QuickFilterView::filterClosed,
ui->treeView,
static_cast<void (QWidget::*)()>(&QWidget::setFocus));
connect(ui->quickFilterView, &QuickFilterView::filterTextChanged, this, [this] {
const auto updateCount = [this] {
tree->showItemsNumber(this->objectFilterProxyModel->rowCount());
});
};
connect(ui->quickFilterView, &QuickFilterView::filterTextChanged, this, updateCount);
connect(ui->quickFilterView, &QuickFilterView::filterTypeChanged, this, updateCount);
}
+4 -1
View File
@@ -38,7 +38,10 @@ public:
void showCount(bool show);
protected:
void setModels(AddressableFilterProxyModel *objectFilterProxyModel);
void setModels(
AddressableFilterProxyModel *objectFilterProxyModel,
int nameColumn = -1,
int typeColumn = -1);
void setStatusBarSizeGripEnabled(bool enabled);
std::unique_ptr<Ui::ListDockWidget> ui;
+161
View File
@@ -0,0 +1,161 @@
#include "NameListView.h"
#include "common/NameListModel.h"
#include <QAction>
#include <QGuiApplication>
#include <QHeaderView>
#include <QMenu>
#include <QSettings>
#include <QSortFilterProxyModel>
#include <QTreeView>
NameListView::NameListView(
QTreeView *view,
QMenu *menu,
AddressableItemModelI *source,
int nameColumn,
const QString &settingsKey)
: QObject(view)
, view(view)
, model(new NameListModel(source, nameColumn, this))
, nameColumn(nameColumn)
, flatNamePosition(view->header()->visualIndex(nameColumn))
, flatIndentation(view->indentation())
, settingsKey(QStringLiteral("listNames/") + settingsKey)
{
view->setModel(model);
view->setExpandsOnDoubleClick(false);
connect(view, &QTreeView::pressed, this, [view](const QModelIndex &index) {
const auto row = index.sibling(index.row(), 0);
if (QGuiApplication::mouseButtons().testFlag(Qt::LeftButton)
&& view->model()->hasChildren(row)) {
view->expand(row);
}
});
menu->addSeparator();
auto compact = menu->addAction(tr("Compact names"));
compact->setObjectName(QStringLiteral("actionCompactNames"));
compact->setCheckable(true);
compact->setToolTip(tr("Replace a repeated name prefix with ▶. Hover to see the full name."));
auto hierarchy = menu->addAction(tr("Name hierarchy"));
hierarchyAction = hierarchy;
hierarchy->setObjectName(QStringLiteral("actionNameHierarchy"));
hierarchy->setCheckable(true);
auto expand = menu->addAction(tr("Expand all"), view, &QTreeView::expandAll);
auto collapse = menu->addAction(tr("Collapse all"), view, &QTreeView::collapseAll);
for (auto action : {compact, hierarchy, expand, collapse}) {
action->setProperty("addressIndependent", true);
}
QSettings settings;
compact->setChecked(settings.value(this->settingsKey + "/compact", false).toBool());
hierarchy->setChecked(settings.value(this->settingsKey + "/hierarchy", false).toBool());
auto updateToggle = [hierarchy] {
hierarchy->setIcon(QIcon(
hierarchy->isChecked() ? ":/img/icons/name-list.svg" : ":/img/icons/name-tree.svg"));
hierarchy->setToolTip(
hierarchy->isChecked() ? tr("Switch to flat list") : tr("Switch to name hierarchy"));
};
updateToggle();
connect(hierarchy, &QAction::toggled, this, updateToggle);
model->setCompact(compact->isChecked());
model->setHierarchy(hierarchy->isChecked());
updateHierarchy();
if (compact->isChecked() || hierarchy->isChecked()) {
view->setColumnWidth(nameColumn, 360);
}
connect(compact, &QAction::toggled, this, [this](bool enabled) {
model->setCompact(enabled);
QSettings().setValue(this->settingsKey + "/compact", enabled);
if (enabled) {
this->view->setColumnWidth(this->nameColumn, 360);
}
});
connect(hierarchy, &QAction::toggled, this, [this](bool enabled) {
if (enabled) {
flatNamePosition = this->view->header()->visualIndex(this->nameColumn);
flatIndentation = this->view->indentation();
}
model->setHierarchy(enabled);
updateHierarchy();
QSettings().setValue(this->settingsKey + "/hierarchy", enabled);
if (enabled) {
this->view->setColumnWidth(this->nameColumn, 360);
}
});
connect(view, &QTreeView::expanded, this, [this](const QModelIndex &index) {
const QString path = index.data(NameListModel::GroupPathRole).toString();
if (!restoring && !path.isEmpty()) {
expandedGroups.insert(path);
} else if (!restoring) {
const QPersistentModelIndex source = model->mapToSource(index);
if (source.isValid() && !expandedSources.contains(source)) {
expandedSources.append(source);
}
}
});
connect(view, &QTreeView::collapsed, this, [this](const QModelIndex &index) {
if (!restoring) {
expandedGroups.remove(index.data(NameListModel::GroupPathRole).toString());
expandedSources.removeAll(model->mapToSource(index));
}
});
connect(model, &QAbstractItemModel::modelAboutToBeReset, this, [this] {
currentSource = model->mapToSource(this->view->currentIndex());
});
connect(model, &QAbstractItemModel::modelReset, this, &NameListView::restoreView);
}
void NameListView::updateHierarchy()
{
const int position = view->header()->visualIndex(nameColumn);
view->header()->moveSection(position, model->isHierarchy() ? 0 : flatNamePosition);
view->setTreePosition(model->isHierarchy() ? nameColumn : 0);
view->setIndentation(model->isHierarchy() ? 20 : flatIndentation);
}
void NameListView::restoreView()
{
restoring = true;
for (auto it = expandedSources.begin(); it != expandedSources.end();) {
if (!it->isValid()) {
it = expandedSources.erase(it);
} else {
const auto index = model->mapFromSource(*it);
view->setExpanded(index, true);
for (auto parent = index.parent(); parent.isValid(); parent = parent.parent()) {
view->setExpanded(parent, true);
}
++it;
}
}
bool filtered = false;
if (auto source = qobject_cast<QSortFilterProxyModel *>(model->sourceModel())) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
filtered = !source->filterRegularExpression().pattern().isEmpty();
#else
filtered = !source->filterRegExp().pattern().isEmpty();
#endif
}
if (auto source = dynamic_cast<AddressableFilterProxyModel *>(model->sourceModel())) {
filtered = filtered || source->typeFilter().isValid();
}
if (filtered) {
view->expandAll();
}
if (model->isHierarchy() && !filtered) {
for (const QString &path : expandedGroups) {
view->setExpanded(model->groupIndex(path), true);
}
}
auto current = model->mapFromSource(currentSource);
if (current.isValid()) {
for (auto parent = current.parent(); parent.isValid(); parent = parent.parent()) {
view->setExpanded(parent, true);
}
view->setCurrentIndex(current);
view->scrollTo(current);
}
restoring = false;
}
+43
View File
@@ -0,0 +1,43 @@
#ifndef NAMELISTVIEW_H
#define NAMELISTVIEW_H
#include <QObject>
#include <QPersistentModelIndex>
#include <QSet>
class QTreeView;
class QMenu;
class QAction;
class NameListModel;
class AddressableItemModelI;
class NameListView : public QObject
{
public:
NameListView(
QTreeView *view,
QMenu *menu,
AddressableItemModelI *source,
int nameColumn,
const QString &settingsKey);
QAction *toggleAction() const { return hierarchyAction; }
NameListModel *presentationModel() const { return model; }
private:
QTreeView *view;
NameListModel *model;
QAction *hierarchyAction;
int nameColumn;
int flatNamePosition;
int flatIndentation;
QString settingsKey;
QSet<QString> expandedGroups;
QList<QPersistentModelIndex> expandedSources;
QPersistentModelIndex currentSource;
bool restoring = false;
void updateHierarchy();
void restoreView();
};
#endif
+13 -4
View File
@@ -75,12 +75,21 @@ void OverviewView::drawBlock(QPainter &p, GraphView::GraphBlock &block, bool int
p.setBrush(QColor(0, 0, 0, 100));
p.drawRect(blockRect.translated(2, 2));
// Draw basic block highlighting/tracing
// Draw basic block highlighting/tracing and radare2's abc colors.
QColor blockColor;
auto bb = Core()->getBBHighlighter()->getBasicBlock(block.entry);
if (bb) {
QColor color(bb->color);
color.setAlphaF(0.5);
p.setBrush(color);
blockColor = bb->color;
} else {
auto core = Core()->core();
RAnalBlock *analBlock = r_anal_get_block_at(core->anal, block.entry);
if (analBlock && (analBlock->color.r || analBlock->color.g || analBlock->color.b)) {
blockColor = QColor(analBlock->color.r, analBlock->color.g, analBlock->color.b);
}
}
if (blockColor.isValid()) {
blockColor.setAlphaF(0.5);
p.setBrush(blockColor);
} else {
p.setBrush(disassemblyBackgroundColor);
}
+131 -3
View File
@@ -1,15 +1,37 @@
#include "QuickFilterView.h"
#include "common/AddressableItemModel.h"
#include "ui_QuickFilterView.h"
#include <QSet>
#include <QSignalBlocker>
#include <QToolButton>
QuickFilterView::QuickFilterView(QWidget *parent, bool defaultOn)
: QWidget(parent)
, ui(new Ui::QuickFilterView())
{
ui->setupUi(this);
filterTimer.setSingleShot(true);
filterTimer.setInterval(150);
connect(&filterTimer, &QTimer::timeout, this, [this] {
emit filterTextChanged(ui->filterLineEdit->text());
});
connect(ui->filterLineEdit, &QLineEdit::textChanged, this, [this](const QString &text) {
emit filterTextChanged(text);
if (text.isEmpty()) {
filterTimer.stop();
emit filterTextChanged(text);
} else {
filterTimer.start();
}
});
connect(ui->typeFilterComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this] {
const QVariant type = ui->typeFilterComboBox->currentData();
if (typeModel) {
typeModel->setTypeFilter(type);
}
emit filterTypeChanged(type);
});
if (!defaultOn) {
@@ -19,24 +41,130 @@ QuickFilterView::QuickFilterView(QWidget *parent, bool defaultOn)
QuickFilterView::~QuickFilterView() {}
void QuickFilterView::setTypeFilter(AddressableFilterProxyModel *model, int column)
{
disconnect(sourceChangedConnection);
disconnect(modelDestroyedConnection);
if (typeModel && typeModel != model) {
typeModel->setTypeFilter({});
typeModel->setTypeFilterColumn(-1);
}
typeModel = model;
typeColumn = column;
if (model) {
model->setTypeFilterColumn(column);
sourceChangedConnection = connect(
model, &QAbstractProxyModel::sourceModelChanged, this, &QuickFilterView::bindTypeSource);
modelDestroyedConnection = connect(model, &QObject::destroyed, this, [this] {
setTypeFilter(nullptr, -1);
});
}
bindTypeSource();
ui->typeFilterComboBox->setVisible(model && column >= 0 && !ui->filterLineEdit->isHidden());
}
void QuickFilterView::bindTypeSource()
{
for (const auto &connection : typeConnections) {
disconnect(connection);
}
typeConnections.clear();
typeSource = typeModel ? typeModel->sourceModel() : nullptr;
if (typeSource) {
typeConnections << connect(
typeSource, &QAbstractItemModel::modelReset, this, &QuickFilterView::updateTypes);
typeConnections << connect(
typeSource, &QAbstractItemModel::rowsInserted, this, &QuickFilterView::updateTypes);
typeConnections << connect(
typeSource, &QAbstractItemModel::rowsRemoved, this, &QuickFilterView::updateTypes);
typeConnections << connect(
typeSource, &QAbstractItemModel::layoutChanged, this, &QuickFilterView::updateTypes);
typeConnections << connect(
typeSource,
&QAbstractItemModel::dataChanged,
this,
[this](const QModelIndex &first, const QModelIndex &last) {
if (first.column() <= typeColumn && last.column() >= typeColumn) {
updateTypes();
}
});
typeConnections << connect(
typeSource, &QObject::destroyed, this, &QuickFilterView::updateTypes);
}
updateTypes();
}
void QuickFilterView::updateTypes()
{
QSet<QString> values;
if (typeSource && typeColumn >= 0) {
const auto collect = [&](const auto &self, const QModelIndex &parent) -> void {
for (int row = 0; row < typeSource->rowCount(parent); ++row) {
values.insert(typeSource->index(row, typeColumn, parent).data().toString());
const auto child = typeSource->index(row, 0, parent);
if (typeSource->hasChildren(child)) {
self(self, child);
}
}
};
collect(collect, {});
}
QStringList types = values.values();
types.sort();
const QVariant selected = typeModel ? typeModel->typeFilter() : QVariant();
const int selectedIndex = selected.isValid() ? types.indexOf(selected.toString()) + 1 : 0;
{
const QSignalBlocker blocker(ui->typeFilterComboBox);
ui->typeFilterComboBox->clear();
ui->typeFilterComboBox->addItem(tr("All types"), QVariant());
for (const QString &type : types) {
ui->typeFilterComboBox->addItem(type.isEmpty() ? tr("(No type)") : type, type);
}
ui->typeFilterComboBox->setCurrentIndex(selectedIndex);
ui->typeFilterComboBox->setEnabled(!types.isEmpty());
}
if (selected.isValid() && !selectedIndex && typeModel) {
typeModel->setTypeFilter({});
emit filterTypeChanged({});
}
}
void QuickFilterView::addActionButton(QAction *action)
{
auto button = new QToolButton(this);
button->setAutoRaise(true);
button->setDefaultAction(action);
ui->horizontalLayout->addWidget(button, 0, Qt::AlignRight);
hasActions = true;
show();
}
void QuickFilterView::showFilter()
{
show();
ui->filterLineEdit->show();
ui->typeFilterComboBox->setVisible(typeModel && typeColumn >= 0);
ui->filterLineEdit->setFocus();
}
void QuickFilterView::clearFilter()
{
if (ui->filterLineEdit->text().isEmpty()) {
if (ui->filterLineEdit->text().isEmpty() && ui->typeFilterComboBox->currentIndex() <= 0) {
closeFilter();
} else {
ui->filterLineEdit->setText("");
ui->typeFilterComboBox->setCurrentIndex(0);
}
}
void QuickFilterView::closeFilter()
{
ui->filterLineEdit->setText("");
hide();
ui->typeFilterComboBox->setCurrentIndex(0);
ui->filterLineEdit->hide();
ui->typeFilterComboBox->hide();
if (!hasActions) {
hide();
}
emit filterClosed();
}
+20
View File
@@ -6,12 +6,18 @@
#include <memory>
#include <QPointer>
#include <QTimer>
#include <QWidget>
namespace Ui {
class QuickFilterView;
}
class QAction;
class AddressableFilterProxyModel;
class QAbstractItemModel;
class IAITO_EXPORT QuickFilterView : public QWidget
{
Q_OBJECT
@@ -19,6 +25,8 @@ class IAITO_EXPORT QuickFilterView : public QWidget
public:
explicit QuickFilterView(QWidget *parent = nullptr, bool defaultOn = true);
~QuickFilterView();
void addActionButton(QAction *action);
void setTypeFilter(AddressableFilterProxyModel *model, int column);
public slots:
void showFilter();
@@ -27,10 +35,22 @@ public slots:
signals:
void filterTextChanged(const QString &text);
void filterTypeChanged(const QVariant &type);
void filterClosed();
private:
std::unique_ptr<Ui::QuickFilterView> ui;
bool hasActions = false;
QTimer filterTimer;
int typeColumn = -1;
QPointer<AddressableFilterProxyModel> typeModel;
QPointer<QAbstractItemModel> typeSource;
QList<QMetaObject::Connection> typeConnections;
QMetaObject::Connection sourceChangedConnection;
QMetaObject::Connection modelDestroyedConnection;
void bindTypeSource();
void updateTypes();
};
#endif // QUICKFILTERVIEW_H
+19
View File
@@ -45,6 +45,25 @@
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="typeFilterComboBox">
<property name="visible">
<bool>false</bool>
</property>
<property name="maximumSize">
<size><width>200</width><height>16777215</height></size>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
<property name="toolTip">
<string>Filter by type</string>
</property>
<property name="accessibleName">
<string>Type filter</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
+4 -1
View File
@@ -111,6 +111,9 @@ RefsProxyModel::RefsProxyModel(RefsListModel *sourceModel, QObject *parent)
bool RefsProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
if (!filterAcceptsType(row, parent)) {
return false;
}
QModelIndex index = sourceModel()->index(row, 0, parent);
auto ref = index.data(RefsListModel::XrefDescriptionRole).value<XrefDescription>();
if (ref.from_str.contains(FILTER_REGEX) || ref.to_str.contains(FILTER_REGEX)
@@ -154,7 +157,7 @@ RefsWidget::RefsWidget(MainWindow *main)
refsModel = new RefsListModel(&refs, this);
refsProxyModel = new RefsProxyModel(refsModel, this);
setModels(refsProxyModel);
setModels(refsProxyModel, -1, RefsListModel::TypeColumn);
ui->treeView->sortByColumn(RefsListModel::FromColumn, Qt::AscendingOrder);
connect(Core(), &IaitoCore::seekChanged, this, &RefsWidget::refreshRefs);
+4 -1
View File
@@ -125,6 +125,9 @@ RelocsProxyModel::RelocsProxyModel(RelocsModel *sourceModel, QObject *parent)
bool RelocsProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
if (!filterAcceptsType(row, parent)) {
return false;
}
QModelIndex index = sourceModel()->index(row, 0, parent);
auto reloc = index.data(RelocsModel::RelocDescriptionRole).value<RelocDescription>();
@@ -175,7 +178,7 @@ RelocsWidget::RelocsWidget(MainWindow *main)
relocsModel = new RelocsModel(&relocs, this);
relocsProxyModel = new RelocsProxyModel(relocsModel, this);
setModels(relocsProxyModel);
setModels(relocsProxyModel, -1, RelocsModel::TypeColumn);
ui->treeView->sortByColumn(RelocsModel::NameColumn, Qt::AscendingOrder);
connect(Core(), &IaitoCore::codeRebased, this, &RelocsWidget::refreshRelocs);
+4 -1
View File
@@ -184,6 +184,9 @@ ResourcesProxyModel::ResourcesProxyModel(ResourcesModel *sourceModel, QObject *p
bool ResourcesProxyModel::filterAcceptsRow(int row, const QModelIndex &parent) const
{
if (!filterAcceptsType(row, parent)) {
return false;
}
const QModelIndex index = sourceModel()->index(row, ResourcesModel::NameColumn, parent);
const auto resource
= index.data(ResourcesModel::ResourceDescriptionRole).value<ResourcesDescription>();
@@ -198,7 +201,7 @@ ResourcesWidget::ResourcesWidget(MainWindow *main)
model = new ResourcesModel(&resources, this);
filterModel = new ResourcesProxyModel(model, this);
setModels(filterModel);
setModels(filterModel, -1, ResourcesModel::TypeColumn);
ui->treeView->sortByColumn(ResourcesModel::NameColumn, Qt::AscendingOrder);
showCount(false);
+14
View File
@@ -184,6 +184,20 @@ void SectionsWidget::initSectionsTable()
setModels(proxyModel);
ui->treeView->sortByColumn(SectionsModel::AddressColumn, Qt::AscendingOrder);
ui->treeView->getItemContextMenu()->addAction(tr("Analyze section data"), this, [this]() {
const QModelIndex index = ui->treeView->currentIndex();
if (!index.isValid()) {
return;
}
const auto section
= index.data(SectionsModel::SectionDescriptionRole).value<SectionDescription>();
const bool va = Core()->getConfigb("io.va");
const RVA start = va ? section.vaddr : section.paddr;
const RVA size = qMin(va ? section.vsize : section.size, RVA_MAX - start);
if (size) {
mainWindow->showDataAnalysis(start, start + size - 1);
}
});
}
void SectionsWidget::initQuickFilter()
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff. Show more