mirror of
https://github.com/radareorg/iaito.git
synced 2026-11-10 17:40:38 -10:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18d51daa6a | ||
|
|
f20ddbc357 | ||
|
|
c47985f1b0 | ||
|
|
0e5a49c331 | ||
|
|
74c12685a0 | ||
|
|
93af6edfe9 | ||
|
|
977578a91e | ||
|
|
e980b1cb48 | ||
|
|
b5edef36bc | ||
|
|
1208624c77 | ||
|
|
eff435fc29 | ||
|
|
af1f536b0f | ||
|
|
6affec227d | ||
|
|
e0a120ad08 | ||
|
|
3383cc1d92 | ||
|
|
7eade2df0c | ||
|
|
bd1105f45d | ||
|
|
a7532b4d80 | ||
|
|
11240a1a0f | ||
|
|
ddb7bdb428 | ||
|
|
b912778e62 | ||
|
|
dc1fde07f5 | ||
|
|
1e089d9c37 | ||
|
|
e0e9690fd5 |
No files matched your search
@@ -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,6 +3,9 @@ AStyle*
|
||||
./config.mk
|
||||
config.mk
|
||||
|
||||
# Generated configuration header (qmake / Meson)
|
||||
IaitoConfig.h
|
||||
|
||||
# OSX files
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
PKGNAME iaito
|
||||
VERSION 6.2.2
|
||||
VERSION 6.2.4
|
||||
CONTACT pancake ; [email protected]
|
||||
|
||||
LANG_CXX!
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> ®ions, const QJsonArray &blocks)
|
||||
{
|
||||
QList<ExceptionRegion> result;
|
||||
for (const auto ®ion : 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;
|
||||
}
|
||||
@@ -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> ®ions, const QJsonArray &blocks);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ private slots:
|
||||
|
||||
private:
|
||||
QLineEdit *m_filterLineEdit;
|
||||
QCheckBox *m_binaryPackagesCheckBox;
|
||||
QCheckBox *m_showAllPlatformsCheckBox;
|
||||
QTableWidget *m_tableWidget;
|
||||
QPushButton *m_refreshButton;
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
|
||||
class FridaSession;
|
||||
class QWidget;
|
||||
|
||||
QList<QWidget *> createFridaPanels(FridaSession *session);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 ¤t)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -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 ¤t);
|
||||
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);
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
@@ -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 ®ion : 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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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(">");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
|
||||
@@ -183,6 +183,7 @@ private:
|
||||
void updateCursorPosition();
|
||||
|
||||
QString getWindowTitle() const override;
|
||||
QWidget *widgetToFocusOnRaise() override;
|
||||
|
||||
/**
|
||||
* @brief Event filter that intercept the following events:
|
||||
|
||||
@@ -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 ®ion : 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 ®ion : 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 ®ion : 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 ®ion : 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 ®ion : 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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 ®ion : 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 ®ion : 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());
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ®ion)
|
||||
{
|
||||
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 ®ion : 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 ®ion : 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 ®ion : regions) {
|
||||
if (region.touches(address, size)) {
|
||||
addRegion(menu, region);
|
||||
}
|
||||
}
|
||||
menu->menuAction()->setVisible(!menu->isEmpty());
|
||||
}
|
||||
@@ -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> ®ions, 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 ®ion);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -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
@@ -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>();
|
||||
|
||||
@@ -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<></class>
|
||||
<extends>QTreeView</extends>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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 ¤tArea() 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
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
Reference in new issue
Block a user