Introduce the advanced DataAnalysis widget

This commit is contained in:
pancake committed 2026-09-29 21:30:37 +02:00
1 parent 11240a1a0f
commit a7532b4d80
19 files changed
+2139 -4

No files matched your search

+3
View File
@@ -3,6 +3,9 @@ AStyle*
./config.mk
config.mk
# Generated configuration header (qmake / Meson)
IaitoConfig.h
# OSX files
*.DS_Store
.AppleDouble
+50
View File
@@ -0,0 +1,50 @@
# Data Analysis
Open **View → Data → Analysis**. The dock is hidden in the default layout and when
first added to an older saved layout. Hidden docks do not sample data. **Zoom** is
also available under **View → Data**.
- **Algorithms** enables multiple byte metrics. Hover over a named legend entry
for its definition; click it to edit its color or visibility. Clicking the legend
does not remove the series.
- **Options → Resolution (buckets)** selects 32, 64, 128, 256, 512, 1024, 2048 or
4096 buckets. Lower values give coarser graphs and less work. The default is 1024.
- **Options** also controls lines, filled lines or vertical bars, separate tracks,
greyscale/rainbow/theme colors, custom colors, and structural markers.
- Click the graph to seek. Drag to select bytes. Wheel or `+`/`-` zooms;
Shift+wheel or the arrow keys pans. `Home` fits all data, and `Esc` clears selection.
- The bottom entropy overview shows the complete address span. Drag its window
to pan, drag its edges to resize the range, or double-click to fit all data.
- **Range** fits the current section, segment, map, block or selection.
**Edit range** accepts radare2 expressions and inclusive endpoints.
- **Follow** keeps the current seek address visible. Offset synchronization and
the global address-selection synchronization setting control integration with
disassembly and hexdump widgets.
- **Find marker** searches sections, segments, maps, flags (including search hits)
and comments. The graph context menu provides comments, flags and “Show in”.
Hover details appear after the pointer rests for 650 ms.
- Hexdump selections, sections and segments have direct analysis actions.
Other addressable widgets can use **Show in → Data Analysis**.
The graph uses the current radare2 address space: virtual addresses with `io.va`
enabled, otherwise physical offsets in the active descriptor. Gaps and failed reads
do not contribute invented bytes. Each bucket reads at most 4096 bytes from centered
windows in readable spans. Tooltips, the status line and exported CSV report how
many bytes were actually read. Zoom in for complete coverage of a region. Entropy
is measured per bucket, so very small buckets have a lower attainable entropy.
Multiple overlaid series use a normalized percentage scale. Entropy's native scale
is 0–8 bits/byte, unique bytes 0–256, and mean byte 0–255. A single series or separate
tracks uses its native scale. CSV exports normalized values plus exact bucket bounds
and coverage. Display settings are saved with the layout.
Other widgets can call `MainWindow::showDataAnalysis(start, end)` or
`DataAnalysisWidget::showRange(start, end)` with **inclusive** endpoints. Internally,
`DataAnalysis::Range` and graph signals use half-open ranges.
Run the focused tests with:
```sh
make -C tests/dataanalysis -j8 > /dev/null
QT_QPA_PLATFORM=offscreen tests/dataanalysis/build/test_dataanalysis
```
+9 -2
View File
@@ -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 \
+122
View File
@@ -0,0 +1,122 @@
#include "DataAnalysis.h"
#include <algorithm>
#include <cmath>
namespace DataAnalysis {
Range sizedRange(RVA start, RVA size)
{
return {start, start + qMin(size, RVA_MAX - start)};
}
Range intersect(Range a, Range b)
{
const RVA start = qMax(a.start, b.start);
return {start, qMax(start, qMin(a.end, b.end))};
}
Range bucket(Range range, int index, int count)
{
if (!range.valid() || count <= 0 || index < 0 || index >= count) {
return {};
}
// Avoid both floating point address rounding and multiplication overflow.
auto boundary = [range, count](RVA i) {
return range.start + (range.size() / count) * i + (range.size() % count) * i / count;
};
return {boundary(index), boundary(index + 1)};
}
Range centered(Range domain, RVA address, RVA size)
{
if (!domain.valid()) {
return {};
}
size = qBound<RVA>(1, size, domain.size());
address = qBound(domain.start, address, domain.end - 1);
const RVA start = address - qMin(address - domain.start, size / 2);
const RVA clamped = qMin(start, domain.end - size);
return {clamped, clamped + size};
}
QVector<Range> mergeRanges(QVector<Range> ranges)
{
std::sort(ranges.begin(), ranges.end(), [](Range a, Range b) { return a.start < b.start; });
QVector<Range> result;
for (auto range : ranges) {
if (!range.valid()) {
continue;
}
if (result.isEmpty() || result.last().end < range.start) {
result.append(range);
} else {
result.last().end = qMax(result.last().end, range.end);
}
}
return result;
}
Sample sample(
Range range,
const QVector<Range> &readable,
const std::function<QByteArray(RVA, int)> &read,
int budget)
{
Sample result;
result.range = range;
if (!range.valid() || budget <= 0) {
return result;
}
QVector<Range> spans;
for (auto candidate : readable) {
const Range span = intersect(range, candidate);
if (span.valid()) {
spans.append(span);
result.covered += span.size();
}
}
std::array<int, 256> histogram{};
RVA remaining = result.covered;
for (auto span : spans) {
if (budget <= 0) {
break;
}
// Allocate the budget proportionally among disjoint readable spans.
const int share = qMax(1, int((long double) span.size() / remaining * budget));
const int length = int(qMin<RVA>(span.size(), qMin(budget, share)));
const RVA address = span.start + (span.size() - length) / 2;
const QByteArray data = read(address, length).left(length);
for (unsigned char byte : data) {
++histogram[byte];
}
result.bytes += data.size();
budget -= length;
remaining -= span.size();
}
if (!result.bytes) {
return result;
}
auto &v = result.values;
for (int byte = 0; byte < 256; ++byte) {
const int n = histogram[byte];
if (!n) {
continue;
}
const double p = double(n) / result.bytes;
v[Entropy] -= p * std::log2(p) / 8.0;
v[Printable] += (byte >= 32 && byte <= 126) ? p : 0;
v[Zero] += byte == 0 ? p : 0;
v[FF] += byte == 255 ? p : 0;
v[Unique] += 1.0 / 256;
v[Mean] += p * byte / 255;
v[Alpha] += ((byte >= 'a' && byte <= 'z') || (byte >= 'A' && byte <= 'Z')) ? p : 0;
v[Digit] += (byte >= '0' && byte <= '9') ? p : 0;
v[Space] += (byte == ' ' || (byte >= 9 && byte <= 13)) ? p : 0;
v[Control] += (byte < 32 || byte == 127) ? p : 0;
v[High] += byte >= 128 ? p : 0;
}
return result;
}
} // namespace DataAnalysis
+64
View File
@@ -0,0 +1,64 @@
#ifndef DATAANALYSIS_H
#define DATAANALYSIS_H
#include "core/IaitoCommon.h"
#include <array>
#include <functional>
#include <QByteArray>
#include <QVector>
namespace DataAnalysis {
// Half-open ranges, including when the exclusive end is RVA_MAX.
struct Range
{
RVA start = 0;
RVA end = 0;
bool valid() const { return start < end; }
RVA size() const { return end - start; }
bool contains(RVA address) const { return start <= address && address < end; }
bool operator==(const Range &other) const { return start == other.start && end == other.end; }
bool operator!=(const Range &other) const { return !(*this == other); }
};
enum Metric {
Entropy,
Printable,
Zero,
FF,
Unique,
Mean,
Alpha,
Digit,
Space,
Control,
High,
Count
};
struct Sample
{
Range range;
RVA covered = 0;
int bytes = 0;
std::array<double, Count> values{}; // Normalized to [0, 1]; entropy / 8.
};
Range sizedRange(RVA start, RVA size);
Range intersect(Range a, Range b);
Range bucket(Range range, int index, int count);
Range centered(Range domain, RVA address, RVA size);
QVector<Range> mergeRanges(QVector<Range> ranges);
// Bounded, deterministic sampling. Reads only known readable spans; missing bytes
// never contribute fabricated 0xff data. Large buckets use centered windows.
Sample sample(
Range range,
const QVector<Range> &readable,
const std::function<QByteArray(RVA, int)> &read,
int budget = 4096);
} // namespace DataAnalysis
#endif
+33 -1
View File
@@ -48,6 +48,7 @@
#include "widgets/ConsoleWidget.h"
#include "widgets/CustomCommandWidget.h"
#include "widgets/Dashboard.h"
#include "widgets/DataAnalysisWidget.h"
#include "widgets/DebugActions.h"
#include "widgets/DecompilerWidget.h"
#include "widgets/DisassemblerGraphView.h"
@@ -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
+3
View File
@@ -66,6 +66,7 @@ class FilesWidget;
class FilesystemWidget;
class BinariesWidget;
class ZoomWidget;
class DataAnalysisWidget;
class XrefsWidget;
class RefsWidget;
class CodeInfoWidget;
@@ -151,6 +152,7 @@ public:
QString getUniqueObjectName(const QString &widgetType) const;
void showMemoryWidget();
void showMemoryWidget(MemoryWidgetType type);
void showDataAnalysis(RVA start, RVA end);
void gotoOffset(const QString &offset);
enum class AddressTypeHint { Function, Data, Unknown };
QMenu *createShowInMenu(
@@ -366,6 +368,7 @@ private:
CallGraphWidget *callGraphDock = nullptr;
CallGraphWidget *globalCallGraphDock = nullptr;
ZoomWidget *zoomDock = nullptr;
DataAnalysisWidget *dataAnalysisDock = nullptr;
XrefsWidget *xrefsDock = nullptr;
RefsWidget *refsDock = nullptr;
CodeInfoWidget *codeInfoDock = nullptr;
+6
View File
@@ -130,9 +130,15 @@
<string>Analysis</string>
</property>
</widget>
<widget class="QMenu" name="menuData">
<property name="title">
<string>Data</string>
</property>
</widget>
<addaction name="actionHighlight"/>
<addaction name="separator"/>
<addaction name="menuAnalysis"/>
<addaction name="menuData"/>
<addaction name="menuAddInfoWidgets"/>
<addaction name="menuAddIoWidgets"/>
<addaction name="separator"/>
+1 -1
View File
@@ -5,8 +5,8 @@
#include <QAbstractItemModel>
#include <QAbstractItemView>
#include <QMenu>
#include <QSortFilterProxyModel>
#include <QSet>
#include <QSortFilterProxyModel>
#include "IaitoDockWidget.h"
#include "IaitoTreeView.h"
+632
View File
@@ -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();
}
}
+86
View File
@@ -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
+966
View File
@@ -0,0 +1,966 @@
#include "DataAnalysisWidget.h"
#include "common/IaitoSeekable.h"
#include "core/MainWindow.h"
#include "dialogs/CommentsDialog.h"
#include "dialogs/FlagDialog.h"
#include "ui_DataAnalysisWidget.h"
#include <QActionGroup>
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QColorDialog>
#include <QDialogButtonBox>
#include <QElapsedTimer>
#include <QFileDialog>
#include <QFormLayout>
#include <QJsonArray>
#include <QJsonObject>
#include <QLineEdit>
#include <QListWidget>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QSaveFile>
#include <QScrollArea>
#include <QSet>
#include <QTextStream>
#include <QToolButton>
#include <algorithm>
using namespace DataAnalysis;
namespace {
RVA number(const QJsonObject &object, const char *key)
{
return object[QLatin1String(key)].toVariant().toULongLong();
}
QByteArray readData(RVA address, int length)
{
auto core = Core()->core();
QByteArray bytes(length, '\0');
int count = 0;
while (count < length) {
// nread distinguishes short/failed reads from actual 0xff bytes. A read
// can end at a submap boundary even when the following map is readable.
const int n = r_io_nread_at(
core->io,
address + count,
reinterpret_cast<ut8 *>(bytes.data()) + count,
length - count);
if (n <= 0) {
break;
}
count += n;
}
bytes.resize(count);
return bytes;
}
} // namespace
DataAnalysisWidget::DataAnalysisWidget(MainWindow *main)
: AddressableDockWidget(main)
, ui(new Ui::DataAnalysisWidget)
{
setObjectName("DataAnalysisWidget");
updateWindowTitle();
auto *contents = new QWidget(this);
ui->setupUi(contents);
ui->legend->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
setWidget(contents);
plot = new DataAnalysisView(false, contents);
plot->setObjectName("analysisPlot");
overview = new DataAnalysisView(true, contents);
overview->setObjectName("analysisOverview");
overview->setFixedHeight(80);
auto *scroll = new QScrollArea(contents);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setWidget(plot);
ui->plotLayout->addWidget(scroll, 1);
ui->plotLayout->addWidget(overview);
ui->verticalLayout->setStretch(3, 1);
setupActions();
updateColors();
refreshDeferrer = createRefreshDeferrer([this]() { reloadTimer.start(); });
reloadTimer.setSingleShot(true);
reloadTimer.setInterval(100);
rangeTimer.setSingleShot(true);
rangeTimer.setInterval(80);
sampleTimer.setInterval(0);
connect(&reloadTimer, &QTimer::timeout, this, &DataAnalysisWidget::reload);
connect(&rangeTimer, &QTimer::timeout, this, [this]() {
if (!samplingOverview) {
beginSampling(false);
}
});
connect(&sampleTimer, &QTimer::timeout, this, &DataAnalysisWidget::sampleBatch);
connect(plot, &DataAnalysisView::seekRequested, seekable, &IaitoSeekable::seek);
connect(plot, &DataAnalysisView::selectionRequested, this, [this](RVA start, RVA end) {
selectRange({start, end}, true);
});
for (auto *view : {plot, overview}) {
connect(view, &DataAnalysisView::rangeRequested, this, [this](RVA start, RVA end) {
setRange({start, end});
});
connect(view, &DataAnalysisView::contextRequested, this, &DataAnalysisWidget::showContextMenu);
}
connect(seekable, &IaitoSeekable::seekableSeekChanged, this, &DataAnalysisWidget::onSeekChanged);
connect(Core(), &IaitoCore::addressRangeSelectionChanged, this, [this](RVA start, RVA end) {
if (!publishingSelection && seekable->isSynchronized()
&& Config()->getAddressRangeSelectionSyncEnabled()) {
selectRange(
start != RVA_INVALID && end != RVA_INVALID && end >= start ? Range{start, end + 1}
: Range{},
false);
}
});
connect(Core(), &IaitoCore::refreshAll, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::codeRebased, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::ioModeChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::ioCacheChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::flagsChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::commentsChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::refreshCodeViews, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::instructionChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Core(), &IaitoCore::debugTaskStateChanged, this, &DataAnalysisWidget::scheduleReload);
connect(Config(), &Configuration::colorsUpdated, this, &DataAnalysisWidget::updateColors);
connect(this, &QDockWidget::visibilityChanged, this, [this](bool visible) {
if (!visible) {
metadataDirty |= sampleTimer.isActive() || rangeTimer.isActive();
sampleTimer.stop();
rangeTimer.stop();
reloadTimer.stop();
} else if (metadataDirty) {
scheduleReload();
}
});
scheduleReload();
}
DataAnalysisWidget::~DataAnalysisWidget() = default;
QWidget *DataAnalysisWidget::widgetToFocusOnRaise()
{
return plot;
}
void DataAnalysisWidget::setupActions()
{
auto menuButton = [this](const QString &label, QMenu *menu) {
auto *button = new QToolButton(ui->toolbar);
button->setText(label);
button->setPopupMode(QToolButton::InstantPopup);
button->setMenu(menu);
ui->toolbar->addWidget(button);
};
auto *ranges = new QMenu(this);
ranges->addAction(tr("All data"), this, [this]() { setRange(plot->domain); });
ranges->addAction(tr("Current section"), this, [this]() {
fitMarker(DataAnalysisView::Section);
});
ranges->addAction(tr("Current segment"), this, [this]() {
fitMarker(DataAnalysisView::Segment);
});
ranges->addAction(tr("Current map"), this, [this]() { fitMarker(DataAnalysisView::Map); });
ranges->addAction(tr("Current block"), this, [this]() {
const int size = Core()->core()->blocksize;
setRange(sizedRange(seekable->getOffset(), qMax(1, size)));
});
selectionAction
= ranges->addAction(tr("Fit selection"), this, &DataAnalysisWidget::fitSelection);
selectionAction->setEnabled(false);
ranges->addSeparator();
ranges->addAction(tr("Edit range…"), this, &DataAnalysisWidget::editRange);
menuButton(tr("Range"), ranges);
auto *algorithms = new QMenu(this);
for (int i = 0; i < Count; ++i) {
auto *action = algorithms->addAction(DataAnalysisView::metricName(i));
action->setCheckable(true);
action->setChecked(plot->metrics.contains(i));
metricActions[i] = action;
action->setToolTip(DataAnalysisView::metricDescription(i));
auto *entry = ui->legend->addAction(DataAnalysisView::metricName(i));
entry->setToolTip(
DataAnalysisView::metricDescription(i) + tr("\nClick to configure this algorithm."));
legendActions[i] = entry;
connect(entry, &QAction::triggered, this, [this, i]() { configureMetric(i); });
connect(action, &QAction::toggled, this, &DataAnalysisWidget::updateMetrics);
}
menuButton(tr("Algorithms"), algorithms);
ui->toolbar
->addAction(
tr("+"),
this,
[this]() { plot->zoom(0.5, plot->visibleRange.start + plot->visibleRange.size() / 2); })
->setToolTip(tr("Zoom in (+)"));
ui->toolbar
->addAction(
tr("−"),
this,
[this]() { plot->zoom(2, plot->visibleRange.start + plot->visibleRange.size() / 2); })
->setToolTip(tr("Zoom out (-)"));
ui->toolbar->addAction(tr("Fit"), this, [this]() { setRange(plot->domain); })
->setToolTip(tr("Fit all data (Home)"));
followAction = ui->toolbar->addAction(tr("Follow"));
followAction->setCheckable(true);
followAction->setChecked(true);
followAction->setToolTip(tr("Pan to keep the current disassembly or hexdump address visible"));
connect(followAction, &QAction::toggled, this, [this](bool checked) {
if (checked) {
onSeekChanged(seekable->getOffset());
}
});
optionsMenu = new QMenu(this);
tracksAction = optionsMenu->addAction(tr("Separate algorithm tracks"));
tracksAction->setCheckable(true);
connect(tracksAction, &QAction::toggled, this, [this](bool checked) {
plot->separateTracks = checked;
plot->setMinimumHeight(checked ? qMax(180, int(plot->metrics.size()) * 90) : 180);
plot->update();
});
auto *styles = optionsMenu->addMenu(tr("Drawing style"));
auto *styleGroup = new QActionGroup(styles);
const QStringList styleNames = {tr("Lines"), tr("Filled lines"), tr("Vertical bars")};
for (int i = 0; i < styleNames.size(); ++i) {
auto *action = styles->addAction(styleNames[i]);
styleGroup->addAction(action);
action->setCheckable(true);
action->setData(i);
action->setChecked(i == plot->style);
connect(action, &QAction::triggered, this, [this, i]() {
plot->style = static_cast<DataAnalysisView::Style>(i);
plot->update();
});
}
styles->setObjectName("analysisStyles");
auto *colors = optionsMenu->addMenu(tr("Colors"));
auto *colorGroup = new QActionGroup(colors);
const QStringList modes
= {tr("Greyscale"), tr("Rainbow"), tr("Theme single color"), tr("Theme palette")};
for (int i = 0; i < modes.size(); ++i) {
auto *action = colors->addAction(modes[i]);
colorGroup->addAction(action);
action->setCheckable(true);
action->setData(i);
action->setChecked(i == colorMode);
connect(action, &QAction::triggered, this, [this, i]() {
colorMode = i;
updateColors();
});
}
colors->setObjectName("analysisColors");
colors->addSeparator();
auto *custom = colors->addMenu(tr("Customize algorithm colors"));
for (int i = 0; i < Count; ++i) {
custom->addAction(DataAnalysisView::metricName(i), this, [this, i]() {
const QColor color
= QColorDialog::getColor(plot->colors[i], this, DataAnalysisView::metricName(i));
if (color.isValid()) {
customColors[i] = color;
updateColors();
}
});
}
custom->addSeparator();
custom->addAction(tr("Reset custom colors"), this, [this]() {
customColors.fill(QColor());
updateColors();
});
auto *markers = optionsMenu->addMenu(tr("Markers"));
markers->setObjectName("analysisMarkers");
for (int i = 0; i < DataAnalysisView::MarkerCount; ++i) {
auto *action = markers->addAction(
DataAnalysisView::markerName(static_cast<DataAnalysisView::MarkerType>(i)));
action->setCheckable(true);
action->setData(i);
action->setChecked(plot->markerEnabled[i]);
connect(action, &QAction::toggled, this, [this, i](bool checked) {
plot->markerEnabled[i] = overview->markerEnabled[i] = checked;
plot->update();
overview->update();
});
}
auto *samples = optionsMenu->addMenu(tr("Resolution (buckets)"));
samples->setObjectName("analysisResolution");
auto *sampleGroup = new QActionGroup(samples);
for (int n : {32, 64, 128, 256, 512, 1024, 2048, 4096}) {
auto *action = samples->addAction(QString::number(n));
sampleGroup->addAction(action);
action->setCheckable(true);
action->setData(n);
action->setChecked(n == resolution);
connect(action, &QAction::triggered, this, [this, n]() {
resolution = n;
beginSampling(samplingOverview);
});
}
optionsMenu->addSeparator();
optionsMenu->addAction(&syncAction);
optionsMenu->addAction(tr("Find marker…"), this, &DataAnalysisWidget::findMarker);
optionsMenu->addAction(tr("Refresh"), this, &DataAnalysisWidget::scheduleReload);
exportAction = optionsMenu->addAction(
tr("Export visible samples as CSV…"), this, &DataAnalysisWidget::exportCsv);
exportAction->setEnabled(false);
menuButton(tr("Options"), optionsMenu);
ui->legend->setToolTip(
tr("Click a legend entry to configure its color and visibility."
" Overlaid values use a 0–100% scale; entropy is divided by 8."
" Separate tracks use native units."));
updateMetrics();
}
void DataAnalysisWidget::updateMetrics()
{
plot->metrics.clear();
for (int i = 0; i < Count; ++i) {
legendActions[i]->setVisible(metricActions[i]->isChecked());
if (metricActions[i]->isChecked()) {
plot->metrics.append(i);
}
}
overview->metrics = plot->metrics;
plot->setMinimumHeight(plot->separateTracks ? qMax(180, int(plot->metrics.size()) * 90) : 180);
plot->update();
}
void DataAnalysisWidget::updateColors()
{
const char *keys[]
= {"gui.navbar.code",
"gui.navbar.str",
"gui.navbar.data",
"num",
"flag",
"comment",
"flow",
"func_var",
"reg",
"invalid",
"other"};
QColor base = Config()->getColor("gui.navbar.code");
if (!base.isValid()) {
base = QColor(120, 150, 230);
}
QSet<QRgb> used;
for (int i = 0; i < Count; ++i) {
QColor color;
switch (colorMode) {
case 0:
color = QColor::fromHsv(0, 0, 85 + i * 16);
break;
case 1:
color = QColor::fromHsv((270 + i * 137) % 360, 165, 220);
break;
case 2:
color = QColor::fromHsv(qMax(0, base.hue()), 70 + i * 16, 235 - i * 9);
break;
default:
color = Config()->getColor(keys[i]);
if (!color.isValid() || used.contains(color.rgb())
|| color == palette().color(QPalette::Base)) {
color = QColor::fromHsv((270 + i * 137) % 360, 155, 210);
}
break;
}
if (customColors[i].isValid()) {
color = customColors[i];
}
used.insert(color.rgb());
plot->colors[i] = overview->colors[i] = color;
QPixmap swatch(12, 12);
swatch.fill(color);
metricActions[i]->setIcon(QIcon(swatch));
legendActions[i]->setIcon(QIcon(swatch));
}
plot->update();
overview->update();
}
void DataAnalysisWidget::configureMetric(int metric)
{
QDialog dialog(this);
dialog.setObjectName("analysisMetricSettings");
dialog.setWindowTitle(tr("%1 settings").arg(DataAnalysisView::metricName(metric)));
QFormLayout layout(&dialog);
QLabel description(DataAnalysisView::metricDescription(metric));
description.setWordWrap(true);
description.setMaximumWidth(420);
QCheckBox visible(tr("Show this algorithm"));
visible.setObjectName("metricVisible");
visible.setChecked(metricActions[metric]->isChecked());
QColor chosen = customColors[metric];
QPushButton colorButton(tr("Change color…"));
colorButton.setIcon(legendActions[metric]->icon());
QPushButton resetButton(tr("Use palette color"));
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout.addRow(&description);
layout.addRow(tr("Color:"), &colorButton);
layout.addRow(&resetButton);
layout.addRow(&visible);
layout.addRow(&buttons);
connect(&colorButton, &QPushButton::clicked, &dialog, [&]() {
const QColor color = QColorDialog::getColor(
chosen.isValid() ? chosen : plot->colors[metric],
&dialog,
DataAnalysisView::metricName(metric));
if (color.isValid()) {
chosen = color;
QPixmap swatch(12, 12);
swatch.fill(color);
colorButton.setIcon(QIcon(swatch));
colorButton.setText(color.name());
}
});
connect(&resetButton, &QPushButton::clicked, &dialog, [&]() {
chosen = QColor();
colorButton.setIcon(QIcon());
colorButton.setText(tr("Palette default"));
});
connect(&buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (dialog.exec() == QDialog::Accepted) {
customColors[metric] = chosen;
metricActions[metric]->setChecked(visible.isChecked());
updateColors();
}
}
void DataAnalysisWidget::scheduleReload()
{
metadataDirty = true;
if (refreshDeferrer->attemptRefresh(nullptr)) {
sampleTimer.stop();
reloadTimer.start();
}
}
void DataAnalysisWidget::loadMetadata()
{
using View = DataAnalysisView;
virtualAddresses = Core()->getConfigb("io.va");
readable.clear();
plot->markers.clear();
auto addMarker = [this](Range range, const QString &name, View::MarkerType type) {
if (range.valid()) {
plot->markers.append({range, name, type});
}
};
for (const auto &value : Core()->cmdj("omj").array()) {
const QJsonObject map = value.toObject();
const RVA start = number(map, "from"), last = number(map, "to");
const Range range = last >= start ? Range{start, last == RVA_MAX ? RVA_MAX : last + 1}
: Range{};
if (virtualAddresses) {
addMarker(range, map["name"].toString(), View::Map);
if (range.valid() && map["perm"].toString().contains('r')) {
readable.append(range);
}
}
}
if (!virtualAddresses) {
auto core = Core()->core();
const RVA size = r_io_size(core->io);
if (size != RVA_MAX && size) {
readable.append({0, size});
}
}
readable = mergeRanges(readable);
const Range domain = readable.isEmpty() ? Range{}
: Range{readable.first().start, readable.last().end};
const bool changed = domain != plot->domain;
plot->domain = overview->domain = domain;
if (changed || !plot->visibleRange.valid()) {
plot->visibleRange = overview->visibleRange = domain;
}
for (const auto &entry :
{qMakePair(QString("iSj"), View::Section), qMakePair(QString("iSSj"), View::Segment)}) {
const QJsonDocument document = Core()->cmdj(entry.first);
const QJsonArray objects
= document.isArray()
? document.array()
: document.object()[entry.second == View::Section ? "sections" : "segments"]
.toArray();
for (const auto &value : objects) {
const QJsonObject object = value.toObject();
const RVA start = number(object, virtualAddresses ? "vaddr" : "paddr");
const RVA size = number(object, virtualAddresses ? "vsize" : "size");
addMarker(sizedRange(start, size), object["name"].toString(), entry.second);
}
}
// @F temporarily selects all flag spaces without changing the user's space.
for (const auto &value : Core()->cmdj("fj @F:*").array()) {
const QJsonObject flag = value.toObject();
const RVA address = number(flag, flag.contains("offset") ? "offset" : "addr");
addMarker(
sizedRange(address, qMax<RVA>(1, number(flag, "size"))),
flag["name"].toString(),
View::Flag);
}
for (const auto &comment : Core()->getAllComments("CCu")) {
addMarker(sizedRange(comment.offset, 1), comment.name, View::Comment);
}
std::stable_sort(
plot->markers.begin(),
plot->markers.end(),
[](const View::Marker &a, const View::Marker &b) { return a.range.start < b.range.start; });
overview->markers = plot->markers;
metadataDirty = false;
}
void DataAnalysisWidget::reload()
{
if (!refreshDeferrer->attemptRefresh(nullptr) || Core()->isDebugTaskInProgress()) {
return;
}
sampleTimer.stop();
rangeTimer.stop();
loadMetadata();
plot->seekAddress = overview->seekAddress = seekable->getOffset();
if (requestedRange.valid()) {
const Range requested = intersect(requestedRange, plot->domain);
if (requested.valid()) {
plot->visibleRange = overview->visibleRange = requested;
}
requestedRange = {};
}
if (seekable->isSynchronized() && Config()->getAddressRangeSelectionSyncEnabled()
&& Core()->hasAddressRangeSelection()) {
selectRange(
{Core()->getAddressRangeSelectionStart(), Core()->getAddressRangeSelectionEnd() + 1},
false);
}
plot->samples.clear();
overview->samples.clear();
beginSampling(true);
}
void DataAnalysisWidget::beginSampling(bool mini)
{
rangeTimer.stop();
sampleTimer.stop();
if (!isVisibleToUser()) {
metadataDirty = true;
refreshDeferrer->attemptRefresh(nullptr);
return;
}
samplingOverview = mini;
pendingRange = mini ? plot->domain : plot->visibleRange;
pending.clear();
nextBucket = 0;
plot->loading = pendingRange.valid();
exportAction->setEnabled(false);
if (pendingRange.valid()) {
const int count = int(qMin<RVA>(mini ? 256 : resolution, pendingRange.size()));
pending.resize(count);
sampleTimer.start();
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::sampleBatch()
{
if (!isVisibleToUser() || Core()->isDebugTaskInProgress()) {
sampleTimer.stop();
metadataDirty = true;
refreshDeferrer->attemptRefresh(nullptr);
return;
}
QElapsedTimer timer;
timer.start();
// Yield to navigation and painting every few milliseconds. A range change
// cancels the old job; no stale worker result can overwrite the new range.
do {
pending[nextBucket]
= sample(bucket(pendingRange, nextBucket, pending.size()), readable, readData);
++nextBucket;
} while (nextBucket < pending.size() && timer.elapsed() < 6);
if (nextBucket < pending.size()) {
ui->statusLabel->setText(tr("Analyzing %1… %2%")
.arg(samplingOverview ? tr("overview") : tr("visible range"))
.arg(nextBucket * 100 / pending.size()));
return;
}
sampleTimer.stop();
if (samplingOverview) {
overview->samples = pending;
overview->update();
beginSampling(false);
} else {
plot->samples = pending;
plot->loading = false;
exportAction->setEnabled(!plot->samples.isEmpty());
updateLabels();
plot->update();
}
}
void DataAnalysisWidget::setRange(Range range)
{
range = intersect(range, plot->domain);
if (!range.valid() || range == plot->visibleRange) {
return;
}
plot->visibleRange = overview->visibleRange = range;
plot->samples.clear();
plot->loading = true;
exportAction->setEnabled(false);
if (!samplingOverview) {
sampleTimer.stop();
}
rangeTimer.start();
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::showRange(RVA start, RVA end)
{
if (start == RVA_INVALID || end == RVA_INVALID || end < start) {
return;
}
requestedRange = {start, end + 1};
raiseMemoryWidget();
seekable->seek(start);
if (metadataDirty) {
scheduleReload();
} else {
setRange(requestedRange);
requestedRange = {};
}
}
void DataAnalysisWidget::onSeekChanged(RVA address)
{
plot->seekAddress = overview->seekAddress = address;
if (followAction->isChecked() && plot->domain.contains(address)
&& !plot->visibleRange.contains(address)) {
setRange(centered(plot->domain, address, plot->visibleRange.size()));
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::selectRange(Range range, bool publish)
{
plot->selection = overview->selection = range;
selectionAction->setEnabled(range.valid());
if (publish && seekable->isSynchronized() && Config()->getAddressRangeSelectionSyncEnabled()) {
publishingSelection = true;
if (range.valid()) {
Core()->setAddressRangeSelection(range.start, range.end - 1);
} else {
Core()->clearAddressRangeSelection();
}
publishingSelection = false;
}
updateLabels();
plot->update();
overview->update();
}
void DataAnalysisWidget::updateLabels()
{
const Range range = plot->visibleRange;
if (!range.valid()) {
ui->rangeLabel->setText(tr("No readable data in the current address space"));
ui->statusLabel->setText(tr("Open a file or refresh after mapping data."));
return;
}
QString text = tr("%1: %2 – %3 · %4 bytes · Cursor %5")
.arg(
virtualAddresses ? tr("Addresses") : tr("File offsets"),
RAddressString(range.start),
RAddressString(range.end - 1))
.arg(range.size())
.arg(RAddressString(seekable->getOffset()));
if (plot->selection.valid()) {
text += tr("\nSelection: %1 – %2 · %3 bytes")
.arg(
RAddressString(plot->selection.start),
RAddressString(plot->selection.end - 1))
.arg(plot->selection.size());
}
ui->rangeLabel->setText(text);
if (plot->loading) {
ui->statusLabel->setText(tr("Analyzing…"));
} else {
RVA bytes = 0, covered = 0;
for (const auto &sample : plot->samples) {
bytes += sample.bytes;
covered += sample.covered;
}
if (!bytes) {
ui->statusLabel->setText(tr("No readable bytes in the visible range."));
return;
}
ui->statusLabel->setText(
tr("%1 buckets · %2 / %3 mapped bytes read (%4) · Overview: entropy")
.arg(plot->samples.size())
.arg(bytes)
.arg(covered)
.arg(bytes == covered ? tr("complete") : tr("sampled or unreadable")));
ui->statusLabel->setToolTip(
tr("Each bucket reads up to 4096 bytes from centered windows in readable spans."
" Zoom in for complete coverage. Unmapped bytes are excluded."
" Shannon entropy: 0–8 bits/byte. Overlays use normalized percentages."));
}
}
void DataAnalysisWidget::fitSelection()
{
if (plot->selection.valid()) {
setRange(plot->selection);
}
}
void DataAnalysisWidget::fitMarker(DataAnalysisView::MarkerType type)
{
Range best;
for (const auto &marker : plot->markers) {
if (marker.type == type && marker.range.contains(seekable->getOffset())
&& (!best.valid() || marker.range.size() < best.size())) {
best = marker.range;
}
}
if (best.valid()) {
setRange(best);
} else {
ui->statusLabel->setText(
tr("No %1 contains the current address.").arg(DataAnalysisView::markerName(type)));
}
}
void DataAnalysisWidget::editRange()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Data analysis range"));
QFormLayout layout(&dialog);
QLineEdit start(RAddressString(plot->visibleRange.start));
QLineEdit end(RAddressString(plot->visibleRange.valid() ? plot->visibleRange.end - 1 : 0));
QLabel hint(tr("Enter addresses or radare2 expressions. Both endpoints are inclusive."));
hint.setWordWrap(true);
QLabel error;
error.setWordWrap(true);
QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
layout.addRow(&hint);
layout.addRow(tr("Start:"), &start);
layout.addRow(tr("End (inclusive):"), &end);
layout.addRow(&error);
layout.addRow(&buttons);
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
connect(&buttons, &QDialogButtonBox::accepted, &dialog, [&]() {
RVA first, last;
if (!Core()->tryMath(start.text(), first) || !Core()->tryMath(end.text(), last)
|| first == RVA_INVALID || last == RVA_INVALID || first > last) {
error.setText(tr("Enter a valid start and an end greater than or equal to it."));
return;
}
const Range requested{first, last + 1};
if (intersect(requested, plot->domain) != requested) {
error.setText(
tr("The range must be within %1 – %2.")
.arg(RAddressString(plot->domain.start), RAddressString(plot->domain.end - 1)));
return;
}
setRange(requested);
dialog.accept();
});
dialog.exec();
}
void DataAnalysisWidget::findMarker()
{
QDialog dialog(this);
dialog.setWindowTitle(tr("Find section, map, flag or comment"));
dialog.resize(620, 400);
QVBoxLayout layout(&dialog);
QLineEdit filter;
filter.setPlaceholderText(tr("Filter by name, comment, type or hexadecimal address…"));
QListWidget list;
QLabel hint(
tr("Activate a result to seek. Section, segment and map results also fit the range."
" Showing up to 500 matches."));
hint.setWordWrap(true);
QDialogButtonBox buttons(QDialogButtonBox::Close);
layout.addWidget(&filter);
layout.addWidget(&list);
layout.addWidget(&hint);
layout.addWidget(&buttons);
// Keep result indices stable while the modal loop receives metadata updates.
const auto markers = plot->markers;
auto fill = [&]() {
list.clear();
for (int i = 0; i < markers.size(); ++i) {
const auto &marker = markers[i];
const QString label = QString("%1 · %2 · %3")
.arg(
RAddressString(marker.range.start),
DataAnalysisView::markerName(marker.type),
marker.name);
if (label.contains(filter.text(), Qt::CaseInsensitive)) {
auto *item = new QListWidgetItem(label, &list);
item->setData(Qt::UserRole, i);
if (list.count() >= 500) {
break;
}
}
}
};
connect(&filter, &QLineEdit::textChanged, &dialog, fill);
connect(&list, &QListWidget::itemActivated, &dialog, [&](QListWidgetItem *item) {
const int index = item->data(Qt::UserRole).toInt();
if (index < 0 || index >= markers.size()) {
return;
}
const auto marker = markers[index];
seekable->seek(marker.range.start);
if (marker.type <= DataAnalysisView::Map) {
setRange(marker.range);
} else if (!plot->visibleRange.contains(marker.range.start)) {
setRange(centered(plot->domain, marker.range.start, plot->visibleRange.size()));
}
dialog.accept();
});
connect(&buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
fill();
dialog.exec();
}
void DataAnalysisWidget::showContextMenu(RVA address, QPoint position)
{
QMenu menu(this);
menu.addAction(RAddressString(address))->setEnabled(false);
menu.addAction(tr("Seek here"), this, [this, address]() { seekable->seek(address); });
if (mainWindow) {
auto *showIn = mainWindow->createShowInMenu(&menu, address);
showIn->setTitle(tr("Show in"));
menu.addMenu(showIn);
}
menu.addAction(tr("Copy address"), this, [address]() {
QApplication::clipboard()->setText(RAddressString(address));
});
menu.addAction(tr("Add or edit comment…"), this, [this, address]() {
CommentsDialog::addOrEditComment(address, this);
});
const Range selection = plot->selection;
const RVA flagSize = selection.contains(address) ? selection.end - address : 1;
menu.addAction(tr("Add or edit flag…"), this, [this, address, flagSize]() {
FlagDialog dialog(address, flagSize, this);
dialog.exec();
});
menu.addSeparator();
menu.addAction(selectionAction);
menu.addAction(tr("Clear selection"), this, [this]() { selectRange({}, true); });
menu.addAction(tr("Edit range…"), this, &DataAnalysisWidget::editRange);
menu.addAction(tr("Find marker…"), this, &DataAnalysisWidget::findMarker);
menu.addAction(tr("Fit all data"), this, [this]() { setRange(plot->domain); });
menu.addSeparator();
menu.addAction(&syncAction);
menu.exec(position);
}
void DataAnalysisWidget::exportCsv()
{
const auto samples = plot->samples;
const auto metrics = plot->metrics;
const QString path = QFileDialog::getSaveFileName(
this, tr("Export data analysis"), QString(), tr("CSV files (*.csv)"));
if (path.isEmpty()) {
return;
}
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, tr("Export failed"), file.errorString());
return;
}
QTextStream out(&file);
out << "start,end_exclusive,bytes_read,mapped_bytes";
for (int metric : metrics) {
QString name = DataAnalysisView::metricName(metric);
name.replace('"', "\"\"");
out << ",\"" << name << " (normalized 0-1)\"";
}
out << '\n';
for (const auto &sample : samples) {
out << RAddressString(sample.range.start) << ',' << RAddressString(sample.range.end) << ','
<< sample.bytes << ',' << sample.covered;
for (int metric : metrics) {
out << ',';
if (sample.bytes) {
out << QString::number(sample.values[metric], 'g', 12);
}
}
out << '\n';
}
out.flush();
if (!file.commit()) {
QMessageBox::warning(this, tr("Export failed"), file.errorString());
}
}
QVariantMap DataAnalysisWidget::serializeViewProprties()
{
auto properties = AddressableDockWidget::serializeViewProprties();
QVariantList metrics, colors, markers;
for (int i = 0; i < Count; ++i) {
if (metricActions[i]->isChecked()) {
metrics.append(i);
}
colors.append(customColors[i].isValid() ? customColors[i].name() : QString());
}
for (bool enabled : plot->markerEnabled) {
markers.append(enabled);
}
properties["metrics"] = metrics;
properties["colors"] = colors;
properties["markers"] = markers;
properties["style"] = int(plot->style);
properties["colorMode"] = colorMode;
properties["resolution"] = resolution;
properties["tracks"] = tracksAction->isChecked();
properties["follow"] = followAction->isChecked();
return properties;
}
void DataAnalysisWidget::deserializeViewProperties(const QVariantMap &properties)
{
AddressableDockWidget::deserializeViewProperties(properties);
const QVariantList metrics
= properties.value("metrics", QVariantList{Entropy, Printable, Zero}).toList();
const QVariantList colors = properties.value("colors").toList();
for (int i = 0; i < Count; ++i) {
metricActions[i]->setChecked(metrics.contains(i));
customColors[i] = i < colors.size() ? QColor(colors[i].toString()) : QColor();
}
auto restoreMenu = [this](const char *name, int selected) {
for (auto *action : optionsMenu->findChild<QMenu *>(name)->actions()) {
if (action->isCheckable() && action->data().toInt() == selected) {
action->trigger();
}
}
};
restoreMenu("analysisStyles", qBound(0, properties.value("style", 1).toInt(), 2));
restoreMenu("analysisColors", qBound(0, properties.value("colorMode", 3).toInt(), 3));
int count = properties.value("resolution", 1024).toInt();
if (!QList<int>{32, 64, 128, 256, 512, 1024, 2048, 4096}.contains(count)) {
count = 1024;
}
restoreMenu("analysisResolution", count);
const QVariantList markers
= properties.value("markers", QVariantList{true, false, false, true, true}).toList();
for (auto *action : optionsMenu->findChild<QMenu *>("analysisMarkers")->actions()) {
const int index = action->data().toInt();
action->setChecked(index < markers.size() && markers[index].toBool());
}
tracksAction->setChecked(properties.value("tracks", false).toBool());
followAction->setChecked(properties.value("follow", true).toBool());
updateColors();
}
+81
View File
@@ -0,0 +1,81 @@
#ifndef DATAANALYSISWIDGET_H
#define DATAANALYSISWIDGET_H
#include "AddressableDockWidget.h"
#include "DataAnalysisView.h"
#include <memory>
#include <QTimer>
namespace Ui {
class DataAnalysisWidget;
}
class QMenu;
class DataAnalysisWidget : public AddressableDockWidget
{
Q_OBJECT
public:
explicit DataAnalysisWidget(MainWindow *main);
~DataAnalysisWidget() override;
QVariantMap serializeViewProprties() override;
void deserializeViewProperties(const QVariantMap &properties) override;
// Reusable entry point for other widgets. Both endpoints are inclusive.
void showRange(RVA start, RVA end);
protected:
QString getWindowTitle() const override { return tr("Data Analysis"); }
QWidget *widgetToFocusOnRaise() override;
private:
std::unique_ptr<Ui::DataAnalysisWidget> ui;
DataAnalysisView *plot;
DataAnalysisView *overview;
RefreshDeferrer *refreshDeferrer;
QTimer reloadTimer;
QTimer sampleTimer;
QTimer rangeTimer;
QVector<DataAnalysis::Range> readable;
QVector<DataAnalysis::Sample> pending;
DataAnalysis::Range pendingRange;
DataAnalysis::Range requestedRange;
int nextBucket = 0;
int resolution = 1024;
int colorMode = 3;
bool samplingOverview = false;
bool virtualAddresses = true;
bool metadataDirty = true;
bool publishingSelection = false;
QAction *followAction;
QAction *tracksAction;
QAction *selectionAction;
QAction *exportAction;
QMenu *optionsMenu;
std::array<QAction *, DataAnalysis::Count> metricActions;
std::array<QAction *, DataAnalysis::Count> legendActions;
std::array<QColor, DataAnalysis::Count> customColors;
void setupActions();
void scheduleReload();
void reload();
void loadMetadata();
void beginSampling(bool mini);
void sampleBatch();
void setRange(DataAnalysis::Range range);
void updateLabels();
void updateColors();
void updateMetrics();
void configureMetric(int metric);
void onSeekChanged(RVA address);
void selectRange(DataAnalysis::Range range, bool publish);
void fitSelection();
void editRange();
void fitMarker(DataAnalysisView::MarkerType type);
void findMarker();
void showContextMenu(RVA address, QPoint position);
void exportCsv();
};
#endif
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DataAnalysisWidget</class>
<widget class="QWidget" name="DataAnalysisWidget">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing"><number>4</number></property>
<property name="leftMargin"><number>4</number></property>
<property name="topMargin"><number>4</number></property>
<property name="rightMargin"><number>4</number></property>
<property name="bottomMargin"><number>4</number></property>
<item><widget class="QToolBar" name="toolbar"><property name="movable"><bool>false</bool></property></widget></item>
<item><widget class="QLabel" name="rangeLabel">
<property name="text"><string>Open a file to analyze its data</string></property>
<property name="wordWrap"><bool>true</bool></property>
<property name="textInteractionFlags"><set>Qt::TextSelectableByMouse</set></property>
</widget></item>
<item><widget class="QToolBar" name="legend"><property name="movable"><bool>false</bool></property></widget></item>
<item><layout class="QVBoxLayout" name="plotLayout"/></item>
<item><widget class="QLabel" name="statusLabel">
<property name="wordWrap"><bool>true</bool></property>
<property name="text"><string>Click to seek · Drag to select · Wheel to zoom · Shift+wheel to pan</string></property>
<property name="textInteractionFlags"><set>Qt::TextSelectableByMouse</set></property>
</widget></item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+5
View File
@@ -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);
+1
View File
@@ -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;
+20
View File
@@ -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(); });
+14
View File
@@ -184,6 +184,20 @@ void SectionsWidget::initSectionsTable()
setModels(proxyModel);
ui->treeView->sortByColumn(SectionsModel::AddressColumn, Qt::AscendingOrder);
ui->treeView->getItemContextMenu()->addAction(tr("Analyze section data"), this, [this]() {
const QModelIndex index = ui->treeView->currentIndex();
if (!index.isValid()) {
return;
}
const auto section
= index.data(SectionsModel::SectionDescriptionRole).value<SectionDescription>();
const bool va = Core()->getConfigb("io.va");
const RVA start = va ? section.vaddr : section.paddr;
const RVA size = qMin(va ? section.vsize : section.size, RVA_MAX - start);
if (size) {
mainWindow->showDataAnalysis(start, start + size - 1);
}
});
}
void SectionsWidget::initQuickFilter()
+15
View File
@@ -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]() {