mirror of
https://github.com/radareorg/iaito.git
synced 2026-11-07 06:25:59 -10:00
Introduce the advanced DataAnalysis widget
This commit is contained in:
1 parent
11240a1a0f
commit
a7532b4d80
19 files changed
+2139
-4
No files matched your search
@@ -3,6 +3,9 @@ AStyle*
|
||||
./config.mk
|
||||
config.mk
|
||||
|
||||
# Generated configuration header (qmake / Meson)
|
||||
IaitoConfig.h
|
||||
|
||||
# OSX files
|
||||
*.DS_Store
|
||||
.AppleDouble
|
||||
|
||||
@@ -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
|
||||
```
|
||||
+9
-2
@@ -359,7 +359,10 @@ 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
|
||||
@@ -576,11 +579,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 \
|
||||
|
||||
@@ -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
|
||||
+33
-1
@@ -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"
|
||||
@@ -1208,6 +1209,7 @@ void MainWindow::initUI()
|
||||
connectMenuStatusTips(ui->menuEdit);
|
||||
connectMenuStatusTips(ui->menuCode);
|
||||
connectMenuStatusTips(ui->menuAnalysis);
|
||||
connectMenuStatusTips(ui->menuData);
|
||||
connectMenuStatusTips(ui->menuTools);
|
||||
connectMenuStatusTips(ui->menuPlugins);
|
||||
|
||||
@@ -1578,8 +1580,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 +1608,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 +1634,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 +1688,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 +2471,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)) {
|
||||
@@ -2547,6 +2560,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 +2608,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 +3229,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"/>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
#include <QAbstractItemModel>
|
||||
#include <QAbstractItemView>
|
||||
#include <QMenu>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QSet>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "IaitoDockWidget.h"
|
||||
#include "IaitoTreeView.h"
|
||||
|
||||
@@ -0,0 +1,632 @@
|
||||
#include "DataAnalysisView.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QLinearGradient>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#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);
|
||||
setMinimumSize(220, overview ? 70 : 180);
|
||||
setAccessibleName(overview ? tr("Data overview") : tr("Data analysis graph"));
|
||||
setAccessibleDescription(
|
||||
overview ? tr("Drag the window to pan; drag its edges to resize. Double-click to fit.")
|
||||
: tr("Click to seek; drag to select bytes; wheel to zoom; Shift+wheel to pan."
|
||||
" 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"));
|
||||
}
|
||||
}
|
||||
|
||||
void DataAnalysisView::zoom(double factor, RVA anchor)
|
||||
{
|
||||
if (!domain.valid() || !visibleRange.valid()) {
|
||||
return;
|
||||
}
|
||||
const long double scaled = (long double) visibleRange.size() * factor;
|
||||
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();
|
||||
if (!event->angleDelta().y()) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
if (event->modifiers() & Qt::ShiftModifier) {
|
||||
pan(event->angleDelta().y() > 0 ? -1 : 1);
|
||||
} else {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
const qreal x = event->position().x();
|
||||
#else
|
||||
const qreal x = event->posF().x();
|
||||
#endif
|
||||
zoom(event->angleDelta().y() > 0 ? 0.5 : 2.0, addressAt(x));
|
||||
}
|
||||
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,86 @@
|
||||
#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:
|
||||
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 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 to zoom · Shift+wheel to pan</string></property>
|
||||
<property name="textInteractionFlags"><set>Qt::TextSelectableByMouse</set></property>
|
||||
</widget></item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1655,6 +1655,11 @@ void HexWidget::contextMenuEvent(QContextMenuEvent *event)
|
||||
buildInsertMenu(menu);
|
||||
addSyncOffsetActions(menu);
|
||||
|
||||
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);
|
||||
|
||||
@@ -331,6 +331,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;
|
||||
|
||||
@@ -166,6 +166,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(); });
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -140,6 +140,21 @@ SegmentsWidget::SegmentsWidget(MainWindow *main)
|
||||
ui->quickFilterView->closeFilter();
|
||||
showCount(false);
|
||||
|
||||
ui->treeView->getItemContextMenu()->addAction(tr("Analyze segment data"), this, [this]() {
|
||||
const QModelIndex index = ui->treeView->currentIndex();
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
const auto segment
|
||||
= index.data(SegmentsModel::SegmentDescriptionRole).value<SegmentDescription>();
|
||||
const bool va = Core()->getConfigb("io.va");
|
||||
const RVA start = va ? segment.vaddr : segment.paddr;
|
||||
const RVA size = qMin(va ? segment.vsize : segment.size, RVA_MAX - start);
|
||||
if (size) {
|
||||
mainWindow->showDataAnalysis(start, start + size - 1);
|
||||
}
|
||||
});
|
||||
|
||||
connect(Core(), &IaitoCore::refreshAll, this, &SegmentsWidget::refreshSegments);
|
||||
connect(Core(), &IaitoCore::codeRebased, this, &SegmentsWidget::refreshSegments);
|
||||
connect(Core(), &IaitoCore::commentsChanged, this, [this]() {
|
||||
|
||||
Reference in new issue
Block a user