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/shared | |
| 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/shared')
21 files changed, 1698 insertions, 72 deletions
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 |
