summaryrefslogtreecommitdiff
path: root/plugins/warp/ui/shared
diff options
context:
space:
mode:
authorMason Reed <mason@vector35.com>2025-08-26 23:59:38 -0400
committerMason Reed <mason@vector35.com>2025-10-01 21:38:39 -0400
commitede39aee7e00c40a43b67ca18dd8ab80ee863d85 (patch)
tree67c5eda347ece2282e3c888f38066b496e06dec8 /plugins/warp/ui/shared
parenta1c46813e7f279aa4cfdb9dbb91c45b559ebeacd (diff)
[WARP] Enhanced network support
Diffstat (limited to 'plugins/warp/ui/shared')
-rw-r--r--plugins/warp/ui/shared/fetchdialog.cpp227
-rw-r--r--plugins/warp/ui/shared/fetchdialog.h56
-rw-r--r--plugins/warp/ui/shared/fetcher.cpp109
-rw-r--r--plugins/warp/ui/shared/fetcher.h85
-rw-r--r--plugins/warp/ui/shared/function.cpp26
-rw-r--r--plugins/warp/ui/shared/misc.cpp63
-rw-r--r--plugins/warp/ui/shared/misc.h32
-rw-r--r--plugins/warp/ui/shared/search.cpp301
-rw-r--r--plugins/warp/ui/shared/search.h234
9 files changed, 1107 insertions, 26 deletions
diff --git a/plugins/warp/ui/shared/fetchdialog.cpp b/plugins/warp/ui/shared/fetchdialog.cpp
new file mode 100644
index 00000000..0230ad51
--- /dev/null
+++ b/plugins/warp/ui/shared/fetchdialog.cpp
@@ -0,0 +1,227 @@
+#include "fetchdialog.h"
+
+#include <QDialogButtonBox>
+#include <QFormLayout>
+#include <QInputDialog>
+#include <QLabel>
+
+#include "action.h"
+#include "fetcher.h"
+
+using namespace BinaryNinja;
+
+static void AddListItem(QListWidget *list, const QString &value)
+{
+ if (value.trimmed().isEmpty())
+ return;
+ // Avoid duplicates
+ for (int i = 0; i < list->count(); ++i)
+ if (list->item(i)->text().compare(value, Qt::CaseInsensitive) == 0)
+ return;
+ list->addItem(value.trimmed());
+}
+
+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");
+
+ auto form = new QFormLayout();
+ m_containerCombo = new QComboBox(this);
+ populateContainers();
+ m_containerCombo->addItem("All Containers"); // index 0 for "all"
+ for (const auto &c: m_containers)
+ m_containerCombo->addItem(QString::fromStdString(c->GetName()));
+
+ // TODO: Need to add tooltip to explain that a source must have atleast one of these tags to be considered.
+
+ // Tags editor
+ m_tagsList = new QListWidget(this);
+ m_addTagBtn = new QPushButton("Add", this);
+ m_removeTagBtn = new QPushButton("Remove", this);
+ auto tagBtnRow = new QHBoxLayout();
+ tagBtnRow->addWidget(m_addTagBtn);
+ tagBtnRow->addWidget(m_removeTagBtn);
+ auto tagCol = new QVBoxLayout();
+ tagCol->addWidget(m_tagsList);
+ tagCol->addLayout(tagBtnRow);
+ auto tagWrapper = new QWidget(this);
+ tagWrapper->setLayout(tagCol);
+
+ // Make tags list compact with a fixed maximum height and no vertical expansion
+ m_tagsList->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+ m_tagsList->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+ m_tagsList->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+ m_tagsList->setMaximumHeight(120);
+ m_tagsList->setToolTip("A source must have atleast ONE of these tags to be considered");
+
+ // Defaults from processor tags
+ for (const auto &t: m_fetchProcessor->GetTags())
+ AddListItem(m_tagsList, QString::fromStdString(t));
+
+ // Batch size and matcher checkbox
+ m_batchSize = new QSpinBox(this);
+ m_batchSize->setRange(10, 1000);
+ m_batchSize->setValue(100);
+ m_batchSize->setToolTip("Number of functions to fetch in each batch");
+
+ m_rerunMatcher = new QCheckBox("Re-run matcher after fetch", this);
+ m_rerunMatcher->setChecked(true);
+
+ m_clearProcessed = new QCheckBox("Refetch all functions", this);
+ m_clearProcessed->setToolTip("Clears the processed cache before fetching again, this will refetch all functions in the view");
+ m_clearProcessed->setChecked(false);
+
+ form->addRow(new QLabel("Container: "), m_containerCombo);
+ // TODO: Need to plumb this through to the fetcher, and also likely have a blacklisted or whitelist mode for this dialog.
+ // TODO: Alos wan to prefill the list of sources from the view/global settings.
+ // form->addRow(new QLabel("Allowed Sources: "), srcWrapper);
+ form->addRow(new QLabel("Allowed Tags: "), tagWrapper);
+ form->addRow(new QLabel("Batch Size: "), m_batchSize);
+ form->addRow(m_rerunMatcher);
+ form->addRow(m_clearProcessed);
+
+ auto buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ connect(buttons, &QDialogButtonBox::accepted, this, &WarpFetchDialog::onAccept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+ auto root = new QVBoxLayout(this);
+ root->addLayout(form);
+ root->addWidget(buttons);
+ setLayout(root);
+
+ // Wire buttons
+ connect(m_addTagBtn, &QPushButton::clicked, this, &WarpFetchDialog::onAddTag);
+ connect(m_removeTagBtn, &QPushButton::clicked, this, &WarpFetchDialog::onRemoveTag);
+}
+
+void WarpFetchDialog::populateContainers()
+{
+ m_containers = Warp::Container::All();
+}
+
+void WarpFetchDialog::onAddTag()
+{
+ bool ok = false;
+ const auto text = QInputDialog::getText(this, "Add Tag", "Tag:", QLineEdit::Normal, {}, &ok);
+ if (ok)
+ AddListItem(m_tagsList, text);
+}
+
+void WarpFetchDialog::onRemoveTag()
+{
+ for (auto *item: m_tagsList->selectedItems())
+ delete item;
+}
+
+std::vector<Warp::SourceTag> WarpFetchDialog::collectTags() const
+{
+ std::vector<Warp::SourceTag> out;
+ out.reserve(m_tagsList->count());
+ for (int i = 0; i < m_tagsList->count(); ++i)
+ out.emplace_back(m_tagsList->item(i)->text().trimmed().toStdString());
+ return out;
+}
+
+void WarpFetchDialog::onAccept()
+{
+ const int idx = m_containerCombo->currentIndex();
+ std::optional<size_t> containerIndex;
+ if (idx > 0) // 0 == All Containers
+ containerIndex = static_cast<size_t>(idx - 1);
+
+ auto tags = collectTags();
+ const auto batch = static_cast<size_t>(m_batchSize->value());
+ const bool rerun = m_rerunMatcher->isChecked();
+
+ // Persist tags to the shared processor for consistency across navigation
+ m_fetchProcessor->SetTags(tags);
+
+ if (m_clearProcessed->isChecked())
+ m_fetchProcessor->ClearProcessed();
+
+ // Execute the network fetch in batches
+ runBatchedFetch(containerIndex, tags, batch, rerun);
+
+ accept();
+}
+
+void WarpFetchDialog::runBatchedFetch(const std::optional<size_t> &containerIndex,
+ const std::vector<Warp::SourceTag> &tags,
+ size_t batchSize,
+ bool rerunMatcher)
+{
+ if (!m_bv)
+ return;
+ // Collect functions in the view and enqueue them to the shared fetcher
+ std::vector<Ref<Function>> funcs = m_bv->GetAnalysisFunctionList();
+ if (funcs.empty())
+ return;
+ const size_t totalFuncs = funcs.size();
+ const size_t totalBatches = (totalFuncs + batchSize - 1) / batchSize;
+
+ // Create a background task to show progress in the UI
+ Ref<BackgroundTask> task = new BackgroundTask("Fetching WARP functions (0 / " + std::to_string(totalBatches) + ")", false);
+
+ auto fetcher = m_fetchProcessor;
+ auto bv = m_bv;
+
+ // TODO: Too many captures in this thing lol.
+ WorkerInteractiveEnqueue([fetcher, bv, funcs = std::move(funcs), batchSize, rerunMatcher, task]() mutable {
+ size_t processed = 0;
+ size_t batchIndex = 0;
+
+ while (processed < funcs.size())
+ {
+ 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();
+
+ ++batchIndex;
+ processed += thisBatchCount;
+
+ task->SetProgressText("Fetching WARP functions (" + std::to_string(batchIndex) + " / " + std::to_string((funcs.size() + batchSize - 1) / batchSize) + ")");
+ }
+
+ task->Finish();
+ // TODO: Print how long it took?
+ Logger("WARP Fetcher").LogInfo("Finished fetching WARP functions...");
+
+ 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";
+
+ // TODO: Because we register this in every widget this will happen, this is bad behavior!
+ 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");
+}
diff --git a/plugins/warp/ui/shared/fetchdialog.h b/plugins/warp/ui/shared/fetchdialog.h
new file mode 100644
index 00000000..72591a4c
--- /dev/null
+++ b/plugins/warp/ui/shared/fetchdialog.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <QDialog>
+#include <QComboBox>
+#include <QListWidget>
+#include <QSpinBox>
+#include <QCheckBox>
+
+#include "uicontext.h"
+#include "viewframe.h"
+#include "warp.h"
+#include "fetcher.h"
+
+class WarpFetchDialog : public QDialog
+{
+ Q_OBJECT
+
+ QComboBox *m_containerCombo;
+
+ QListWidget *m_tagsList;
+ QPushButton *m_addTagBtn;
+ QPushButton *m_removeTagBtn;
+
+ QSpinBox *m_batchSize;
+ QCheckBox *m_rerunMatcher;
+ QCheckBox *m_clearProcessed;
+
+ std::vector<Warp::Ref<Warp::Container> > m_containers;
+
+ std::shared_ptr<WarpFetcher> m_fetchProcessor;
+ BinaryViewRef m_bv;
+
+public:
+ explicit WarpFetchDialog(BinaryViewRef bv,
+ std::shared_ptr<WarpFetcher> fetchProcessor,
+ QWidget *parent = nullptr);
+
+private slots:
+ void onAddTag();
+
+ void onRemoveTag();
+
+ void onAccept();
+
+private:
+ void populateContainers();
+
+ std::vector<Warp::SourceTag> collectTags() const;
+
+ void runBatchedFetch(const std::optional<size_t> &containerIndex,
+ const std::vector<Warp::SourceTag> &tags,
+ size_t batchSize,
+ bool rerunMatcher);
+};
+
+void RegisterWarpFetchFunctionsCommand();
diff --git a/plugins/warp/ui/shared/fetcher.cpp b/plugins/warp/ui/shared/fetcher.cpp
new file mode 100644
index 00000000..767932e3
--- /dev/null
+++ b/plugins/warp/ui/shared/fetcher.cpp
@@ -0,0 +1,109 @@
+#include "fetcher.h"
+
+#include <QSettings>
+
+WarpFetcher::WarpFetcher()
+{
+ m_logger = new BinaryNinja::Logger("WARP Fetcher");
+ QSettings qtSettings;
+ const QString key = "warp/allowedTags";
+
+ QStringList tags = qtSettings.value(key).toStringList();
+ if (tags.isEmpty()) {
+ tags = QStringList{ "official", "trusted" };
+ qtSettings.setValue(key, tags);
+ qtSettings.sync();
+ }
+
+ std::vector<Warp::SourceTag> initialTags;
+ initialTags.reserve(tags.size());
+ for (const auto& t : tags)
+ initialTags.emplace_back(t.trimmed().toStdString());
+
+ SetTags(initialTags);
+}
+
+void WarpFetcher::AddPendingFunction(const FunctionRef &func)
+{
+ std::lock_guard<std::mutex> lock(m_requestMutex);
+ const auto guid = Warp::GetAnalysisFunctionGUID(*func);
+ if (!guid.has_value() || m_processedGuids.contains(*guid))
+ return;
+ m_pendingRequests.push_back(func);
+}
+
+std::vector<FunctionRef> WarpFetcher::FlushPendingFunctions()
+{
+ std::lock_guard<std::mutex> lock(m_requestMutex);
+ std::vector<FunctionRef> requests = std::move(m_pendingRequests);
+ m_pendingRequests.clear();
+ return requests;
+}
+
+void WarpFetcher::ExecuteCompletionCallback()
+{
+ BinaryNinja::ExecuteOnMainThread([this]() {
+ // TODO: Holding the mutex here is dangerous!
+ std::lock_guard<std::mutex> lock(m_requestMutex);
+ m_completionCallbacks.erase(
+ std::ranges::remove_if(m_completionCallbacks,
+ [](const auto &cb) { return cb() != RemoveCallback; }).begin(),
+ m_completionCallbacks.end());
+ });
+}
+
+std::shared_ptr<WarpFetcher> WarpFetcher::Global()
+{
+ static auto global = std::make_shared<WarpFetcher>();
+ return global;
+}
+
+void WarpFetcher::FetchPendingFunctions()
+{
+ m_requestInProgress = true;
+ const auto requests = FlushPendingFunctions();
+ if (requests.empty())
+ {
+ m_logger->LogDebug("No pending requests to fetch... skipping");
+ m_requestInProgress = false;
+ return;
+ }
+
+ const auto start_time = std::chrono::high_resolution_clock::now();
+
+ // Because we must fetch for a single target we map the function guids to the associated platform to perform fetches for each.
+ std::map<PlatformRef, std::vector<Warp::FunctionGUID>> platformMappedGuids;
+ for (const auto &func: requests)
+ {
+ const auto guid = Warp::GetAnalysisFunctionGUID(*func);
+ if (!guid.has_value())
+ continue;
+ auto platform = func->GetPlatform();
+ platformMappedGuids[platform].push_back(guid.value());
+ }
+
+ const auto tags = GetTags();
+ for (const auto &[platform, guids] : platformMappedGuids)
+ {
+ m_logger->LogDebugF("Fetching {} functions for platform {}", guids.size(), platform->GetName());
+ auto target = Warp::Target::FromPlatform(*platform);
+ for (const auto &container: Warp::Container::All())
+ container->FetchFunctions(*target, guids, tags);
+
+ std::lock_guard<std::mutex> lock(m_requestMutex);
+ for (const auto &guid: guids)
+ m_processedGuids.insert(guid);
+ }
+
+ m_requestInProgress = false;
+ ExecuteCompletionCallback();
+ const auto end_time = std::chrono::high_resolution_clock::now();
+ const std::chrono::duration<double> elapsed_time = end_time - start_time;
+ m_logger->LogDebug("Fetch batch took %f seconds", elapsed_time.count());
+}
+
+void WarpFetcher::ClearProcessed()
+{
+ m_logger->LogInfoF("Clearing {} processed functions from cache...", m_processedGuids.size());
+ m_processedGuids.clear();
+}
diff --git a/plugins/warp/ui/shared/fetcher.h b/plugins/warp/ui/shared/fetcher.h
new file mode 100644
index 00000000..a7195ac4
--- /dev/null
+++ b/plugins/warp/ui/shared/fetcher.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include <atomic>
+#include <mutex>
+#include <unordered_set>
+#include <vector>
+#include <functional>
+#include <QSettings>
+
+#include "warp.h"
+#include "binaryninjaapi.h"
+#include "uitypes.h"
+
+enum WarpFetchCompletionStatus
+{
+ KeepCallback,
+ RemoveCallback,
+};
+
+// Responsible for fetching data from the containers, to later be queried from the container interface.
+class WarpFetcher
+{
+ LoggerRef m_logger;
+
+ std::mutex m_requestMutex;
+ std::vector<FunctionRef> m_pendingRequests;
+ // TODO: Easy way to clear this if user wants to refetch.
+ std::unordered_set<Warp::FunctionGUID> m_processedGuids;
+
+ // List of callbacks to call when done fetching data, assume that others are using this as well.
+ std::vector<std::function<WarpFetchCompletionStatus()> > m_completionCallbacks;
+
+public:
+ explicit WarpFetcher();
+
+ // The global fetcher instance, this is used for the fetch dialog and the sidebar.
+ static std::shared_ptr<WarpFetcher> Global();
+
+ std::atomic<bool> m_requestInProgress = false;
+
+ // Set the allowed source tags, sources with none of these tags will not be fetched from.
+ void SetTags(const std::vector<Warp::SourceTag> &tags)
+ {
+ std::lock_guard<std::mutex> lock(m_requestMutex);
+ // TODO: This is kinda a hack, the fetcher instance should not sync through qt settings!
+ QStringList qtTags = {};
+ for (const auto& t : tags)
+ qtTags.append(QString::fromStdString(t));
+ QSettings().setValue("warp/allowedTags", qtTags);
+ }
+
+ std::vector<Warp::SourceTag> GetTags() const
+ {
+ std::lock_guard<std::mutex> lock(const_cast<std::mutex &>(m_requestMutex));
+ // TODO: This is kinda a hack, the fetcher instance should not sync through qt settings!
+ QSettings qtSettings;
+ QStringList tags = qtSettings.value("warp/allowedTags").toStringList();
+ if (tags.isEmpty()) {
+ // The default tags to allow.
+ tags = QStringList{ "official", "trusted" };
+ qtSettings.setValue("warp/allowedTags", tags);
+ qtSettings.sync();
+ }
+ std::vector<Warp::SourceTag> initialTags = {};
+ for (const auto& t : tags)
+ initialTags.emplace_back(t.trimmed().toStdString());
+ return initialTags;
+ }
+
+ void AddCompletionCallback(std::function<WarpFetchCompletionStatus()> cb)
+ {
+ std::lock_guard<std::mutex> lock(m_requestMutex);
+ m_completionCallbacks.push_back(std::move(cb));
+ }
+
+ void AddPendingFunction(const FunctionRef &func);
+
+ void FetchPendingFunctions();
+
+ void ClearProcessed();
+private:
+ std::vector<FunctionRef> FlushPendingFunctions();
+
+ void ExecuteCompletionCallback();
+};
diff --git a/plugins/warp/ui/shared/function.cpp b/plugins/warp/ui/shared/function.cpp
index 88d27cd1..e1609e0b 100644
--- a/plugins/warp/ui/shared/function.cpp
+++ b/plugins/warp/ui/shared/function.cpp
@@ -14,35 +14,15 @@ WarpFunctionItem::WarpFunctionItem(Warp::Ref<Warp::Function> function,
{
m_function = function;
- // TODO: This needs to be better. Symbol can be nullptr.
BinaryNinja::Ref<BinaryNinja::Symbol> symbol = m_function->GetSymbol(*analysisFunction);
std::string symbolName = symbol->GetShortName();
setText(QString::fromStdString(symbolName));
- BinaryNinja::InstructionTextToken nameToken = {255, TextToken, symbolName};
// 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 = {};
-
- // TODO: Make this not look like garbage
- BinaryNinja::Ref<BinaryNinja::Type> type = m_function->GetType(*analysisFunction);
- if (type)
- {
- BinaryNinja::Ref<BinaryNinja::Platform> platform = analysisFunction->GetPlatform();
- std::vector<BinaryNinja::InstructionTextToken> beforeTokens = type->GetTokensBeforeName(platform);
- std::vector<BinaryNinja::InstructionTextToken> afterTokens = type->GetTokensAfterName(platform);
-
- for (const auto &token: beforeTokens)
- tokenData.tokens.emplace_back(token);
- tokenData.tokens.emplace_back(255, TextToken, " ");
- tokenData.tokens.emplace_back(nameToken);
- for (const auto &token: afterTokens)
- tokenData.tokens.emplace_back(token);
- } else
- {
- tokenData.tokens.emplace_back(nameToken);
- }
-
+ TokenData tokenData = TokenData(symbolName);
+ if (BinaryNinja::Ref<BinaryNinja::Type> type = m_function->GetType(*analysisFunction))
+ tokenData = TokenData(*type, symbolName);
setData(QVariant::fromValue(tokenData), Qt::UserRole);
}
diff --git a/plugins/warp/ui/shared/misc.cpp b/plugins/warp/ui/shared/misc.cpp
index 71147079..bbe3ab24 100644
--- a/plugins/warp/ui/shared/misc.cpp
+++ b/plugins/warp/ui/shared/misc.cpp
@@ -8,6 +8,21 @@
#include "render.h"
#include "theme.h"
+TokenData::TokenData(const std::string &name)
+{
+ tokens.emplace_back(255, TextToken, name);
+}
+
+TokenData::TokenData(const BinaryNinja::Type &type, const std::string& name)
+{
+ for (const auto &token: type.GetTokensBeforeName())
+ tokens.emplace_back(token);
+ tokens.emplace_back(255, TextToken, " ");
+ tokens.emplace_back(255, TextToken, name);
+ for (const auto &token: type.GetTokensAfterName())
+ tokens.emplace_back(token);
+}
+
void TokenDataDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
painter->save();
@@ -23,7 +38,7 @@ void TokenDataDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
painter->translate(option.rect.topLeft());
- auto renderContext = RenderContext((QWidget *) option.widget);
+ auto renderContext = RenderContext(const_cast<QWidget *>(option.widget));
renderContext.init(*painter);
HighlightTokenState highlightState;
renderContext.drawDisassemblyLine(*painter, 5, 5, {tokenData.tokens.begin(), tokenData.tokens.end()},
@@ -35,7 +50,7 @@ void TokenDataDelegate::paint(QPainter *painter, const QStyleOptionViewItem &opt
QSize TokenDataDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
auto tokenData = index.data(Qt::UserRole).value<TokenData>();
- auto renderContext = RenderContext((QWidget *) option.widget);
+ auto renderContext = RenderContext(const_cast<QWidget *>(option.widget));
QFontMetrics fontMetrics = QFontMetrics(renderContext.getFont());
QString line = "";
for (const auto &token: tokenData.tokens)
@@ -79,3 +94,47 @@ bool GenericTextFilterModel::lessThan(const QModelIndex &sourceLeft, const QMode
auto rightData = sourceRight.data().toString();
return QString::localeAwareCompare(leftData, rightData) < 0;
}
+
+ParsedQuery::ParsedQuery(const QString &rawQuery)
+{
+ query = rawQuery;
+ qualifiers = {};
+
+ // Regex capturing: key, and value (bare, quoted and unquoted)
+ static QRegularExpression re(R"((^|\s)([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(?:\"([^\"]*)\"|'([^']*)'|(\S+)))",
+ QRegularExpression::CaseInsensitiveOption);
+
+ // Collect matches to remove later (from end to start to keep indices stable)
+ struct Span
+ {
+ qsizetype start;
+ qsizetype length;
+ QString key;
+ QString value;
+ };
+ QVector<Span> spans;
+
+ auto it = re.globalMatch(rawQuery);
+ while (it.hasNext())
+ {
+ const auto m = it.next();
+ const QString key = m.captured(2).toLower();
+ // Value can be in group 3 (double quotes), 4 (single quotes), or 5 (bare)
+ QString val = m.captured(3);
+ if (val.isNull() || val.isEmpty()) val = m.captured(4);
+ if (val.isNull() || val.isEmpty()) val = m.captured(5);
+ spans.push_back(Span{m.capturedStart(0), m.capturedLength(0), key, val});
+ }
+
+ // Keep only the last value per key (last occurrence wins, do not accumulate)
+ for (const auto &s: spans)
+ qualifiers[s.key] = s.value;
+
+ // Remove matched spans from the text (replace with a single space to preserve boundaries)
+ std::sort(spans.begin(), spans.end(), [](const Span &a, const Span &b) { return a.start > b.start; });
+ for (const auto &s: spans)
+ query.replace(s.start, s.length, QStringLiteral(" "));
+
+ // Normalize whitespace
+ query = query.simplified();
+}
diff --git a/plugins/warp/ui/shared/misc.h b/plugins/warp/ui/shared/misc.h
index d2e6a1a8..35f640f5 100644
--- a/plugins/warp/ui/shared/misc.h
+++ b/plugins/warp/ui/shared/misc.h
@@ -5,9 +5,11 @@
#include <QStyledItemDelegate>
#include <QTableView>
#include <QVector>
+#include <utility>
#include "binaryninjaapi.h"
#include "filter.h"
+#include "warp.h"
// Used to serialize into the item data for rendering with TokenDataDelegate.
struct TokenData
@@ -16,6 +18,10 @@ struct TokenData
TokenData() = default;
+ TokenData(const std::string& name);
+
+ TokenData(const BinaryNinja::Type &type, const std::string& name);
+
TokenData(const std::vector<BinaryNinja::InstructionTextToken> &tokens)
{
for (const auto &token: tokens)
@@ -72,7 +78,7 @@ class GenericTextFilterModel : public QSortFilterProxyModel
Q_OBJECT
public:
- GenericTextFilterModel(QObject *parent): QSortFilterProxyModel(parent)
+ GenericTextFilterModel(QObject *parent) : QSortFilterProxyModel(parent)
{
}
@@ -82,3 +88,27 @@ public:
bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override;
};
+
+// Used to parse qualifiers out of a user-supplied string (or "query")
+struct ParsedQuery
+{
+ // The actual query, without the qualifiers like source:<uuid>
+ QString query;
+ // The qualifiers used to build other optional parts of the query.
+ QHash<QString, QString> qualifiers;
+
+ ParsedQuery(QString query, const QHash<QString, QString> &qualifiers)
+ : query(std::move(query)), qualifiers(qualifiers)
+ {
+ }
+
+ explicit ParsedQuery(const QString &rawQuery);
+
+ [[nodiscard]] std::optional<QString> GetValue(const QString &key) const
+ {
+ const auto it = qualifiers.constFind(key);
+ if (it == qualifiers.constEnd() || it->isEmpty())
+ return std::nullopt;
+ return it.value();
+ }
+};
diff --git a/plugins/warp/ui/shared/search.cpp b/plugins/warp/ui/shared/search.cpp
new file mode 100644
index 00000000..1431e9b6
--- /dev/null
+++ b/plugins/warp/ui/shared/search.cpp
@@ -0,0 +1,301 @@
+#include "search.h"
+#include "misc.h"
+#include "../../../../../ui/mainwindow.h"
+
+QVariant WarpSearchModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size())
+ return {};
+ const auto &it = m_items[index.row()];
+
+ // Provide tokenized type for functions via UserRole for TokenDataDelegate
+ 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()));
+ return {};
+ }
+
+
+ if (role == Qt::DisplayRole)
+ {
+ // TODO: We might want to run the demangler here on the name.
+ switch (index.column())
+ {
+ case DisplayCol: return QString::fromStdString(it->GetName());
+ case KindCol:
+ {
+ switch (it->GetKind())
+ {
+ case WARPContainerSearchItemKindFunction: return "Function";
+ case WARPContainerSearchItemKindType: return "Type";
+ case WARPContainerSearchItemKindSource: return "Source";
+ case WARPContainerSearchItemKindSymbol: return "Symbol";
+ default: return {};
+ }
+ }
+ case SourceCol: return QString::fromStdString(it->GetSource().ToString());
+ default: return {};
+ }
+ }
+ return {};
+}
+
+QVariant WarpSearchModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
+ {
+ switch (section)
+ {
+ case DisplayCol: return "Item";
+ case KindCol: return "Kind";
+ case SourceCol: return "Source";
+ default: return {};
+ }
+ }
+ return {};
+}
+
+void WarpSearchDelegate::paint(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+ // TODO: We actually still want the function icon, I think? So this should not early return and instead replace the name.
+ // If model provided TokenData (e.g., for function types), render via TokenDataDelegate
+ if (index.data(Qt::UserRole).canConvert<TokenData>())
+ {
+ TokenDataDelegate(parent()).paint(p, option, index);
+ return;
+ }
+
+
+ QStyleOptionViewItem opt(option);
+ initStyleOption(&opt, index);
+
+ // We will draw the background/selection via style, and custom-draw icon (left) + text.
+ const QWidget *w = option.widget;
+ QStyle *style = w ? w->style() : QApplication::style();
+
+ // Let style draw the item background/selection etc., but not the default text
+ QString originalText = opt.text;
+ opt.text.clear();
+ style->drawControl(QStyle::CE_ItemViewItem, &opt, p, w);
+
+ // Retrieve text for DisplayCol
+ const QString &text = originalText;
+
+ // Retrieve kind text from the hidden KindCol
+ const QModelIndex kindIndex = index.sibling(index.row(), WarpSearchModel::KindCol);
+ const QString kind = kindIndex.isValid() ? kindIndex.data(Qt::DisplayRole).toString() : QString();
+
+ // Map kind -> icon (emoji for simplicity; replace with QIcon/QPixmap if desired)
+ const QString icon = kindToIcon(kind);
+
+ // Layout rects
+ const QRect r = opt.rect;
+ const int marginH = 8;
+ const int iconSide = 16;
+ const int spacing = 8;
+
+ // Reserve space on the left for the icon if we have one
+ QRect iconRect = QRect(r.left() + marginH, r.center().y() - iconSide / 2, iconSide, iconSide);
+ QRect textRect = r.adjusted((icon.isEmpty() ? marginH : (marginH + iconSide + spacing)), 0, -marginH, 0);
+
+ // Draw icon first (without clipping), then text with clipping
+ p->save();
+ if (!icon.isEmpty())
+ {
+ QFont iconFont = opt.font;
+ iconFont.setPointSizeF(iconFont.pointSizeF() + 2);
+ p->setFont(iconFont);
+ p->setPen(
+ opt.palette.color(opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text));
+ p->drawText(iconRect, Qt::AlignCenter, icon);
+ }
+ p->restore();
+
+ // Draw elided text, vertically centered (with clipping so long strings don't overflow)
+ p->save();
+ p->setClipRect(textRect);
+ const QFontMetrics fm(opt.font);
+ const QString elided = fm.elidedText(text, Qt::ElideRight, textRect.width());
+ p->setPen(opt.palette.color(opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text));
+ p->drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft, elided);
+ p->restore();
+}
+
+void WarpSearchRunner::fetchPage(std::size_t offset, std::size_t limit)
+{
+ const auto session = currentSession();
+ auto container = m_container;
+ auto q = m_queryText;
+ auto src = m_source;
+
+ // Runs search on Qt threadpool then moves the result to the main thread to update the UI.
+ using TaskResult = std::pair<std::optional<Warp::ContainerSearchResponse>, QString>;
+ QFutureWatcher<TaskResult> *watcher = new QFutureWatcher<TaskResult>(this);
+ const auto future = QtConcurrent::run([container, q, src, offset, limit]() -> TaskResult {
+ const Warp::ContainerSearchQuery query(q.toStdString(), offset, limit, src);
+ auto respOpt = container->Search(query);
+ // TODO: We may want to provide better errors in the future, e.g. BNWARPGetError
+ if (!respOpt.has_value())
+ return {std::nullopt, QStringLiteral("No response from container")};
+ return {std::move(respOpt), QString()};
+ });
+ connect(watcher, &QFutureWatcher<TaskResult>::finished, this, [this, watcher, session]() {
+ const TaskResult result = watcher->result();
+ watcher->deleteLater();
+ if (const auto &err = result.second; !err.isEmpty())
+ emit pageError(session, err);
+ else if (const auto &resp = result.first; resp.has_value())
+ emit pageReady(session, *resp);
+ });
+ watcher->setFuture(future);
+}
+
+WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget *parent) : QWidget(parent)
+{
+ m_model = new WarpSearchModel(this);
+ m_runner = new WarpSearchRunner(container, this);
+ auto *layout = new QVBoxLayout(this);
+ m_query = new QLineEdit(this);
+ m_status = new QLabel(this);
+ m_view = new QTableView(this);
+
+ // TODO: I am not necessarily a fan with how this looks.
+ m_query->setPlaceholderText("Search… (Optionally filter with source:<uuid>)");
+
+ // This is where we make the table not look like a table but instead a list.
+ m_view->setShowGrid(false);
+ m_view->verticalHeader()->setVisible(false);
+ m_view->horizontalHeader()->setVisible(false);
+ m_view->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_view->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_view->setFocusPolicy(Qt::NoFocus);
+
+ layout->addWidget(m_query);
+ layout->addWidget(m_status);
+ layout->addWidget(m_view);
+
+ m_view->setModel(m_model);
+
+ // Use a delegate to draw the DisplayCol text with an icon (optionally) aligned to the left.
+ m_view->setItemDelegateForColumn(WarpSearchModel::DisplayCol, new WarpSearchDelegate(m_view));
+
+ // Hide extra columns: only show the combined "Item" column; do not display source GUID.
+ m_view->setColumnHidden(WarpSearchModel::KindCol, true);
+ m_view->setColumnHidden(WarpSearchModel::SourceCol, true);
+ m_view->horizontalHeader()->setSectionResizeMode(WarpSearchModel::DisplayCol, QHeaderView::Stretch);
+
+ // Debounce user input
+ m_debounce.setSingleShot(true);
+ m_debounce.setInterval(SEARCH_DEBOUNCE_MS);
+ connect(m_query, &QLineEdit::textChanged, this, [this](const QString &) { m_debounce.start(); });
+
+ connect(&m_debounce, &QTimer::timeout, this, [this]() {
+ const QString raw = m_query->text();
+
+ // Parse generic qualifiers and clean the free-text query
+ const auto parsed = ParsedQuery(raw);
+
+ std::optional<Warp::Source> inlineSource = std::nullopt;
+ if (const auto val = parsed.GetValue("source"); val.has_value())
+ inlineSource = Warp::Source::FromString(val.value().toStdString());
+ const auto effectiveSource = inlineSource.has_value() ? inlineSource : m_source;
+ // TODO: Filter for tags and function guid.
+
+ m_runner->startSession(parsed.query, effectiveSource);
+ m_model->beginNewSearch(SEARCH_PAGE_SIZE);
+ });
+
+
+ // Infinite scroll trigger
+ connect(m_model, &WarpSearchModel::fetchMoreRequested, this, [this](std::size_t offset, std::size_t limit) {
+ m_status->setText(QStringLiteral("Loading… (%1/%2)").arg(m_model->currentCount()).arg(m_model->total()));
+ m_runner->fetchPage(offset, limit);
+ });
+
+ // Append results if session still valid
+ connect(m_runner, &WarpSearchRunner::pageReady, this,
+ [this](std::uint64_t, const Warp::ContainerSearchResponse &resp) {
+ m_model->appendResponse(resp);
+ m_status->setText(
+ QStringLiteral("Showing %1 of %2").arg(m_model->currentCount()).arg(m_model->total()));
+ });
+
+ // In cases like if the network container server goes down.
+ connect(m_runner, &WarpSearchRunner::pageError, this,
+ [this](std::uint64_t, const QString &msg) {
+ m_status->setText(QStringLiteral("Error: %1").arg(msg));
+ });
+
+ // Add a context menu so that we can actually do actionable things with what we find.
+ m_view->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(m_view, &QTableView::customContextMenuRequested, this, [this](const QPoint &pos) {
+ const QModelIndex idx = m_view->indexAt(pos);
+ if (!idx.isValid())
+ return;
+
+ // TODO: Getting the current view here is really awful, but i dont care right now.
+ auto ctx = MainWindow::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 itemFunc = item->GetFunction();
+
+ QMenu menu(this);
+ QAction *copySourceId = menu.addAction(tr("Copy Source"));
+ QAction *searchSource = menu.addAction(tr("Search Source"));
+ QAction *applyType = menu.addAction(tr("Apply Type"));
+ QAction *applyFunction = menu.addAction(tr("Apply to Current Function"));
+
+ // 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->setVisible(applyType->isEnabled());
+
+ applyFunction->setEnabled(func != nullptr && itemFunc);
+ applyFunction->setVisible(applyFunction->isEnabled());
+
+ QAction *chosen = menu.exec(m_view->viewport()->mapToGlobal(pos));
+ if (!chosen)
+ return;
+ if (chosen == copySourceId)
+ QApplication::clipboard()->setText(QString::fromStdString(item->GetSource().ToString()));
+ else if (chosen == searchSource)
+ {
+ const QString sourceId = QString::fromStdString(item->GetSource().ToString());
+ // TODO: This does not preserve the query that existed prior, doing so may result in duplicate source qualifiers.
+ m_query->setText(QStringLiteral("source:\"%1\"").arg(sourceId));
+ } else if (chosen == applyType)
+ {
+ if (func && item->GetKind() == WARPContainerSearchItemKindFunction)
+ {
+ func->SetUserType(itemType);
+ binaryView->UpdateAnalysis();
+ }
+ else
+ binaryView->DefineUserType(item->GetName(), itemType);
+ } else if (chosen == applyFunction)
+ {
+ itemFunc->Apply(*func);
+ binaryView->UpdateAnalysis();
+ }
+ });
+
+ // This will start searching with an empty query once the widget is constructed. This is a decent behavior considering
+ // we should be heavily caching queries such as an empty one.
+ m_debounce.start();
+}
diff --git a/plugins/warp/ui/shared/search.h b/plugins/warp/ui/shared/search.h
new file mode 100644
index 00000000..86c90942
--- /dev/null
+++ b/plugins/warp/ui/shared/search.h
@@ -0,0 +1,234 @@
+#pragma once
+
+#include <QAbstractTableModel>
+#include <QApplication>
+#include <QClipboard>
+#include <QFutureWatcher>
+#include <QHeaderView>
+#include <QLabel>
+#include <QLineEdit>
+#include <QMenu>
+#include <QObject>
+#include <QPainter>
+#include <QVector>
+#include <QString>
+#include <QStyledItemDelegate>
+#include <QTableView>
+#include <QTimer>
+#include <QVBoxLayout>
+#include <QtConcurrent/QtConcurrentRun>
+
+#include "uitypes.h"
+#include "warp.h"
+
+// Will search in batches of 50 items.
+constexpr auto SEARCH_PAGE_SIZE = 50;
+// Will debounce the search for 350 MS.
+constexpr auto SEARCH_DEBOUNCE_MS = 350;
+
+// Table model showing a paginated list of SearchItem.
+class WarpSearchModel final : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ enum Columns : int
+ {
+ DisplayCol = 0,
+ KindCol,
+ SourceCol,
+ ColumnCount
+ };
+
+ explicit WarpSearchModel(QObject *parent = nullptr)
+ : QAbstractTableModel(parent)
+ {
+ }
+
+ int rowCount(const QModelIndex &parent) const override
+ {
+ if (parent.isValid()) return 0;
+ return m_items.size();
+ }
+
+ int columnCount(const QModelIndex &parent) 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;
+
+ // Begin a new search session: clears all current items and resets counters.
+ Q_INVOKABLE void beginNewSearch(std::size_t pageSize = SEARCH_PAGE_SIZE)
+ {
+ beginResetModel();
+ m_items.clear();
+ m_total = 0;
+ m_pageSize = pageSize;
+ endResetModel();
+ emit cleared();
+ // Immediately ask for the first page from offset 0
+ emit fetchMoreRequested(0, m_pageSize);
+ }
+
+ // Append results from a Warp::ContainerSearchResponse (infinite scroll, no replacement).
+ void appendResponse(const Warp::ContainerSearchResponse &resp)
+ {
+ // Update total first (so canFetchMore reflects new value)
+ m_total = resp.total;
+ if (resp.items.empty())
+ {
+ emit responseUpdated(currentCount(), m_total);
+ return;
+ }
+ const int begin = m_items.size();
+ const int end = begin + static_cast<int>(resp.items.size()) - 1;
+ beginInsertRows(QModelIndex(), begin, end);
+ for (const auto &refItem: resp.items)
+ m_items.push_back(refItem);
+ endInsertRows();
+ emit responseUpdated(currentCount(), m_total);
+ }
+
+ // Qt's infinite scroll hooks: the view will call these when near the end.
+ bool canFetchMore(const QModelIndex &parent) const override
+ {
+ Q_UNUSED(parent);
+ return static_cast<std::size_t>(m_items.size()) < m_total;
+ }
+
+ void fetchMore(const QModelIndex &parent) override
+ {
+ Q_UNUSED(parent);
+ if (!canFetchMore({}))
+ return;
+ const std::size_t nextOffset = m_items.size();
+ emit fetchMoreRequested(nextOffset, m_pageSize);
+ }
+
+ // Convenience accessors.
+ std::size_t total() const { return m_total; }
+ int currentCount() const { return m_items.size(); }
+ void setPageSize(std::size_t pageSize) { m_pageSize = pageSize; }
+
+ Q_INVOKABLE Warp::Ref<Warp::ContainerSearchItem> itemAt(int row) const
+ {
+ if (row < 0 || row >= m_items.size())
+ return {};
+ return m_items[row];
+ }
+
+signals:
+ // Emitted when model is cleared for a new query.
+ void cleared();
+
+ // Emitted after items are appended or totals updated.
+ void responseUpdated(int currentCount, std::size_t total);
+
+ // Request the next page; connect this to your async search runner.
+ void fetchMoreRequested(std::size_t offset, std::size_t limit);
+
+private:
+ QVector<Warp::Ref<Warp::ContainerSearchItem> > m_items;
+ std::size_t m_total = 0;
+ std::size_t m_pageSize = SEARCH_PAGE_SIZE;
+};
+
+// A delegate to render the DisplayCol text with an icon (mapped from KindCol) on the right.
+class WarpSearchDelegate final : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit WarpSearchDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent)
+ {
+ }
+
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
+ {
+ // Base text size + space for an icon on the right
+ const QSize base = QStyledItemDelegate::sizeHint(option, index);
+ const int iconSide = 16;
+ return QSize(base.width(), qMax(base.height(), iconSide + 4)); // keep row height >= icon
+ }
+
+ void paint(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+
+private:
+ static QString kindToIcon(const QString &kind)
+ {
+ // Example mapping; extend as needed
+ if (kind.compare(QStringLiteral("Function"), Qt::CaseInsensitive) == 0)
+ return QStringLiteral("ƒ");
+ if (kind.compare(QStringLiteral("Type"), Qt::CaseInsensitive) == 0)
+ return QStringLiteral("𝑇");
+ if (kind.compare(QStringLiteral("Source"), Qt::CaseInsensitive) == 0)
+ return QStringLiteral("📦");
+ // Default: no icon
+ return {};
+ }
+};
+
+class WarpSearchRunner : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit WarpSearchRunner(Warp::Ref<Warp::Container> container, QObject *parent = nullptr)
+ : QObject(parent), m_container(std::move(container))
+ {
+ }
+
+signals:
+ // Emitted on the UI thread after the worker finishes
+ void pageReady(std::uint64_t sessionId, Warp::ContainerSearchResponse resp);
+
+ void pageError(std::uint64_t sessionId, QString message);
+
+public slots:
+ // Start a new logical search session (new query or new filters)
+ void startSession(QString queryText, const std::optional<Warp::Source> &source)
+ {
+ m_queryText = std::move(queryText);
+ m_source = source;
+ m_sessionId.fetchAndAddRelaxed(1);
+ }
+
+ // Fetch a page for the current session. Safe to call multiple times (infinite scroll).
+ void fetchPage(std::size_t offset, std::size_t limit);
+
+private:
+ std::uint64_t currentSession() const { return m_sessionId.loadRelaxed(); }
+
+ Warp::Ref<Warp::Container> m_container;
+ QAtomicInteger<quint64> m_sessionId{0};
+ QString m_queryText;
+ std::optional<Warp::Source> m_source;
+};
+
+class WarpSearchWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget *parent = nullptr);
+
+ void setSourceFilter(const std::optional<Warp::Source> &src)
+ {
+ m_source = src;
+ m_debounce.start();
+ }
+
+private:
+ WarpSearchModel *m_model;
+ WarpSearchRunner *m_runner;
+ QLineEdit *m_query;
+ QLabel *m_status;
+ QTableView *m_view;
+ QTimer m_debounce;
+ // Source to filter for if no source is provided in the search.
+ std::optional<Warp::Source> m_source;
+};