diff options
| author | Mason Reed <mason@vector35.com> | 2026-03-13 12:24:18 -0700 |
|---|---|---|
| committer | Mason Reed <35282038+emesare@users.noreply.github.com> | 2026-03-24 18:46:48 -0700 |
| commit | 3c88b11e5df33116580ac008e36092775df66135 (patch) | |
| tree | cd64fbe149f05683ddabcccd0db7b87356f1f8cf /plugins/warp/ui | |
| parent | f325aa7b6026a1daef84931baeb1f50d7da10c10 (diff) | |
[WARP] Improved UX and API
- Exposes WARP type objects directly
- Adds processor API (for generating warp files directly)
- Adds file and chunk API
- Misc cleanup
- Simplified the amount of commands
- Replaced the "Create" commands with a purpose built processor dialog
- Added a native QT viewer for WARP files
- Simplified committing to a remote with a purpose built commit dialog
Diffstat (limited to 'plugins/warp/ui')
28 files changed, 1799 insertions, 393 deletions
diff --git a/plugins/warp/ui/CMakeLists.txt b/plugins/warp/ui/CMakeLists.txt index 328289ac..d95ea6a7 100644 --- a/plugins/warp/ui/CMakeLists.txt +++ b/plugins/warp/ui/CMakeLists.txt @@ -12,7 +12,13 @@ file(GLOB SOURCES CONFIGURE_DEPENDS containers.cpp containers.h shared/search.cpp shared/search.h shared/fetcher.cpp shared/fetcher.h - shared/fetchdialog.cpp shared/fetchdialog.h) + shared/fetchdialog.cpp shared/fetchdialog.h + shared/processordialog.cpp shared/processordialog.h + shared/commitdialog.cpp shared/commitdialog.h + shared/file.cpp shared/file.h + shared/chunk.cpp shared/chunk.h + shared/source.cpp shared/source.h + shared/selectprojectfilesdialog.cpp shared/selectprojectfilesdialog.h) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) diff --git a/plugins/warp/ui/containers.cpp b/plugins/warp/ui/containers.cpp index f800f977..49415a52 100644 --- a/plugins/warp/ui/containers.cpp +++ b/plugins/warp/ui/containers.cpp @@ -1,94 +1,5 @@ #include "containers.h" -QVariant WarpSourcesModel::data(const QModelIndex& index, int role) const -{ - if (!index.isValid()) - return {}; - if (index.row() < 0 || index.row() >= rowCount()) - return {}; - - const auto& r = m_rows[static_cast<size_t>(index.row())]; - - // Build a small two-dot status icon (left: writable, right: uncommitted) - auto statusIcon = [](bool writable, bool uncommitted) -> QIcon { - static QIcon cache[2][2]; // [writable][uncommitted] - QIcon& cached = cache[writable ? 1 : 0][uncommitted ? 1 : 0]; - if (!cached.isNull()) - return cached; - - const int w = 16, h = 12, radius = 4; - QPixmap pm(w, h); - pm.fill(Qt::transparent); - QPainter p(&pm); - p.setRenderHint(QPainter::Antialiasing, true); - - // Colors - QColor writableOn(76, 175, 80); // green - QColor writableOff(158, 158, 158); // grey - QColor uncommittedOn(255, 193, 7); // amber - QColor uncommittedOff(158, 158, 158); // grey - - // Left dot: writable - p.setBrush(writable ? writableOn : writableOff); - p.setPen(Qt::NoPen); - p.drawEllipse(QPoint(4, h / 2), radius, radius); - - // Right dot: uncommitted - p.setBrush(uncommitted ? uncommittedOn : uncommittedOff); - p.drawEllipse(QPoint(w - 6, h / 2), radius, radius); - - p.end(); - cached = QIcon(pm); - return cached; - }; - - if (role == Qt::DecorationRole && index.column() == PathCol) - { - return statusIcon(r.writable, r.uncommitted); - } - - if (role == Qt::ToolTipRole && index.column() == PathCol) - { - QStringList parts; - parts << (r.writable ? "Writable" : "Read-only"); - parts << (r.uncommitted ? "Uncommitted changes" : "No uncommitted changes"); - return parts.join(" • "); - } - - if (role == Qt::DisplayRole) - { - switch (index.column()) - { - case GuidCol: - return r.guid; - case PathCol: - return r.path; - case WritableCol: - return r.writable ? "Yes" : "No"; - case UncommittedCol: - return r.uncommitted ? "Yes" : "No"; - default: - return {}; - } - } - - if (role == Qt::CheckStateRole) - { - // Optional: expose as checkboxes if someone ever shows these columns - switch (index.column()) - { - case WritableCol: - return r.writable ? Qt::Checked : Qt::Unchecked; - case UncommittedCol: - return r.uncommitted ? Qt::Checked : Qt::Unchecked; - default: - break; - } - } - - return {}; -} - WarpContainerWidget::WarpContainerWidget(Warp::Ref<Warp::Container> container, QWidget* parent) : QWidget(parent) { m_container = std::move(container); @@ -100,95 +11,14 @@ WarpContainerWidget::WarpContainerWidget(Warp::Ref<Warp::Container> container, Q // Sources tab m_sourcesPage = new QWidget(this); auto* sourcesLayout = new QVBoxLayout(m_sourcesPage); - m_sourcesView = new QTableView(m_sourcesPage); - m_sourcesModel = new WarpSourcesModel(m_sourcesPage); - m_sourcesModel->setContainer(m_container); - m_sourcesView->setModel(m_sourcesModel); - m_sourcesView->horizontalHeader()->setStretchLastSection(true); - m_sourcesView->setSelectionBehavior(QAbstractItemView::SelectRows); - m_sourcesView->setSelectionMode(QAbstractItemView::SingleSelection); - - // Make the table look like a simple list that shows only the source path - m_sourcesView->setShowGrid(false); - m_sourcesView->verticalHeader()->setVisible(false); - m_sourcesView->horizontalHeader()->setVisible(false); - m_sourcesView->setAlternatingRowColors(false); - m_sourcesView->setEditTriggers(QAbstractItemView::NoEditTriggers); - m_sourcesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_sourcesView->setWordWrap(false); - m_sourcesView->setIconSize(QSize(16, 12)); - // Ensure long paths truncate from the left: "...tail/of/the/path" - m_sourcesView->setTextElideMode(Qt::ElideLeft); - // Hide GUID column, keep only the Path column visible - m_sourcesView->setColumnHidden(WarpSourcesModel::GuidCol, true); - // Also hide boolean columns; their state is shown as an icon next to the path - m_sourcesView->setColumnHidden(WarpSourcesModel::WritableCol, true); - m_sourcesView->setColumnHidden(WarpSourcesModel::UncommittedCol, true); - // Ensure the remaining (Path) column fills the width - m_sourcesView->horizontalHeader()->setSectionResizeMode(WarpSourcesModel::PathCol, QHeaderView::Stretch); - - // Per-item context menu - m_sourcesView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(m_sourcesView, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) { - QMenu menu(m_sourcesView); - const QModelIndex index = m_sourcesView->indexAt(pos); - - if (!index.isValid()) - { - QAction* actAdd = menu.addAction(tr("Add Source")); - QAction* chosen = menu.exec(m_sourcesView->viewport()->mapToGlobal(pos)); - if (!chosen) - return; - if (chosen == actAdd) - { - std::string sourceName; - if (!BinaryNinja::GetTextLineInput(sourceName, "Source name:", "Add Source")) - return; - if (const auto sourceId = m_container->AddSource(sourceName); !sourceId.has_value()) - { - BinaryNinja::LogAlertF("Failed to add source: {}", sourceName); - return; - } - m_sourcesModel->reload(); - } - } - else - { - m_sourcesView->setCurrentIndex(index.sibling(index.row(), WarpSourcesModel::PathCol)); - - const int row = index.row(); - const QModelIndex pathIdx = m_sourcesModel->index(row, WarpSourcesModel::PathCol); - const QModelIndex guidIdx = m_sourcesModel->index(row, WarpSourcesModel::GuidCol); - const QString path = m_sourcesModel->data(pathIdx, Qt::DisplayRole).toString(); - const QFileInfo fi(path); - - const QString guid = m_sourcesModel->data(guidIdx, Qt::DisplayRole).toString(); - - QAction* actReveal = menu.addAction(tr("Reveal in File Browser")); - actReveal->setEnabled(fi.exists()); - QAction* actCopyPath = menu.addAction(tr("Copy Path")); - QAction* actCopyGuid = menu.addAction(tr("Copy GUID")); - - QAction* chosen = menu.exec(m_sourcesView->viewport()->mapToGlobal(pos)); - if (!chosen) - return; - if (chosen == actCopyPath) - QGuiApplication::clipboard()->setText(path); - else if (chosen == actCopyGuid) - QGuiApplication::clipboard()->setText(guid); - else if (chosen == actReveal) - QDesktopServices::openUrl(QUrl::fromLocalFile(fi.absoluteFilePath())); - } - }); + m_sourcesView = new WarpSourcesView(m_sourcesPage); + m_sourcesView->setContainer(m_container); sourcesLayout->addWidget(m_sourcesView); m_tabs->addTab(m_sourcesPage, tr("Sources")); - // Search tab - m_searchTab = new WarpSearchWidget(m_container, this); - m_tabs->addTab(m_searchTab, tr("Search")); - + // TODO: Maybe introduce some callbacks or something, but i feel like this is fine for now. // Periodic refresh timer for the Sources view m_refreshTimer = new QTimer(this); m_refreshTimer->setInterval(5000); @@ -197,25 +27,26 @@ WarpContainerWidget::WarpContainerWidget(Warp::Ref<Warp::Container> container, Q if (!this->isVisible() || !m_sourcesPage || !m_sourcesPage->isVisible()) return; + WarpSourcesModel* sourcesModel = m_sourcesView->sourceModel(); // Preserve selection by GUID across reloads QString currentGuid; if (const QModelIndex currentIdx = m_sourcesView->currentIndex(); currentIdx.isValid()) { const int row = currentIdx.row(); - const QModelIndex guidIdx = m_sourcesModel->index(row, WarpSourcesModel::GuidCol); - currentGuid = m_sourcesModel->data(guidIdx, Qt::DisplayRole).toString(); + const QModelIndex guidIdx = sourcesModel->index(row, WarpSourcesModel::GuidCol); + currentGuid = sourcesModel->data(guidIdx, Qt::DisplayRole).toString(); } - m_sourcesModel->reload(); + sourcesModel->reload(); if (!currentGuid.isEmpty()) { - for (int r = 0; r < m_sourcesModel->rowCount(); ++r) + for (int r = 0; r < sourcesModel->rowCount(); ++r) { - const QModelIndex gIdx = m_sourcesModel->index(r, WarpSourcesModel::GuidCol); - if (m_sourcesModel->data(gIdx, Qt::DisplayRole).toString() == currentGuid) + const QModelIndex gIdx = sourcesModel->index(r, WarpSourcesModel::GuidCol); + if (sourcesModel->data(gIdx, Qt::DisplayRole).toString() == currentGuid) { - m_sourcesView->setCurrentIndex(m_sourcesModel->index(r, WarpSourcesModel::PathCol)); + m_sourcesView->setCurrentIndex(sourcesModel->index(r, WarpSourcesModel::PathCol)); break; } } @@ -223,11 +54,11 @@ WarpContainerWidget::WarpContainerWidget(Warp::Ref<Warp::Container> container, Q }); m_refreshTimer->start(); - // Optional: force a refresh when switching back to the Sources tab + // TODO: Do we want to reload this on tab changed??? connect(m_tabs, &QTabWidget::currentChanged, this, [this](const int idx) { QWidget* w = m_tabs->widget(idx); if (w == m_sourcesPage) - m_sourcesModel->reload(); + m_sourcesView->sourceModel()->reload(); }); } diff --git a/plugins/warp/ui/containers.h b/plugins/warp/ui/containers.h index b95c32db..935b552e 100644 --- a/plugins/warp/ui/containers.h +++ b/plugins/warp/ui/containers.h @@ -1,8 +1,5 @@ #pragma once -#include <QWidget> -#include <optional> -#include <QClipboard> #include <QDesktopServices> #include <QInputDialog> #include <QListWidget> @@ -11,93 +8,7 @@ #include "theme.h" #include "warp.h" #include "../../../../ui/mainwindow.h" - -class WarpSourcesModel final : public QAbstractTableModel -{ - Q_OBJECT - -public: - enum Columns : int - { - GuidCol = 0, - PathCol, - WritableCol, - UncommittedCol, - ColumnCount - }; - - explicit WarpSourcesModel(QObject* parent = nullptr) : QAbstractTableModel(parent) {} - - void setContainer(Warp::Ref<Warp::Container> container) - { - m_container = std::move(container); - reload(); - } - - void reload() - { - // Fetch synchronously (can be adapted to async if needed) - beginResetModel(); - m_rows.clear(); - for (const auto& src : m_container->GetSources()) - { - QString guid = QString::fromStdString(src.ToString()); - QString path = QString::fromStdString(m_container->SourcePath(src).value_or(std::string {})); - bool writable = m_container->IsSourceWritable(src); - bool uncommitted = m_container->IsSourceUncommitted(src); - m_rows.push_back({guid, path, writable, uncommitted}); - } - endResetModel(); - } - - int rowCount(const QModelIndex& parent = QModelIndex()) const override - { - if (parent.isValid()) - return 0; - return static_cast<int>(m_rows.size()); - } - - int columnCount(const QModelIndex& parent = QModelIndex()) const override - { - Q_UNUSED(parent); - return ColumnCount; - } - - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - - QVariant headerData(int section, Qt::Orientation orientation, int role) const override - { - if (orientation == Qt::Horizontal && role == Qt::DisplayRole) - { - switch (section) - { - case GuidCol: - return "Source GUID"; - case PathCol: - return "Path"; - case WritableCol: - return "Writable"; - case UncommittedCol: - return "Uncommitted"; - default: - return {}; - } - } - return {}; - } - -private: - struct Row - { - QString guid; - QString path; - bool writable; - bool uncommitted; - }; - - std::vector<Row> m_rows; - Warp::Ref<Warp::Container> m_container; -}; +#include "shared/source.h" class WarpContainerWidget : public QWidget { @@ -112,8 +23,7 @@ private: QTabWidget* m_tabs = nullptr; // Sources - QTableView* m_sourcesView = nullptr; - WarpSourcesModel* m_sourcesModel = nullptr; + WarpSourcesView* m_sourcesView = nullptr; QWidget* m_sourcesPage = nullptr; QTimer* m_refreshTimer = nullptr; diff --git a/plugins/warp/ui/matched.cpp b/plugins/warp/ui/matched.cpp index 786b9d26..f3132938 100644 --- a/plugins/warp/ui/matched.cpp +++ b/plugins/warp/ui/matched.cpp @@ -1,10 +1,7 @@ #include "matched.h" - -#include <QGridLayout> - #include "theme.h" -const char* WARP_APPLY_ACTIVITY = "analysis.warp.apply"; +#include <QGridLayout> WarpMatchedWidget::WarpMatchedWidget(BinaryViewRef current) { @@ -28,7 +25,8 @@ WarpMatchedWidget::WarpMatchedWidget(BinaryViewRef current) m_splitter->addWidget(m_tableWidget); // Removes the match for the function, this is irreversible currently, and the user must run the matcher again. - // TODO: We previously were trying to instead toggle the application of the match, but because the symbols are applied + // TODO: We previously were trying to instead toggle the application of the match, but because the symbols are + // applied // TODO: when applying the match metadata we would persist that regardless. m_tableWidget->RegisterContextMenuAction( "Remove Match", [this](WarpFunctionItem*, std::optional<uint64_t> address) { @@ -76,7 +74,8 @@ void WarpMatchedWidget::Update() for (const auto& analysisFunction : m_current->GetAnalysisFunctionList()) { if (const auto& matchedFunction = Warp::Function::GetMatched(*analysisFunction)) - m_tableWidget->InsertFunction(analysisFunction->GetStart(), new WarpFunctionItem(matchedFunction, analysisFunction)); + m_tableWidget->InsertFunction( + analysisFunction->GetStart(), new WarpFunctionItem(matchedFunction, analysisFunction)); else m_tableWidget->RemoveFunction(analysisFunction->GetStart()); } diff --git a/plugins/warp/ui/matches.cpp b/plugins/warp/ui/matches.cpp index cede06ab..918312fd 100644 --- a/plugins/warp/ui/matches.cpp +++ b/plugins/warp/ui/matches.cpp @@ -1,15 +1,13 @@ -#include <QGridLayout> -#include <QHeaderView> - #include "matches.h" +#include "theme.h" +#include "warp.h" +#include "shared/misc.h" #include <QClipboard> #include <QFormLayout> #include <thread> - -#include "theme.h" -#include "warp.h" -#include "shared/misc.h" +#include <QGridLayout> +#include <QHeaderView> WarpCurrentFunctionWidget::WarpCurrentFunctionWidget(QWidget* parent) : QWidget(parent) { @@ -74,23 +72,24 @@ WarpCurrentFunctionWidget::WarpCurrentFunctionWidget(QWidget* parent) : QWidget( m_tableWidget->GetModel()->SetMatchedFunction(selectedFunction); }); // If the selected function is the current match, let the user remove the match. - m_tableWidget->RegisterContextMenuAction("Remove Match", + m_tableWidget->RegisterContextMenuAction( + "Remove Match", [this](WarpFunctionItem*, std::optional<uint64_t>) { WarpRemoveMatchDialog dlg(this, m_current); if (dlg.execute()) m_tableWidget->GetModel()->SetMatchedFunction(nullptr); }, [this](WarpFunctionItem* item, std::optional<uint64_t>) { - if (item == nullptr) - return false; - Warp::Ref<Warp::Function> selectedFunction = item->GetFunction(); - if (!selectedFunction) - return false; - Warp::Ref<Warp::Function> matchedFunction = m_tableWidget->GetModel()->GetMatchedFunction(); - if (!matchedFunction) - return false; - return BNWARPFunctionsEqual(selectedFunction->m_object, matchedFunction->m_object); - }); + if (item == nullptr) + return false; + Warp::Ref<Warp::Function> selectedFunction = item->GetFunction(); + if (!selectedFunction) + return false; + Warp::Ref<Warp::Function> matchedFunction = m_tableWidget->GetModel()->GetMatchedFunction(); + if (!matchedFunction) + return false; + return BNWARPFunctionsEqual(selectedFunction->m_object, matchedFunction->m_object); + }); m_tableWidget->RegisterContextMenuAction( "Search for Source", [this](WarpFunctionItem* item, std::optional<uint64_t>) { // Apply the source as the filter. diff --git a/plugins/warp/ui/plugin.cpp b/plugins/warp/ui/plugin.cpp index e99059b9..0cf8deea 100644 --- a/plugins/warp/ui/plugin.cpp +++ b/plugins/warp/ui/plugin.cpp @@ -1,12 +1,15 @@ #include "plugin.h" - -#include <QToolBar> - #include "matched.h" #include "matches.h" #include "symbollist.h" #include "viewframe.h" +#include "shared/processordialog.h" #include "shared/fetchdialog.h" +#include "shared/file.h" + +#include <QToolBar> +#include <QVBoxLayout> +#include <utility> using namespace BinaryNinja; @@ -61,7 +64,7 @@ void ShowNetworkNotice() } } -WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP"), m_data(data) +WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP"), m_data(std::move(data)) { m_logger = LogRegistry::CreateLogger("WARP UI"); m_currentFrame = nullptr; @@ -85,30 +88,25 @@ WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP") }); fetchAction->setToolTip("Fetch data from WARP containers"); - auto commitIcon = GetColoredIcon(":/icons/images/arrow-push.png", getThemeColor(BlueStandardHighlightColor)); - auto commitAction = headerToolbar->addAction(commitIcon, "Commit a WARP file to a source", [this]() { - UIActionHandler* handler = m_currentFrame->getCurrentViewInterface()->actionHandler(); - handler->executeAction("WARP\\Commit File"); + auto processIcon = GetColoredIcon(":/icons/images/plus.png", getThemeColor(BlueStandardHighlightColor)); + auto processAction = headerToolbar->addAction(processIcon, "Process files or views for WARP", [this]() { + auto* dialog = new ProcessorDialog(this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->onAddBinaryView(m_data); + dialog->show(); }); - commitAction->setToolTip("Commit a WARP file to a source"); + processAction->setToolTip("Process files or views for WARP"); // We want to make it clear that the container actions for fetching and pushing are seperate. headerToolbar->addSeparator(); - auto loadIcon = GetColoredIcon(":/icons/images/file-add.png", getThemeColor(BlueStandardHighlightColor)); + auto loadIcon = GetColoredIcon(":/icons/images/archive.png", getThemeColor(BlueStandardHighlightColor)); auto loadAction = headerToolbar->addAction(loadIcon, "Load Signature File", [this]() { UIActionHandler* handler = m_currentFrame->getCurrentViewInterface()->actionHandler(); handler->executeAction("WARP\\Load File"); }); loadAction->setToolTip("Load a signature file to match against"); - auto saveIcon = GetColoredIcon(":/icons/images/edit.png", getThemeColor(BlueStandardHighlightColor)); - auto saveAction = headerToolbar->addAction(saveIcon, "Create Signature File", [this]() { - UIActionHandler* handler = m_currentFrame->getCurrentViewInterface()->actionHandler(); - handler->executeAction("WARP\\Create\\From Current View"); - }); - saveAction->setToolTip("Save data to a signature file"); - headerToolbar->addSeparator(); static auto matcherStopIcon = GetColoredIcon(":/icons/images/stop.png", getThemeColor(RedStandardHighlightColor)); @@ -234,10 +232,8 @@ void WarpSidebarWidget::notifyViewLocationChanged(View* view, const ViewLocation // Warp sidebar really should only update if it is visible, otherwise its a waste of cycles. if (!this->isVisible()) return; - auto function = location.getFunction(); - // TODO: Only update if the function exists? // NOTE: The function called will exit early if it is the same function. - m_currentFunctionWidget->SetCurrentFunction(function); + m_currentFunctionWidget->SetCurrentFunction(location.getFunction()); } void WarpSidebarWidget::focus() @@ -249,6 +245,45 @@ void WarpSidebarWidget::focus() WarpSidebarWidgetType::WarpSidebarWidgetType() : SidebarWidgetType(QImage(":/icons/images/warp.png"), "WARP") {} +void RegisterCommands() +{ + RegisterPluginAction( + "Fetch", + [](const UIActionContext& context) { + WarpFetchDialog dlg(context.binaryView, WarpFetcher::Global(), nullptr); + dlg.exec(); + }, + [](const UIActionContext& context) { return context.binaryView != nullptr; }); + RegisterPluginAction("Process", [](const UIActionContext& context) { + auto* dlg = new ProcessorDialog(context.widget); + dlg->setAttribute(Qt::WA_DeleteOnClose); + if (context.binaryView) + dlg->onAddBinaryView(context.binaryView); + dlg->show(); + }); + RegisterPluginAction("View File", [](const UIActionContext& context) { + std::string path; + if (!GetOpenFileNameInput(path, "Open WARP File", "*.warp")) + return; + auto file = Warp::File::FromPath(path); + if (!file) + return; + + auto* dlg = new QDialog(context.widget); + dlg->setWindowTitle(QString::fromStdString("WARP File: " + path)); + dlg->setAttribute(Qt::WA_DeleteOnClose); + + auto* layout = new QVBoxLayout(dlg); + layout->setContentsMargins(10, 10, 10, 10); + auto* fileWidget = new FileWidget(dlg); + fileWidget->setFile(file); + layout->addWidget(fileWidget); + + dlg->resize(1000, 700); + dlg->show(); + }); +} + extern "C" { BN_DECLARE_UI_ABI_VERSION @@ -267,7 +302,7 @@ extern "C" BINARYNINJAPLUGIN bool UIPluginInit() #endif { - RegisterWarpFetchFunctionsCommand(); + RegisterCommands(); Sidebar::addSidebarWidgetType(new WarpSidebarWidgetType()); return true; } diff --git a/plugins/warp/ui/plugin.h b/plugins/warp/ui/plugin.h index 0a4c630c..4ad43864 100644 --- a/plugins/warp/ui/plugin.h +++ b/plugins/warp/ui/plugin.h @@ -48,7 +48,7 @@ class WarpSidebarWidgetType : public SidebarWidgetType public: WarpSidebarWidgetType(); - SidebarWidgetLocation defaultLocation() const override { return SidebarWidgetLocation::RightContent; } + SidebarWidgetLocation defaultLocation() const override { return RightContent; } SidebarContextSensitivity contextSensitivity() const override { return PerViewTypeSidebarContext; } WarpSidebarWidget* createWidget(ViewFrame* viewFrame, BinaryViewRef data) override diff --git a/plugins/warp/ui/shared/chunk.cpp b/plugins/warp/ui/shared/chunk.cpp new file mode 100644 index 00000000..911628eb --- /dev/null +++ b/plugins/warp/ui/shared/chunk.cpp @@ -0,0 +1,159 @@ +#include "chunk.h" + +#include <QAction> +#include <QClipboard> +#include <QGuiApplication> +#include <QHeaderView> +#include <QMenu> +#include <QVBoxLayout> + +#include "misc.h" + +ChunkWidget::ChunkWidget(QWidget* parent) : QWidget(parent) +{ + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(4); + + // Search Box + m_searchBox = new QLineEdit(this); + m_searchBox->setPlaceholderText("Search chunk contents..."); + m_searchBox->setClearButtonEnabled(true); + connect(m_searchBox, &QLineEdit::textChanged, this, &ChunkWidget::onSearchTextChanged); + layout->addWidget(m_searchBox); + + m_countLabel = new QLabel(this); + m_countLabel->setContentsMargins(4, 0, 4, 0); + layout->addWidget(m_countLabel); + + // Table Widget (Styled as a list) + m_table = new QTableWidget(this); + m_table->setColumnCount(3); + m_table->setHorizontalHeaderLabels({"Type", "Name", "ID"}); + m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + m_table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + m_table->setItemDelegateForColumn(1, new TokenDataDelegate(this)); + m_table->setColumnHidden(2, true); + + // Visual tweaks to make it look like a nice list + m_table->verticalHeader()->setVisible(false); + m_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table->setSelectionMode(QAbstractItemView::SingleSelection); + m_table->setShowGrid(false); + m_table->setAlternatingRowColors(false); + m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_table->setStyleSheet("QTableWidget::item { padding: 10px; }"); + + layout->addWidget(m_table); +} + +void ChunkWidget::setChunk(Warp::Ref<Warp::Chunk> chunk) +{ + m_chunk = chunk; + m_searchBox->clear(); + populateTable(); +} + +void ChunkWidget::populateTable() +{ + m_table->setRowCount(0); + if (!m_chunk) + { + updateCountLabel(); + return; + } + + auto functions = m_chunk->GetFunctions(); + auto types = m_chunk->GetTypes(); + + m_table->setRowCount(functions.size() + types.size()); + int row = 0; + + for (const auto& func : functions) + { + m_table->setItem(row, 0, new QTableWidgetItem("Function")); + + auto* nameItem = new QTableWidgetItem(); + std::string symbolName = func->GetSymbolName(); + TokenData tokenData(symbolName); + + if (auto warpType = func->GetType()) + { + if (auto analysisType = warpType->GetAnalysisType()) + tokenData = TokenData(*analysisType, symbolName); + } + + nameItem->setText(QString::fromStdString(symbolName)); // Fallback text for search + nameItem->setData(Qt::UserRole, QVariant::fromValue(tokenData)); + m_table->setItem(row, 1, nameItem); + + auto* idItem = new QTableWidgetItem(QString::fromStdString(func->GetGUID().ToString())); + m_table->setItem(row, 2, idItem); + + row++; + } + + for (const auto& type : types) + { + m_table->setItem(row, 0, new QTableWidgetItem("Type")); + + auto* nameItem = new QTableWidgetItem(); + std::string typeName = type->GetName().value_or(""); + TokenData tokenData(typeName); + + if (auto analysisType = type->GetAnalysisType()) + { + tokenData = TokenData(*analysisType, typeName); + } + + nameItem->setText(QString::fromStdString(typeName)); // Fallback text for search + nameItem->setData(Qt::UserRole, QVariant::fromValue(tokenData)); + m_table->setItem(row, 1, nameItem); + + m_table->setItem(row, 2, new QTableWidgetItem("")); + + row++; + } + + updateCountLabel(); +} + +void ChunkWidget::onSearchTextChanged(const QString& text) +{ + for (int i = 0; i < m_table->rowCount(); ++i) + { + bool match = false; + for (int j = 0; j < m_table->columnCount(); ++j) + { + auto* item = m_table->item(i, j); + if (item && item->text().contains(text, Qt::CaseInsensitive)) + { + match = true; + break; + } + } + m_table->setRowHidden(i, !match); + } + updateCountLabel(); +} + +void ChunkWidget::updateCountLabel() +{ + int totalCount = m_table->rowCount(); + int visibleCount = 0; + for (int i = 0; i < totalCount; ++i) + { + if (!m_table->isRowHidden(i)) + visibleCount++; + } + + if (m_searchBox->text().isEmpty()) + { + m_countLabel->setText(QString::number(totalCount) + " items"); + } + else + { + m_countLabel->setText(QString::number(visibleCount) + " of " + QString::number(totalCount) + " items"); + } +}
\ No newline at end of file diff --git a/plugins/warp/ui/shared/chunk.h b/plugins/warp/ui/shared/chunk.h new file mode 100644 index 00000000..a03a8eae --- /dev/null +++ b/plugins/warp/ui/shared/chunk.h @@ -0,0 +1,28 @@ +#pragma once + +#include <QWidget> +#include <QTableWidget> +#include <QLineEdit> +#include <QLabel> +#include "warp.h" + +class ChunkWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ChunkWidget(QWidget* parent = nullptr); + void setChunk(Warp::Ref<Warp::Chunk> chunk); + +private slots: + void onSearchTextChanged(const QString& text); + +private: + void populateTable(); + void updateCountLabel(); + + Warp::Ref<Warp::Chunk> m_chunk; + QLineEdit* m_searchBox; + QLabel* m_countLabel; + QTableWidget* m_table; +};
\ No newline at end of file diff --git a/plugins/warp/ui/shared/commitdialog.cpp b/plugins/warp/ui/shared/commitdialog.cpp new file mode 100644 index 00000000..1ea6f4ba --- /dev/null +++ b/plugins/warp/ui/shared/commitdialog.cpp @@ -0,0 +1,141 @@ +#include "commitdialog.h" +#include "file.h" +#include "misc.h" + +#include <QHBoxLayout> +#include <QFormLayout> +#include <QMessageBox> +#include <QPushButton> +#include <QTimer> + +CommitDialog::CommitDialog(Warp::Ref<Warp::File> file, QWidget* parent) : QDialog(parent), m_file(file) +{ + setWindowModality(Qt::NonModal); + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + setWindowTitle("Commit to Source"); + setMinimumSize(300, 200); + + auto* mainLayout = new QVBoxLayout(this); + + auto* commitFormLayout = new QFormLayout(); + + m_containerCombo = new QComboBox(this); + connect( + m_containerCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CommitDialog::onContainerChanged); + + m_sourcesView = new WarpSourcesView(this); + m_proxyModel = new QSortFilterProxyModel(this); + m_proxyModel->setSourceModel(m_sourcesView->sourceModel()); + m_proxyModel->setFilterKeyColumn(WarpSourcesModel::PathCol); + m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_sourcesView->setModel(m_proxyModel); + + auto* sourceLayout = new QVBoxLayout(); + + auto* filterLayout = new QHBoxLayout(); + m_sourceFilter = new QLineEdit(this); + m_sourceFilter->setPlaceholderText("Filter sources..."); + connect(m_sourceFilter, &QLineEdit::textChanged, m_proxyModel, &QSortFilterProxyModel::setFilterFixedString); + filterLayout->addWidget(m_sourceFilter); + + m_addSourceButton = new QPushButton("+", this); + m_addSourceButton->setFixedWidth(30); + m_addSourceButton->setToolTip("Add source"); + connect(m_addSourceButton, &QPushButton::clicked, this, &CommitDialog::onCreateNewSource); + filterLayout->addWidget(m_addSourceButton); + + sourceLayout->addLayout(filterLayout); + sourceLayout->addWidget(m_sourcesView); + + commitFormLayout->addRow("Container:", m_containerCombo); + commitFormLayout->addRow("Source:", sourceLayout); + + auto commitBtnLabel = QString("Commit %1 chunks").arg(m_file->GetChunks().size()); + m_commitButton = new QPushButton(commitBtnLabel, this); + connect(m_commitButton, &QPushButton::clicked, this, &CommitDialog::onCommit); + + mainLayout->addLayout(commitFormLayout); + mainLayout->addWidget(m_commitButton, 0, Qt::AlignRight); + + populateContainers(); + + if (!m_containers.empty()) + m_sourcesView->setContainer(m_containers[m_containerCombo->currentIndex()]); +} + +void CommitDialog::populateContainers() +{ + m_containers = Warp::Container::All(); + m_containerCombo->clear(); + for (const auto& container : m_containers) + m_containerCombo->addItem(QString::fromStdString(container->GetName())); +} + +void CommitDialog::onContainerChanged(int index) +{ + if (index >= 0 && index < m_containers.size()) + m_sourcesView->setContainer(m_containers[index]); +} + +void CommitDialog::onCreateNewSource() +{ + if (m_sourcesView->addSource()) + { + // Select the newly added source + int rowCount = m_sourcesView->sourceModel()->rowCount(); + if (rowCount > 0) + { + QModelIndex sourceIdx = m_sourcesView->sourceModel()->index(rowCount - 1, WarpSourcesModel::PathCol); + m_sourcesView->setCurrentIndex(m_proxyModel->mapFromSource(sourceIdx)); + } + } +} + +void CommitDialog::onCommit() +{ + if (!m_file) + return; + int containerIdx = m_containerCombo->currentIndex(); + QModelIndex proxyIdx = m_sourcesView->currentIndex(); + + if (m_file->GetChunks().empty()) + { + QMessageBox::critical(this, "Error", "No chunks to commit."); + return; + } + + if (containerIdx < 0 || !proxyIdx.isValid()) + { + QMessageBox::critical(this, "Error", "No source selected, please select a source."); + return; + } + + auto container = m_containers[containerIdx]; + QModelIndex sourceIdx = m_proxyModel->mapToSource(proxyIdx); + auto optSource = m_sourcesView->sourceFromRow(sourceIdx.row()); + if (!optSource.has_value()) + { + QMessageBox::critical(this, "Error", "Failed to retrieve the selected source."); + return; + } + auto source = optSource.value(); + + m_commitButton->setEnabled(false); + m_commitButton->setText("Committing..."); + + auto* worker = new WarpCommitWorker(container, source, m_file); + connect(worker, &WarpCommitWorker::finishedCommitting, this, &CommitDialog::onCommitFinished); + connect(worker, &WarpCommitWorker::finished, worker, &QObject::deleteLater); + worker->start(); +} + +void CommitDialog::onCommitFinished(bool success) +{ + m_commitButton->setEnabled(true); + m_commitButton->setText("Commit"); + + if (success) + QMessageBox::information(this, "Success", "Successfully committed to the source."); + else + QMessageBox::critical(this, "Error", "Failed to commit to the source."); +} diff --git a/plugins/warp/ui/shared/commitdialog.h b/plugins/warp/ui/shared/commitdialog.h new file mode 100644 index 00000000..ccbe8da1 --- /dev/null +++ b/plugins/warp/ui/shared/commitdialog.h @@ -0,0 +1,74 @@ +#pragma once + +#include <QDialog> +#include <QStackedWidget> +#include <QComboBox> +#include <QProgressBar> +#include <QThread> +#include <QVBoxLayout> +#include <QLineEdit> +#include <QPushButton> +#include <QSortFilterProxyModel> + +#include "binaryninjaapi.h" +#include "source.h" +#include "warp.h" +#include "source.h" + +// Worker to commit a file to a container +class WarpCommitWorker : public QThread +{ + Q_OBJECT + + Warp::Ref<Warp::Container> m_container; + Warp::Source m_source; + Warp::Ref<Warp::File> m_file; + +public: + WarpCommitWorker(Warp::Ref<Warp::Container> container, Warp::Source source, Warp::Ref<Warp::File> file, + QObject* parent = nullptr) : QThread(parent), m_container(container), m_source(source), m_file(file) + {} + + void run() override + { + for (const auto& chunk : m_file->GetChunks()) + { + if (auto target = chunk->GetTarget()) + m_container->AddFunctions(*target, m_source, chunk->GetFunctions()); + m_container->AddTypes(m_source, chunk->GetTypes()); + } + + const bool result = m_container->CommitSource(m_source); + emit finishedCommitting(result); + } + +signals: + void finishedCommitting(bool success); +}; + +class CommitDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CommitDialog(Warp::Ref<Warp::File> file, QWidget* parent = nullptr); + +private slots: + void onContainerChanged(int index); + void onCreateNewSource(); + void onCommit(); + void onCommitFinished(bool success); + +private: + void populateContainers(); + + Warp::Ref<Warp::File> m_file; + std::vector<Warp::Ref<Warp::Container>> m_containers; + + QComboBox* m_containerCombo; + QLineEdit* m_sourceFilter; + QPushButton* m_addSourceButton; + QSortFilterProxyModel* m_proxyModel; + WarpSourcesView* m_sourcesView; + QPushButton* m_commitButton; +};
\ No newline at end of file diff --git a/plugins/warp/ui/shared/fetchdialog.cpp b/plugins/warp/ui/shared/fetchdialog.cpp index 44d7c237..41dea3e0 100644 --- a/plugins/warp/ui/shared/fetchdialog.cpp +++ b/plugins/warp/ui/shared/fetchdialog.cpp @@ -25,7 +25,7 @@ static void AddListItem(QListWidget* list, const QString& value) WarpFetchDialog::WarpFetchDialog(BinaryViewRef bv, std::shared_ptr<WarpFetcher> fetcher, QWidget* parent) : QDialog(parent), m_fetchProcessor(std::move(fetcher)), m_bv(std::move(bv)) { - setWindowTitle("Fetch WARP Functions"); + setWindowTitle("WARP Fetcher"); auto form = new QFormLayout(); m_containerCombo = new QComboBox(this); @@ -160,8 +160,8 @@ void WarpFetchDialog::onReject() reject(); } -void WarpFetchDialog::runBatchedFetch(const std::optional<size_t>& containerIndex, - const std::vector<Warp::SourceTag>& allowedTags, bool rerunMatcher) +void WarpFetchDialog::runBatchedFetch( + const std::optional<size_t>& containerIndex, const std::vector<Warp::SourceTag>& allowedTags, bool rerunMatcher) { if (!m_bv) return; @@ -178,48 +178,27 @@ void WarpFetchDialog::runBatchedFetch(const std::optional<size_t>& containerInde auto bv = m_bv; // TODO: Too many captures in this thing lol. - WorkerInteractiveEnqueue( - [fetcher, bv, funcs = std::move(funcs), rerunMatcher, task, allowedTags]() mutable { - const auto batchSize = GetBatchSizeFromView(bv); - size_t processed = 0; - while (processed < funcs.size()) - { - if (task->IsCancelled()) - break; - const size_t remaining = funcs.size() - processed; - const size_t thisBatchCount = std::min(batchSize, remaining); - for (size_t i = 0; i < thisBatchCount; ++i) - fetcher->AddPendingFunction(funcs[processed + i]); - fetcher->FetchPendingFunctions(allowedTags); - processed += thisBatchCount; - task->SetProgressText("Fetching WARP functions (" + std::to_string(processed) + " / " + std::to_string(funcs.size()) + ")"); - } + WorkerInteractiveEnqueue([fetcher, bv, funcs = std::move(funcs), rerunMatcher, task, allowedTags]() mutable { + const auto batchSize = GetBatchSizeFromView(bv); + size_t processed = 0; + while (processed < funcs.size()) + { + if (task->IsCancelled()) + break; + const size_t remaining = funcs.size() - processed; + const size_t thisBatchCount = std::min(batchSize, remaining); + for (size_t i = 0; i < thisBatchCount; ++i) + fetcher->AddPendingFunction(funcs[processed + i]); + fetcher->FetchPendingFunctions(allowedTags); + processed += thisBatchCount; + task->SetProgressText( + "Fetching WARP functions (" + std::to_string(processed) + " / " + std::to_string(funcs.size()) + ")"); + } - task->Finish(); - Logger("WARP Fetcher").LogInfo("Finished fetching WARP functions in %d seconds...", task->GetRuntimeSeconds()); + task->Finish(); + Logger("WARP Fetcher").LogInfo("Finished fetching WARP functions in %d seconds...", task->GetRuntimeSeconds()); - if (rerunMatcher && bv) - Warp::RunMatcher(*bv); - }); -} - -void RegisterWarpFetchFunctionsCommand() -{ - // Register a UI action and bind it globally. Add it to the Tools menu. - const QString actionName = "WARP\\Fetch"; - if (!UIAction::isActionRegistered(actionName)) - UIAction::registerAction(actionName); - - UIActionHandler::globalActions()->bindAction(actionName, - UIAction( - [](const UIActionContext& context) { - if (const BinaryViewRef bv = context.binaryView; bv) - { - WarpFetchDialog dlg(bv, WarpFetcher::Global(), nullptr); - dlg.exec(); - } - }, - [](const UIActionContext& context) { return context.binaryView != nullptr; })); - - Menu::mainMenu("Plugins")->addAction(actionName, "Plugins"); + if (rerunMatcher && bv) + Warp::RunMatcher(*bv); + }); } diff --git a/plugins/warp/ui/shared/fetchdialog.h b/plugins/warp/ui/shared/fetchdialog.h index 72b8c57e..68fca6c1 100644 --- a/plugins/warp/ui/shared/fetchdialog.h +++ b/plugins/warp/ui/shared/fetchdialog.h @@ -52,5 +52,3 @@ private: void runBatchedFetch(const std::optional<size_t>& containerIndex, const std::vector<Warp::SourceTag>& allowedTags, bool rerunMatcher); }; - -void RegisterWarpFetchFunctionsCommand(); diff --git a/plugins/warp/ui/shared/fetcher.cpp b/plugins/warp/ui/shared/fetcher.cpp index 2eab910a..f5aa626a 100644 --- a/plugins/warp/ui/shared/fetcher.cpp +++ b/plugins/warp/ui/shared/fetcher.cpp @@ -73,7 +73,8 @@ void WarpFetcher::FetchPendingFunctions(const std::vector<Warp::SourceTag>& allo auto platform = func->GetPlatform(); platformMappedGuidSet[platform].insert(warpFunc->GetGUID()); - // We want to keep track of the guids so we can constrain the server response to only return functions with any of them. + // We want to keep track of the guids so we can constrain the server response to only return functions with any + // of them. const auto constraints = warpFunc->GetConstraints(); std::vector<Warp::ConstraintGUID> constraintGuids; constraintGuids.reserve(constraints.size()); diff --git a/plugins/warp/ui/shared/fetcher.h b/plugins/warp/ui/shared/fetcher.h index 3464ecd4..ad38ed60 100644 --- a/plugins/warp/ui/shared/fetcher.h +++ b/plugins/warp/ui/shared/fetcher.h @@ -24,6 +24,7 @@ class WarpFetcher std::mutex m_requestMutex; std::vector<FunctionRef> m_pendingRequests; std::unordered_set<Warp::FunctionGUID> m_processedGuids; + public: using CallbackId = uint64_t; using CompletionCallback = std::function<WarpFetchCompletionStatus()>; diff --git a/plugins/warp/ui/shared/file.cpp b/plugins/warp/ui/shared/file.cpp new file mode 100644 index 00000000..70a169fe --- /dev/null +++ b/plugins/warp/ui/shared/file.cpp @@ -0,0 +1,71 @@ +#include "file.h" + +#include <QVBoxLayout> + +FileWidget::FileWidget(QWidget* parent) : QWidget(parent) +{ + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + auto* splitter = new QSplitter(Qt::Horizontal, this); + + // Left side: List of chunks + m_list = new QListWidget(this); + m_list->setSelectionMode(QAbstractItemView::SingleSelection); + m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_list->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + m_list->setUniformItemSizes(true); + connect(m_list, &QListWidget::itemSelectionChanged, this, &FileWidget::onListSelectionChanged); + splitter->addWidget(m_list); + + // Right side: Chunk Widget + m_chunkWidget = new ChunkWidget(this); + splitter->addWidget(m_chunkWidget); + + splitter->setSizes({100, 900}); + layout->addWidget(splitter); +} + +void FileWidget::setFile(Warp::Ref<Warp::File> file) +{ + m_file = file; + m_list->clear(); + m_chunkWidget->setChunk(nullptr); + m_currentChunks.clear(); + + if (!m_file) + return; + + m_currentChunks = m_file->GetChunks(); + for (size_t i = 0; i < m_currentChunks.size(); ++i) + { + auto* listItem = new QListWidgetItem(m_list); + listItem->setText(QString("Chunk #%1").arg(i + 1)); + // Store the chunk index in the item's data for easy retrieval + listItem->setData(Qt::UserRole, QVariant::fromValue(static_cast<qulonglong>(i))); + } + + if (m_list->count() > 0) + m_list->setCurrentRow(0); +} + +void FileWidget::onListSelectionChanged() +{ + auto selectedItems = m_list->selectedItems(); + if (selectedItems.isEmpty()) + { + m_chunkWidget->setChunk(nullptr); + return; + } + + auto* item = selectedItems.first(); + QVariant data = item->data(Qt::UserRole); + if (data.isValid()) + { + size_t index = data.value<qulonglong>(); + if (index < m_currentChunks.size()) + { + m_chunkWidget->setChunk(m_currentChunks[index]); + } + } +}
\ No newline at end of file diff --git a/plugins/warp/ui/shared/file.h b/plugins/warp/ui/shared/file.h new file mode 100644 index 00000000..057fd2cd --- /dev/null +++ b/plugins/warp/ui/shared/file.h @@ -0,0 +1,25 @@ +#pragma once + +#include <QWidget> +#include <QListWidget> +#include <QSplitter> +#include "warp.h" +#include "chunk.h" + +class FileWidget : public QWidget +{ + Q_OBJECT + +public: + explicit FileWidget(QWidget* parent = nullptr); + void setFile(Warp::Ref<Warp::File> file); + +private slots: + void onListSelectionChanged(); + +private: + Warp::Ref<Warp::File> m_file; + QListWidget* m_list; + ChunkWidget* m_chunkWidget; + std::vector<Warp::Ref<Warp::Chunk>> m_currentChunks; +};
\ No newline at end of file diff --git a/plugins/warp/ui/shared/function.cpp b/plugins/warp/ui/shared/function.cpp index 3b2451ab..fd403d77 100644 --- a/plugins/warp/ui/shared/function.cpp +++ b/plugins/warp/ui/shared/function.cpp @@ -21,8 +21,8 @@ WarpFunctionItem::WarpFunctionItem( // Serialize the tokens to make it accessible via QModelIndex. // We will take these tokens and then user them in our custom item delegate. TokenData tokenData = TokenData(symbolName); - if (BinaryNinja::Ref<BinaryNinja::Type> type = m_function->GetType(*analysisFunction)) - tokenData = TokenData(*type, symbolName); + if (Warp::Ref<Warp::Type> warpType = m_function->GetType()) + tokenData = TokenData(*warpType->GetAnalysisType(analysisFunction->GetArchitecture()), symbolName); setData(QVariant::fromValue(tokenData), Qt::UserRole); } @@ -256,8 +256,7 @@ void WarpFunctionTableWidget::RegisterContextMenuAction( m_contextMenuActions[name] = callback; } -void WarpFunctionTableWidget::RegisterContextMenuAction( - const QString& name, +void WarpFunctionTableWidget::RegisterContextMenuAction(const QString& name, const std::function<void(WarpFunctionItem*, std::optional<uint64_t>)>& callback, const std::function<bool(WarpFunctionItem*, std::optional<uint64_t>)>& isValid) { diff --git a/plugins/warp/ui/shared/function.h b/plugins/warp/ui/shared/function.h index c6e5ba63..4cf226c7 100644 --- a/plugins/warp/ui/shared/function.h +++ b/plugins/warp/ui/shared/function.h @@ -112,10 +112,9 @@ public: void RegisterContextMenuAction( const QString& name, const std::function<void(WarpFunctionItem*, std::optional<uint64_t>)>& callback); - void RegisterContextMenuAction( - const QString &name, - const std::function<void(WarpFunctionItem *, std::optional<uint64_t>)> &callback, - const std::function<bool(WarpFunctionItem *, std::optional<uint64_t>)> &isValid); + void RegisterContextMenuAction(const QString& name, + const std::function<void(WarpFunctionItem*, std::optional<uint64_t>)>& callback, + const std::function<bool(WarpFunctionItem*, std::optional<uint64_t>)>& isValid); void SetFunctions(QVector<WarpFunctionItem*> functions); diff --git a/plugins/warp/ui/shared/misc.cpp b/plugins/warp/ui/shared/misc.cpp index 6de314b1..c88cee10 100644 --- a/plugins/warp/ui/shared/misc.cpp +++ b/plugins/warp/ui/shared/misc.cpp @@ -72,6 +72,75 @@ void AddressColorDelegate::paint(QPainter* painter, const QStyleOptionViewItem& QStyledItemDelegate::paint(painter, opt, index); } +void SourcePathDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + + // Draw background and selection highlights + opt.widget->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget); + + QString text = index.data(Qt::DisplayRole).toString(); + int sepIdx = qMax(text.lastIndexOf('/'), text.lastIndexOf('\\')); + QString dirPart = sepIdx != -1 ? text.left(sepIdx + 1) : ""; + QString filePart = sepIdx != -1 ? text.mid(sepIdx + 1) : text; + + QFont regularFont = opt.font; + QFont boldFont = regularFont; + boldFont.setBold(true); + + QFontMetrics fmReg(regularFont); + QFontMetrics fmBold(boldFont); + + // Basic padding inside the list item + QRect textRect = opt.rect.adjusted(3, 0, -3, 0); + + int fileWidth = fmBold.horizontalAdvance(filePart); + int dirWidth = fmReg.horizontalAdvance(dirPart); + + QString textToDrawDir; + QString textToDrawFile = filePart; + + if (dirWidth + fileWidth > textRect.width()) + { + if (fileWidth > textRect.width()) + { + // The file name itself is too long, elide it + textToDrawDir = ""; + textToDrawFile = fmBold.elidedText(filePart, Qt::ElideLeft, textRect.width()); + } + else + { + // Elide the directory part so the bold file name fits + textToDrawDir = fmReg.elidedText(dirPart, Qt::ElideLeft, textRect.width() - fileWidth); + } + } + else + { + textToDrawDir = dirPart; + } + + painter->save(); + + // Set the proper text color based on selection state + if (opt.state & QStyle::State_Selected) + painter->setPen(opt.palette.highlightedText().color()); + else + painter->setPen(opt.palette.text().color()); + + // Draw the directory part + painter->setFont(regularFont); + painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, textToDrawDir); + + // Draw the file part + painter->setFont(boldFont); + int dirAdvance = fmReg.horizontalAdvance(textToDrawDir); + QRect fileRect = textRect.adjusted(dirAdvance, 0, 0, 0); + painter->drawText(fileRect, Qt::AlignLeft | Qt::AlignVCenter, textToDrawFile); + + painter->restore(); +} + bool GenericTextFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { auto filterString = filterRegularExpression().pattern(); @@ -142,13 +211,14 @@ ParsedQuery::ParsedQuery(const QString& rawQuery) query = query.simplified(); } -WarpRemoveMatchDialog::WarpRemoveMatchDialog(QWidget *parent, FunctionRef func) : QDialog(parent), m_func(func) +WarpRemoveMatchDialog::WarpRemoveMatchDialog(QWidget* parent, FunctionRef func) : QDialog(parent), m_func(func) { setWindowTitle("Remove Matching Function"); setModal(true); auto* vbox = new QVBoxLayout(this); - auto* text = new QLabel("Remove the match for this function? You can also mark it as ignored to prevent future automatic matches."); + auto* text = new QLabel( + "Remove the match for this function? You can also mark it as ignored to prevent future automatic matches."); text->setWordWrap(true); vbox->addWidget(text); diff --git a/plugins/warp/ui/shared/misc.h b/plugins/warp/ui/shared/misc.h index 92e27fd8..e9d6fd43 100644 --- a/plugins/warp/ui/shared/misc.h +++ b/plugins/warp/ui/shared/misc.h @@ -67,6 +67,14 @@ public: void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; +class SourcePathDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + explicit SourcePathDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {} + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; +}; class GenericTextFilterModel : public QSortFilterProxyModel { @@ -109,6 +117,7 @@ struct ParsedQuery class WarpRemoveMatchDialog : public QDialog { Q_OBJECT + public: explicit WarpRemoveMatchDialog(QWidget* parent, FunctionRef func); @@ -116,7 +125,7 @@ public: private: FunctionRef m_func; - QCheckBox* m_ignoreCheck{nullptr}; + QCheckBox* m_ignoreCheck {nullptr}; }; constexpr const char* ALLOWED_TAGS_SETTING = "warp.fetcher.allowedSourceTags"; @@ -144,4 +153,15 @@ inline size_t GetBatchSizeFromView(const BinaryViewRef& view) if (!settings->Contains(BATCH_SIZE_SETTING)) return 10000; return settings->Get<uint64_t>(BATCH_SIZE_SETTING, view); +} + +inline void RegisterPluginAction( + std::string name, std::function<void(const UIActionContext&)> action, + std::function<bool(const UIActionContext&)> isValid = [](const UIActionContext&) { return true; }) +{ + const QString actionName = QString("WARP\\%1").arg(QString::fromStdString(name)); + if (!UIAction::isActionRegistered(actionName)) + UIAction::registerAction(actionName); + UIActionHandler::globalActions()->bindAction(actionName, UIAction(action, isValid)); + Menu::mainMenu("Plugins")->addAction(actionName, "Plugins"); }
\ No newline at end of file diff --git a/plugins/warp/ui/shared/processordialog.cpp b/plugins/warp/ui/shared/processordialog.cpp new file mode 100644 index 00000000..9e602b79 --- /dev/null +++ b/plugins/warp/ui/shared/processordialog.cpp @@ -0,0 +1,451 @@ +#include "processordialog.h" +#include "commitdialog.h" +#include "misc.h" +#include "selectprojectfilesdialog.h" + +#include <QHBoxLayout> +#include <QFormLayout> +#include <QFileDialog> +#include <QFile> +#include <QMessageBox> +#include <QMenu> +#include <QContextMenuEvent> +#include <QDirIterator> +#include <QFileInfo> +#include <QDir> + +using namespace BinaryNinja; + +ProcessorDialog::ProcessorDialog(QWidget* parent) : QDialog(parent) +{ + setWindowModality(Qt::NonModal); + setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); + setWindowTitle("WARP Processor"); + setMinimumSize(400, 300); + + auto* mainLayout = new QVBoxLayout(this); + m_stack = new QStackedWidget(this); + mainLayout->addWidget(m_stack); + + // Page 1: Configuration + auto* configPage = new QWidget(this); + auto* configLayout = new QVBoxLayout(configPage); + + auto* entrySearchLayout = new QHBoxLayout(); + entrySearchLayout->setContentsMargins(0, 0, 0, 0); + m_entrySearch = new QLineEdit(this); + m_entrySearch->setPlaceholderText("Search entries..."); + connect(m_entrySearch, &QLineEdit::textChanged, this, &ProcessorDialog::onSearchItems); + entrySearchLayout->addWidget(m_entrySearch); + + m_addButton = new QPushButton("+", this); + m_addButton->setFixedWidth(30); + m_addButton->setToolTip("Add entries"); + connect(m_addButton, &QPushButton::clicked, this, &ProcessorDialog::onAddEntryMenu); + entrySearchLayout->addWidget(m_addButton); + configLayout->addLayout(entrySearchLayout); + + m_entryList = new QListWidget(this); + m_entryList->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_entryList->setContextMenuPolicy(Qt::CustomContextMenu); + m_entryList->setTextElideMode(Qt::ElideLeft); + m_entryList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_entryList->setStyleSheet("QListWidget::item { padding: 2px; }"); + connect(m_entryList, &QListWidget::customContextMenuRequested, this, &ProcessorDialog::showContextMenu); + configLayout->addWidget(m_entryList); + + auto* formLayout = new QFormLayout(); + m_includedDataCombo = new QComboBox(this); + m_includedDataCombo->addItem("Symbols", WARPProcessorIncludedDataSymbols); + m_includedDataCombo->addItem("Signatures", WARPProcessorIncludedDataSignatures); + m_includedDataCombo->addItem("Types", WARPProcessorIncludedDataTypes); + m_includedDataCombo->addItem("All", WARPProcessorIncludedDataAll); + m_includedDataCombo->setCurrentIndex(3); + + m_includedFunctionsCombo = new QComboBox(this); + m_includedFunctionsCombo->addItem("Selected", WARPProcessorIncludedFunctionsSelected); + m_includedFunctionsCombo->addItem("Annotated", WARPProcessorIncludedFunctionsAnnotated); + m_includedFunctionsCombo->addItem("All", WARPProcessorIncludedFunctionsAll); + m_includedFunctionsCombo->setCurrentIndex(1); + + m_workerCountSpinBox = new QSpinBox(this); + m_workerCountSpinBox->setMinimum(2); + m_workerCountSpinBox->setValue(GetWorkerThreadCount()); + + formLayout->addRow("Included Data:", m_includedDataCombo); + formLayout->addRow("Included Functions:", m_includedFunctionsCombo); + formLayout->addRow("Worker Count:", m_workerCountSpinBox); + configLayout->addLayout(formLayout); + + m_processButton = new QPushButton("Process", this); + m_processButton->setEnabled(false); + connect(m_processButton, &QPushButton::clicked, this, &ProcessorDialog::onStartProcessing); + configLayout->addWidget(m_processButton, 0, Qt::AlignRight); + m_stack->addWidget(configPage); + + // Page 2: Processing + auto* processPage = new QWidget(this); + auto* processLayout = new QVBoxLayout(processPage); + m_processingLabel = new QLabel("Processing...", this); + m_processingLabel->setAlignment(Qt::AlignCenter); + m_progressBar = new QProgressBar(this); + m_progressBar->setRange(0, 0); + + m_stateList = new QListWidget(this); + m_stateList->setSelectionMode(QAbstractItemView::NoSelection); + m_stateList->setFocusPolicy(Qt::NoFocus); + m_stateList->setTextElideMode(Qt::ElideLeft); + m_stateList->setWordWrap(false); + m_stateList->setStyleSheet( + "QListWidget { background: transparent; border: none; } QListWidget::item { padding: 1px; }"); + m_stateList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_stateList->setMinimumHeight(100); + + m_cancelButton = new QPushButton("Cancel", this); + connect(m_cancelButton, &QPushButton::clicked, this, &ProcessorDialog::onCancelProcessing); + + m_updateTimer = new QTimer(this); + connect(m_updateTimer, &QTimer::timeout, this, &ProcessorDialog::onUpdateState); + + processLayout->addStretch(); + processLayout->addWidget(m_processingLabel); + processLayout->addWidget(m_progressBar); + processLayout->addWidget(m_stateList); + processLayout->addStretch(); + processLayout->addWidget(m_cancelButton, 0, Qt::AlignRight); + m_stack->addWidget(processPage); + + // Page 3: Results + auto* resultsPage = new QWidget(this); + auto* resultsLayout = new QVBoxLayout(resultsPage); + m_fileWidget = new FileWidget(this); + resultsLayout->addWidget(m_fileWidget); + + auto* buttonLayout = new QHBoxLayout(); + m_saveButton = new QPushButton("Save to File", this); + connect(m_saveButton, &QPushButton::clicked, this, &ProcessorDialog::onSaveToFile); + m_elapsedLabel = new QLabel(this); + m_commitButton = new QPushButton("Commit", this); + connect(m_commitButton, &QPushButton::clicked, this, &ProcessorDialog::onCommit); + buttonLayout->addWidget(m_elapsedLabel); + buttonLayout->addStretch(); + buttonLayout->addWidget(m_saveButton); + buttonLayout->addWidget(m_commitButton); + resultsLayout->addLayout(buttonLayout); + m_stack->addWidget(resultsPage); + + m_stack->setCurrentIndex(ConfigurationPage); +} + +ProcessorDialog::~ProcessorDialog() +{ + m_updateTimer->stop(); + if (m_processor) + m_processor->Cancel(); +} + +void ProcessorDialog::onStartProcessing() +{ + if (m_toProcess.empty()) + return; + + auto includedData = static_cast<BNWARPProcessorIncludedData>(m_includedDataCombo->currentData().toInt()); + auto includedFunctions = + static_cast<BNWARPProcessorIncludedFunctions>(m_includedFunctionsCombo->currentData().toInt()); + auto workerCount = static_cast<size_t>(m_workerCountSpinBox->value()); + + m_processor = std::make_shared<Warp::Processor>(includedData, includedFunctions, workerCount); + + for (const auto& item : m_toProcess) + { + switch (item.type) + { + case ToProcessEntry::ViewMode: + m_processor->AddBinaryView(*item.view); + break; + case ToProcessEntry::PathMode: + m_processor->AddPath(item.path); + break; + case ToProcessEntry::ProjectMode: + m_processor->AddProject(*item.project); + break; + case ToProcessEntry::ProjectFileMode: + m_processor->AddProjectFile(*item.projectFile); + break; + } + } + + m_stack->setCurrentIndex(ProcessingPage); + m_processTimer.start(); + auto* worker = new WarpProcessorWorker(m_processor); + connect(worker, &WarpProcessorWorker::finishedProcessing, this, &ProcessorDialog::onProcessingFinished); + connect(worker, &WarpProcessorWorker::finished, worker, &QObject::deleteLater); + worker->start(); + + m_cancelButton->setEnabled(true); + m_updateTimer->start(100); +} + +void ProcessorDialog::onAddEntryMenu() +{ + QMenu menu(nullptr); + addAddActionsToMenu(&menu); + menu.exec(m_addButton->mapToGlobal(QPoint(0, m_addButton->height()))); +} + +void ProcessorDialog::addAddActionsToMenu(QMenu* menu) +{ + menu->addAction("Add Files...", this, &ProcessorDialog::onAddPath); + menu->addAction("Add Directory...", this, &ProcessorDialog::onAddDirectory); + menu->addAction("Add Project Files", this, &ProcessorDialog::onAddProjectFiles); +} + +void ProcessorDialog::showContextMenu(const QPoint& pos) +{ + QMenu menu(nullptr); + addAddActionsToMenu(&menu); + + QListWidgetItem* item = m_entryList->itemAt(pos); + if (item) + { + menu.addSeparator(); + menu.addAction("Remove", this, &ProcessorDialog::onRemoveItem); + } + + menu.exec(m_entryList->mapToGlobal(pos)); +} + +void ProcessorDialog::onAddBinaryView(Ref<BinaryView> view) +{ + ToProcessEntry item; + item.type = ToProcessEntry::ViewMode; + item.view = view; + item.displayName = QString("View: %1").arg(QString::fromStdString(view->GetFile()->GetFilename())); + + m_toProcess.push_back(item); + m_entryList->addItem(item.displayName); + onSearchItems(); + m_processButton->setEnabled(true); +} + +void ProcessorDialog::onAddPath() +{ + QStringList paths = QFileDialog::getOpenFileNames(this, "Select Files", "", "All Files (*)"); + if (paths.isEmpty()) + return; + + for (const auto& path : paths) + addPathRecursive(path); + + onSearchItems(); + m_processButton->setEnabled(true); +} + +void ProcessorDialog::onAddDirectory() +{ + QString path = QFileDialog::getExistingDirectory(this, "Select Directory", ""); + if (path.isEmpty()) + return; + + addPathRecursive(path); + + onSearchItems(); + m_processButton->setEnabled(true); +} + +void ProcessorDialog::addPathRecursive(const QString& path) +{ + QFileInfo info(path); + if (info.isDir()) + { + QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) + { + addSinglePath(it.next()); + } + } + else + { + addSinglePath(path); + } +} + +void ProcessorDialog::addSinglePath(const QString& path) +{ + ToProcessEntry item; + item.type = ToProcessEntry::PathMode; + item.path = path.toStdString(); + item.displayName = QString("Path: %1").arg(path); + + m_toProcess.push_back(item); + m_entryList->addItem(item.displayName); +} + +void ProcessorDialog::onAddProjectFiles() +{ + auto projects = Project::GetOpenProjects(); + if (projects.empty()) + { + QMessageBox::information(this, "Add Project Files", "No projects are currently open."); + return; + } + + SelectProjectFilesDialog dlg(this); + if (dlg.exec() == Accepted) + { + auto selectedFiles = dlg.getSelectedFiles(); + if (selectedFiles.empty()) + { + // If no files selected, add the entire project + auto project = dlg.getSelectedProject(); + ToProcessEntry item; + item.type = ToProcessEntry::ProjectMode; + item.project = project; + item.displayName = QString("Project: %1").arg(QString::fromStdString(project->GetName())); + m_toProcess.push_back(item); + m_entryList->addItem(item.displayName); + } + else + { + for (auto& file : selectedFiles) + { + ToProcessEntry item; + item.type = ToProcessEntry::ProjectFileMode; + item.projectFile = file; + item.displayName = QString("File: %1").arg(QString::fromStdString(file->GetName())); + m_toProcess.push_back(item); + m_entryList->addItem(item.displayName); + } + } + onSearchItems(); + m_processButton->setEnabled(true); + } +} + +void ProcessorDialog::onRemoveItem() +{ + auto selectedItems = m_entryList->selectedItems(); + if (selectedItems.isEmpty()) + { + int row = m_entryList->currentRow(); + if (row >= 0 && row < (int)m_toProcess.size()) + { + m_toProcess.erase(m_toProcess.begin() + row); + delete m_entryList->takeItem(row); + } + } + else + { + for (auto* item : selectedItems) + { + int row = m_entryList->row(item); + if (row >= 0 && row < (int)m_toProcess.size()) + { + m_toProcess.erase(m_toProcess.begin() + row); + delete m_entryList->takeItem(row); + } + } + } + m_processButton->setEnabled(!m_toProcess.empty()); +} + +void ProcessorDialog::onSearchItems() +{ + QString filter = m_entrySearch->text().toLower(); + for (int i = 0; i < m_entryList->count(); ++i) + { + auto* item = m_entryList->item(i); + item->setHidden(!item->text().toLower().contains(filter)); + } +} + +void ProcessorDialog::onProcessingFinished(Warp::Ref<Warp::File> file) +{ + m_updateTimer->stop(); + + if (!file) + { + QMessageBox::critical(this, "Error", "Failed to process the selected input."); + m_stack->setCurrentIndex(ConfigurationPage); + return; + } + + auto elapsed = m_processTimer.elapsed(); + if (elapsed < 1000) + m_elapsedLabel->setText(QString("Processing took: %1ms").arg(elapsed)); + else + m_elapsedLabel->setText(QString("Processing took: %1s").arg(elapsed / 1000.0, 0, 'f', 2)); + + m_file = file; + m_fileWidget->setFile(m_file); + m_stack->setCurrentIndex(ResultsPage); +} + +void ProcessorDialog::onCancelProcessing() +{ + if (m_processor) + m_processor->Cancel(); + m_cancelButton->setEnabled(false); + m_processingLabel->setText("Cancelling..."); +} + +void ProcessorDialog::onUpdateState() +{ + if (!m_processor) + return; + + auto state = m_processor->GetState(); + size_t total = state.processedFilesCount + state.unprocessedFilesCount; + if (total > 0) + { + m_progressBar->setMaximum(total); + m_progressBar->setValue(state.processedFilesCount); + } + + m_stateList->clear(); + + auto addToList = [&](const std::vector<std::string>& files, const QString& prefix) { + for (auto it = files.rbegin(); it != files.rend(); ++it) + { + auto* item = new QListWidgetItem(prefix + QString::fromStdString(*it)); + item->setTextAlignment(Qt::AlignCenter); + m_stateList->addItem(item); + } + }; + + addToList(state.processingFiles, "Processing: "); + addToList(state.analyzingFiles, "Analyzing: "); +} + +void ProcessorDialog::onSaveToFile() +{ + if (!m_file) + return; + + QString fileName = QFileDialog::getSaveFileName(this, "Save WARP File", "", "WARP Files (*.warp)"); + if (!fileName.isEmpty()) + { + DataBuffer buffer = m_file->ToDataBuffer(); + QFile file(fileName); + if (file.open(QIODevice::WriteOnly)) + { + file.write(static_cast<const char*>(buffer.GetData()), buffer.GetLength()); + file.close(); + QMessageBox::information(this, "Success", "File saved successfully."); + } + else + { + QMessageBox::critical(this, "Error", "Failed to open file for writing."); + } + } +} + +void ProcessorDialog::onCommit() +{ + if (!m_file) + return; + + auto* dialog = new CommitDialog(m_file, this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); +} diff --git a/plugins/warp/ui/shared/processordialog.h b/plugins/warp/ui/shared/processordialog.h new file mode 100644 index 00000000..3f69601d --- /dev/null +++ b/plugins/warp/ui/shared/processordialog.h @@ -0,0 +1,127 @@ +#pragma once + +#include <QDialog> +#include <QStackedWidget> +#include <QComboBox> +#include <QProgressBar> +#include <QThread> +#include <QTimer> +#include <QMenu> +#include <QSpinBox> +#include <QElapsedTimer> +#include <utility> + +#include "binaryninjaapi.h" +#include "warp.h" +#include "file.h" + +// TODO: Both of these are bothersome but I don't really want to do a ID lookup. +Q_DECLARE_METATYPE(BinaryNinja::Ref<BinaryNinja::Project>) +Q_DECLARE_METATYPE(BinaryNinja::Ref<BinaryNinja::ProjectFile>) + +// Worker to run the processor +class WarpProcessorWorker : public QThread +{ + Q_OBJECT + + std::shared_ptr<Warp::Processor> m_processor; + +public: + WarpProcessorWorker(std::shared_ptr<Warp::Processor> processor, QObject* parent = nullptr) : + QThread(parent), m_processor(std::move(processor)) + {} + + void run() override + { + Warp::Ref<Warp::File> file = m_processor->Start(); + emit finishedProcessing(file); + } + +signals: + void finishedProcessing(Warp::Ref<Warp::File> file); +}; + +class ProcessorDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ProcessorDialog(QWidget* parent = nullptr); + ~ProcessorDialog() override; + + void onAddBinaryView(BinaryNinja::Ref<BinaryNinja::BinaryView> view); + void onAddProjectFiles(); + +private slots: + void onStartProcessing(); + void onProcessingFinished(Warp::Ref<Warp::File> file); + void onCancelProcessing(); + void onUpdateState(); + void onSaveToFile(); + void onCommit(); + void onAddPath(); + void onAddDirectory(); + void onRemoveItem(); + void onSearchItems(); + void onAddEntryMenu(); + void showContextMenu(const QPoint& pos); + +private: + void addAddActionsToMenu(QMenu* menu); + void addPathRecursive(const QString& path); + void addSinglePath(const QString& path); + struct ToProcessEntry + { + enum Type + { + ViewMode, + PathMode, + ProjectMode, + ProjectFileMode + } type; + BinaryNinja::Ref<BinaryNinja::BinaryView> view; + std::string path; + BinaryNinja::Ref<BinaryNinja::Project> project; + BinaryNinja::Ref<BinaryNinja::ProjectFile> projectFile; + QString displayName; + }; + + Warp::Ref<Warp::File> m_file; + std::shared_ptr<Warp::Processor> m_processor; + std::vector<ToProcessEntry> m_toProcess; + + enum Page + { + ConfigurationPage = 0, + ProcessingPage = 1, + ResultsPage = 2 + }; + + QStackedWidget* m_stack; + + // Page 1: Configuration + QLineEdit* m_entrySearch; + QPushButton* m_addButton; + QListWidget* m_entryList; + + // Global Config + QComboBox* m_includedDataCombo; + QComboBox* m_includedFunctionsCombo; + QSpinBox* m_workerCountSpinBox; + QPushButton* m_processButton; + + // Page 2: Processing + QLabel* m_processingLabel; + QProgressBar* m_progressBar; + QListWidget* m_stateList; + QPushButton* m_cancelButton; + QTimer* m_updateTimer; + + // Page 3: Results + FileWidget* m_fileWidget; + QPushButton* m_saveButton; + QLabel* m_elapsedLabel; + QPushButton* m_commitButton; + + QElapsedTimer m_processTimer; +}; diff --git a/plugins/warp/ui/shared/search.cpp b/plugins/warp/ui/shared/search.cpp index a852605e..801aa23d 100644 --- a/plugins/warp/ui/shared/search.cpp +++ b/plugins/warp/ui/shared/search.cpp @@ -1,6 +1,6 @@ #include "search.h" #include "misc.h" -#include "../../../../../ui/mainwindow.h" +#include "viewframe.h" QVariant WarpSearchModel::data(const QModelIndex& index, int role) const { @@ -12,8 +12,8 @@ QVariant WarpSearchModel::data(const QModelIndex& index, int role) const if (role == Qt::UserRole && index.column() == DisplayCol) { if (it && it->GetKind() == WARPContainerSearchItemKindFunction) - if (auto itemType = it->GetType(nullptr)) - return QVariant::fromValue(TokenData(*itemType, it->GetName())); + if (auto itemType = it->GetType()) + return QVariant::fromValue(TokenData(*itemType->GetAnalysisType(), it->GetName())); return {}; } @@ -246,21 +246,16 @@ WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget return; // TODO: Getting the current view here is really awful, but i dont care right now. - auto ctx = MainWindow::activeContext(); + auto ctx = UIContext::activeContext(); auto view = ctx->getCurrentView(); auto binaryView = view->getData(); auto viewFrame = ctx->getCurrentViewFrame(); auto viewLocation = viewFrame->getViewLocation(); auto func = viewLocation.getFunction(); - // Retrieve the current architecture from the current function or try the current view. - auto arch = binaryView->GetDefaultArchitecture(); - if (func) - arch = func->GetArchitecture(); - const int row = idx.row(); const auto item = m_model->itemAt(row); - const auto itemType = item->GetType(arch); + const auto itemType = item->GetType(); const auto itemFunc = item->GetFunction(); QMenu menu(this); @@ -272,7 +267,7 @@ WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget // We let users apply the type for types and functions (assuming the function has one) // if the user applies a function, we actually will set the user type for the current view location function. // For types, we will just throw it in the user types. - applyType->setEnabled(itemType != nullptr); + applyType->setEnabled(itemType->m_object != nullptr); applyType->setVisible(applyType->isEnabled()); applyFunction->setEnabled(func != nullptr && itemFunc); @@ -294,11 +289,12 @@ WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget { if (func && item->GetKind() == WARPContainerSearchItemKindFunction) { - func->SetUserType(itemType); + func->SetUserType(itemType->GetAnalysisType(func->GetArchitecture())); binaryView->UpdateAnalysis(); } else - binaryView->DefineUserType(item->GetName(), itemType); + binaryView->DefineUserType( + item->GetName(), itemType->GetAnalysisType(binaryView->GetDefaultArchitecture())); } else if (chosen == applyFunction) { diff --git a/plugins/warp/ui/shared/selectprojectfilesdialog.cpp b/plugins/warp/ui/shared/selectprojectfilesdialog.cpp new file mode 100644 index 00000000..e8454cff --- /dev/null +++ b/plugins/warp/ui/shared/selectprojectfilesdialog.cpp @@ -0,0 +1,141 @@ +#include "selectprojectfilesdialog.h" + +#include <QFormLayout> +#include <QLabel> +#include <QLineEdit> +#include <QPushButton> +#include <QVBoxLayout> + +using namespace BinaryNinja; + +SelectProjectFilesDialog::SelectProjectFilesDialog(QWidget* parent) : QDialog(parent) +{ + setWindowTitle("Select Project Files"); + setMinimumSize(700, 400); + auto* layout = new QVBoxLayout(this); + + auto* topLayout = new QFormLayout(); + m_projectCombo = new QComboBox(this); + auto projects = Project::GetOpenProjects(); + for (auto& project : projects) + { + m_projectCombo->addItem(QString::fromStdString(project->GetName()), QVariant::fromValue(project)); + } + topLayout->addRow("Project:", m_projectCombo); + layout->addLayout(topLayout); + + m_searchBar = new QLineEdit(this); + m_searchBar->setPlaceholderText("Search files..."); + connect(m_searchBar, &QLineEdit::textChanged, this, &SelectProjectFilesDialog::filterLists); + layout->addWidget(m_searchBar); + + auto* listsLayout = new QHBoxLayout(); + + auto* notAddingBox = new QVBoxLayout(); + notAddingBox->addWidget(new QLabel("Available:")); + m_notAddingList = new QListWidget(this); + m_notAddingList->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_notAddingList->setTextElideMode(Qt::ElideLeft); + m_notAddingList->setStyleSheet("QListWidget::item { padding: 2px; }"); + notAddingBox->addWidget(m_notAddingList); + listsLayout->addLayout(notAddingBox); + + auto* middleButtons = new QVBoxLayout(); + middleButtons->addStretch(); + auto* addButton = new QPushButton(">>", this); + connect(addButton, &QPushButton::clicked, [this]() { moveSelected(m_notAddingList, m_addingList); }); + middleButtons->addWidget(addButton); + auto* removeButton = new QPushButton("<<", this); + connect(removeButton, &QPushButton::clicked, [this]() { moveSelected(m_addingList, m_notAddingList); }); + middleButtons->addWidget(removeButton); + middleButtons->addStretch(); + listsLayout->addLayout(middleButtons); + + auto* addingBox = new QVBoxLayout(); + addingBox->addWidget(new QLabel("Selected:")); + m_addingList = new QListWidget(this); + m_addingList->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_addingList->setTextElideMode(Qt::ElideLeft); + m_addingList->setStyleSheet("QListWidget::item { padding: 2px; }"); + addingBox->addWidget(m_addingList); + listsLayout->addLayout(addingBox); + + layout->addLayout(listsLayout); + + auto* buttons = new QHBoxLayout(this); + auto* ok = new QPushButton("Add", this); + connect(ok, &QPushButton::clicked, this, &QDialog::accept); + auto* cancel = new QPushButton("Cancel", this); + connect(cancel, &QPushButton::clicked, this, &QDialog::reject); + buttons->addStretch(); + buttons->addWidget(ok); + buttons->addWidget(cancel); + layout->addLayout(buttons); + + connect(m_projectCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, + &SelectProjectFilesDialog::updateFileList); + + connect(m_notAddingList, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) { + m_addingList->addItem(m_notAddingList->takeItem(m_notAddingList->row(item))); + filterLists(); + }); + connect(m_addingList, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) { + m_notAddingList->addItem(m_addingList->takeItem(m_addingList->row(item))); + filterLists(); + }); + + updateFileList(); +} + +void SelectProjectFilesDialog::updateFileList() +{ + m_notAddingList->clear(); + m_addingList->clear(); + m_currentProject = m_projectCombo->currentData().value<Ref<Project>>(); + if (m_currentProject) + { + for (auto& file : m_currentProject->GetFiles()) + { + auto* item = new QListWidgetItem(QString::fromStdString(file->GetPathInProject())); + item->setData(Qt::UserRole, QVariant::fromValue(file)); + m_notAddingList->addItem(item); + } + } + filterLists(); +} + +void SelectProjectFilesDialog::filterLists() +{ + QString filter = m_searchBar->text().toLower(); + for (int i = 0; i < m_notAddingList->count(); ++i) + { + auto* item = m_notAddingList->item(i); + item->setHidden(!item->text().toLower().contains(filter)); + } + for (int i = 0; i < m_addingList->count(); ++i) + { + auto* item = m_addingList->item(i); + item->setHidden(!item->text().toLower().contains(filter)); + } +} + +void SelectProjectFilesDialog::moveSelected(QListWidget* from, QListWidget* to) +{ + QList<QListWidgetItem*> items = from->selectedItems(); + for (auto* item : items) + to->addItem(from->takeItem(from->row(item))); + filterLists(); +} + +std::vector<Ref<ProjectFile>> SelectProjectFilesDialog::getSelectedFiles() const +{ + std::vector<Ref<ProjectFile>> files; + for (int i = 0; i < m_addingList->count(); ++i) + files.push_back(m_addingList->item(i)->data(Qt::UserRole).value<Ref<ProjectFile>>()); + return files; +} + +Ref<Project> SelectProjectFilesDialog::getSelectedProject() const +{ + return m_currentProject; +}
\ No newline at end of file diff --git a/plugins/warp/ui/shared/selectprojectfilesdialog.h b/plugins/warp/ui/shared/selectprojectfilesdialog.h new file mode 100644 index 00000000..ce5e8093 --- /dev/null +++ b/plugins/warp/ui/shared/selectprojectfilesdialog.h @@ -0,0 +1,26 @@ +#pragma once + +#include <QComboBox> +#include <QDialog> +#include <QListWidget> + +#include "binaryninjaapi.h" + +class SelectProjectFilesDialog : public QDialog +{ + Q_OBJECT + BinaryNinja::Ref<BinaryNinja::Project> m_currentProject; + QComboBox* m_projectCombo; + QLineEdit* m_searchBar; + QListWidget* m_notAddingList; + QListWidget* m_addingList; + +public: + SelectProjectFilesDialog(QWidget* parent = nullptr); + + void updateFileList(); + void filterLists(); + void moveSelected(QListWidget* from, QListWidget* to); + [[nodiscard]] std::vector<BinaryNinja::Ref<BinaryNinja::ProjectFile>> getSelectedFiles() const; + [[nodiscard]] BinaryNinja::Ref<BinaryNinja::Project> getSelectedProject() const; +}; diff --git a/plugins/warp/ui/shared/source.cpp b/plugins/warp/ui/shared/source.cpp new file mode 100644 index 00000000..c7736151 --- /dev/null +++ b/plugins/warp/ui/shared/source.cpp @@ -0,0 +1,204 @@ +#include "source.h" + +#include <QClipboard> +#include <QFileInfo> +#include <QHeaderView> +#include <QPainter> + +QVariant WarpSourcesModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return {}; + if (index.row() < 0 || index.row() >= rowCount()) + return {}; + + const auto& r = m_rows[static_cast<size_t>(index.row())]; + + // Build a small two-dot status icon (left: writable, right: uncommitted) + auto statusIcon = [](bool writable, bool uncommitted) -> QIcon { + static QIcon cache[2][2]; // [writable][uncommitted] + QIcon& cached = cache[writable ? 1 : 0][uncommitted ? 1 : 0]; + if (!cached.isNull()) + return cached; + + const int w = 16, h = 12, radius = 4; + QPixmap pm(w, h); + pm.fill(Qt::transparent); + QPainter p(&pm); + p.setRenderHint(QPainter::Antialiasing, true); + + // Colors + QColor writableOn(76, 175, 80); // green + QColor writableOff(158, 158, 158); // grey + QColor uncommittedOn(255, 193, 7); // amber + QColor uncommittedOff(158, 158, 158); // grey + + // Left dot: writable + p.setBrush(writable ? writableOn : writableOff); + p.setPen(Qt::NoPen); + p.drawEllipse(QPoint(4, h / 2), radius, radius); + + // Right dot: uncommitted + p.setBrush(uncommitted ? uncommittedOn : uncommittedOff); + p.drawEllipse(QPoint(w - 6, h / 2), radius, radius); + + p.end(); + cached = QIcon(pm); + return cached; + }; + + if (role == Qt::DecorationRole && index.column() == PathCol) + { + return statusIcon(r.writable, r.uncommitted); + } + + if (role == Qt::ToolTipRole && index.column() == PathCol) + { + QStringList parts; + parts << (r.writable ? "Writable" : "Read-only"); + parts << (r.uncommitted ? "Uncommitted changes" : "No uncommitted changes"); + return parts.join(" • "); + } + + if (role == Qt::DisplayRole) + { + switch (index.column()) + { + case GuidCol: + return r.guid; + case PathCol: + return r.path; + case WritableCol: + return r.writable ? "Yes" : "No"; + case UncommittedCol: + return r.uncommitted ? "Yes" : "No"; + default: + return {}; + } + } + + if (role == Qt::CheckStateRole) + { + // Optional: expose as checkboxes if someone ever shows these columns + switch (index.column()) + { + case WritableCol: + return r.writable ? Qt::Checked : Qt::Unchecked; + case UncommittedCol: + return r.uncommitted ? Qt::Checked : Qt::Unchecked; + default: + break; + } + } + + return {}; +} + +WarpSourcesView::WarpSourcesView(QWidget* parent) : QTableView(parent) +{ + m_model = new WarpSourcesModel(this); + QTableView::setModel(m_model); + + horizontalHeader()->setStretchLastSection(true); + setSelectionBehavior(SelectRows); + setSelectionMode(SingleSelection); + + // Make the table look like a simple list that shows only the source path + setShowGrid(false); + verticalHeader()->setVisible(false); + horizontalHeader()->setVisible(false); + setAlternatingRowColors(false); + setEditTriggers(NoEditTriggers); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setWordWrap(false); + setIconSize(QSize(16, 12)); + // Ensure long paths truncate from the left: "...tail/of/the/path" + setTextElideMode(Qt::ElideLeft); + + // Hide GUID column, keep only the Path column visible + setColumnHidden(WarpSourcesModel::GuidCol, true); + // Also hide boolean columns; their state is shown as an icon next to the path + setColumnHidden(WarpSourcesModel::WritableCol, true); + setColumnHidden(WarpSourcesModel::UncommittedCol, true); + // Ensure the remaining (Path) column fills the width + horizontalHeader()->setSectionResizeMode(WarpSourcesModel::PathCol, QHeaderView::Stretch); + + // Per-item context menu + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) { + if (!m_model || !m_container) + return; + + QMenu menu(this); + const QModelIndex index = indexAt(pos); + + if (!index.isValid()) + { + QAction* actAdd = menu.addAction(tr("Add Source")); + QAction* chosen = menu.exec(viewport()->mapToGlobal(pos)); + if (!chosen) + return; + if (chosen == actAdd) + addSource(); + } + else + { + setCurrentIndex(index.sibling(index.row(), WarpSourcesModel::PathCol)); + + const int row = index.row(); + const QModelIndex pathIdx = m_model->index(row, WarpSourcesModel::PathCol); + const QModelIndex guidIdx = m_model->index(row, WarpSourcesModel::GuidCol); + const QString path = m_model->data(pathIdx, Qt::DisplayRole).toString(); + const QFileInfo fi(path); + + const QString guid = m_model->data(guidIdx, Qt::DisplayRole).toString(); + + QAction* actReveal = menu.addAction(tr("Reveal in File Browser")); + actReveal->setEnabled(fi.exists()); + QAction* actCopyPath = menu.addAction(tr("Copy Path")); + QAction* actCopyGuid = menu.addAction(tr("Copy GUID")); + + QAction* chosen = menu.exec(viewport()->mapToGlobal(pos)); + if (!chosen) + return; + if (chosen == actCopyPath) + QGuiApplication::clipboard()->setText(path); + else if (chosen == actCopyGuid) + QGuiApplication::clipboard()->setText(guid); + else if (chosen == actReveal) + QDesktopServices::openUrl(QUrl::fromLocalFile(fi.absoluteFilePath())); + } + }); +} + +void WarpSourcesView::setContainer(Warp::Ref<Warp::Container> container) +{ + m_container = std::move(container); + m_model->setContainer(m_container); +} + +bool WarpSourcesView::addSource() +{ + if (!m_model || !m_container) + return false; + + std::string sourceName; + if (!BinaryNinja::GetTextLineInput(sourceName, "Source name:", "Add Source")) + return false; + if (const auto sourceId = m_container->AddSource(sourceName); !sourceId.has_value()) + { + BinaryNinja::LogAlertF("Failed to add source: {}", sourceName); + return false; + } + m_model->reload(); + return true; +} + +std::optional<Warp::Source> WarpSourcesView::sourceFromRow(int row) const +{ + if (!m_model || row < 0 || row >= m_model->rowCount()) + return std::nullopt; + const QModelIndex guidIdx = m_model->index(row, WarpSourcesModel::GuidCol); + std::string guidStr = m_model->data(guidIdx, Qt::DisplayRole).toString().toStdString(); + return Warp::WarpUUID::FromString(guidStr); +}
\ No newline at end of file diff --git a/plugins/warp/ui/shared/source.h b/plugins/warp/ui/shared/source.h new file mode 100644 index 00000000..7dad9333 --- /dev/null +++ b/plugins/warp/ui/shared/source.h @@ -0,0 +1,116 @@ +#pragma once + +#include <QWidget> +#include <QDesktopServices> +#include <QInputDialog> +#include <QListWidget> +#include <QTableView> + +#include "theme.h" +#include "warp.h" + +class WarpSourcesModel final : public QAbstractTableModel +{ + Q_OBJECT + +public: + enum Columns : int + { + GuidCol = 0, + PathCol, + WritableCol, + UncommittedCol, + ColumnCount + }; + + explicit WarpSourcesModel(QObject* parent = nullptr) : QAbstractTableModel(parent) {} + + void setContainer(Warp::Ref<Warp::Container> container) + { + m_container = std::move(container); + reload(); + } + + void reload() + { + // Fetch synchronously (can be adapted to async if needed) + beginResetModel(); + m_rows.clear(); + for (const auto& src : m_container->GetSources()) + { + QString guid = QString::fromStdString(src.ToString()); + QString path = QString::fromStdString(m_container->SourcePath(src).value_or(std::string {})); + bool writable = m_container->IsSourceWritable(src); + bool uncommitted = m_container->IsSourceUncommitted(src); + m_rows.push_back({guid, path, writable, uncommitted}); + } + endResetModel(); + } + + int rowCount(const QModelIndex& parent = QModelIndex()) const override + { + if (parent.isValid()) + return 0; + return static_cast<int>(m_rows.size()); + } + + int columnCount(const QModelIndex& parent = QModelIndex()) const override + { + Q_UNUSED(parent); + return ColumnCount; + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override + { + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + switch (section) + { + case GuidCol: + return "Source GUID"; + case PathCol: + return "Path"; + case WritableCol: + return "Writable"; + case UncommittedCol: + return "Uncommitted"; + default: + return {}; + } + } + return {}; + } + +private: + struct Row + { + QString guid; + QString path; + bool writable; + bool uncommitted; + }; + + std::vector<Row> m_rows; + Warp::Ref<Warp::Container> m_container; +}; + + +class WarpSourcesView : public QTableView +{ + Q_OBJECT + +public: + explicit WarpSourcesView(QWidget* parent = nullptr); + + void setContainer(Warp::Ref<Warp::Container> container); + bool addSource(); + + [[nodiscard]] WarpSourcesModel* sourceModel() const { return m_model; } + [[nodiscard]] std::optional<Warp::Source> sourceFromRow(int row) const; + +private: + WarpSourcesModel* m_model = nullptr; + Warp::Ref<Warp::Container> m_container; +};
\ No newline at end of file |
