summaryrefslogtreecommitdiff
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
parenta1c46813e7f279aa4cfdb9dbb91c45b559ebeacd (diff)
[WARP] Enhanced network support
-rw-r--r--Cargo.lock75
-rw-r--r--plugins/warp/Cargo.toml4
-rw-r--r--plugins/warp/api/python/warp.py122
-rw-r--r--plugins/warp/api/python/warp_enums.py7
-rw-r--r--plugins/warp/api/warp.cpp113
-rw-r--r--plugins/warp/api/warp.h96
-rw-r--r--plugins/warp/api/warpcore.h40
-rw-r--r--plugins/warp/benches/function.rs9
-rw-r--r--plugins/warp/src/container.rs90
-rw-r--r--plugins/warp/src/container/disk.rs57
-rw-r--r--plugins/warp/src/container/memory.rs15
-rw-r--r--plugins/warp/src/container/network.rs133
-rw-r--r--plugins/warp/src/container/network/client.rs405
-rw-r--r--plugins/warp/src/convert/types.rs11
-rw-r--r--plugins/warp/src/matcher.rs10
-rw-r--r--plugins/warp/src/plugin.rs84
-rw-r--r--plugins/warp/src/plugin/commit.rs150
-rw-r--r--plugins/warp/src/plugin/ffi.rs12
-rw-r--r--plugins/warp/src/plugin/ffi/container.rs271
-rw-r--r--plugins/warp/src/plugin/ffi/file.rs38
-rw-r--r--plugins/warp/src/plugin/ffi/function.rs2
-rw-r--r--plugins/warp/src/plugin/load.rs11
-rw-r--r--plugins/warp/src/plugin/settings.rs76
-rw-r--r--plugins/warp/src/plugin/workflow.rs10
-rw-r--r--plugins/warp/tests/determinism.rs4
-rw-r--r--plugins/warp/tests/matcher.rs2
-rw-r--r--plugins/warp/ui/CMakeLists.txt5
-rw-r--r--plugins/warp/ui/containers.cpp274
-rw-r--r--plugins/warp/ui/containers.h175
-rw-r--r--plugins/warp/ui/matches.cpp89
-rw-r--r--plugins/warp/ui/matches.h12
-rw-r--r--plugins/warp/ui/plugin.cpp90
-rw-r--r--plugins/warp/ui/plugin.h2
-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
42 files changed, 3371 insertions, 256 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 93b6283d..ab4940fb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -322,6 +322,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
name = "cc"
version = "1.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -450,6 +459,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
+name = "compact_str"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "serde",
+ "static_assertions",
+]
+
+[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -639,6 +663,27 @@ dependencies = [
]
[[package]]
+name = "directories"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
name = "dispatch2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1440,6 +1485,16 @@ dependencies = [
]
[[package]]
+name = "libredox"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
+dependencies = [
+ "bitflags 2.9.1",
+ "libc",
+]
+
+[[package]]
name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1831,6 +1886,12 @@ dependencies = [
]
[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
name = "parking_lot"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2098,6 +2159,17 @@ dependencies = [
]
[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror 2.0.12",
+]
+
+[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3021,6 +3093,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom 0.3.3",
"js-sys",
+ "serde",
"sha1_smol",
"wasm-bindgen",
]
@@ -3076,8 +3149,10 @@ dependencies = [
"arboard",
"binaryninja",
"binaryninjacore-sys",
+ "compact_str",
"criterion",
"dashmap",
+ "directories",
"insta",
"itertools 0.14.0",
"log",
diff --git a/plugins/warp/Cargo.toml b/plugins/warp/Cargo.toml
index 761ac961..702c500a 100644
--- a/plugins/warp/Cargo.toml
+++ b/plugins/warp/Cargo.toml
@@ -19,12 +19,14 @@ arboard = "3.4"
walkdir = "2.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-uuid = { version = "1.12.0", features = ["v4"] }
+uuid = { version = "1.12.0", features = ["v4", "serde"] }
thiserror = "2.0"
ar = { git = "https://github.com/mdsteele/rust-ar" }
tempdir = "0.3.7"
regex = "1.11"
reqwest = { version = "0.12", features = ["blocking", "json", "multipart"] }
+directories = "6.0"
+compact_str = { version = "0.9.0", features = ["serde"] }
# For reports
minijinja = "2.10.2"
diff --git a/plugins/warp/api/python/warp.py b/plugins/warp/api/python/warp.py
index 66fef872..29537895 100644
--- a/plugins/warp/api/python/warp.py
+++ b/plugins/warp/api/python/warp.py
@@ -8,6 +8,7 @@ from binaryninja import BinaryView, Function, BasicBlock, Architecture, Platform
from binaryninja._binaryninjacore import BNFreeString, BNAllocString, BNType
from . import _warpcore as warpcore
+from .warp_enums import WARPContainerSearchItemKind
class WarpUUID:
@@ -195,6 +196,108 @@ class WarpFunction:
warpcore.BNWARPFunctionApply(self.handle, function.handle)
+class WarpContainerSearchQuery:
+ def __init__(self, query: str, offset: Optional[int] = None, limit: Optional[int] = None, source: Optional[Source] = None, source_tags: Optional[List[str]] = None):
+ self.query = query
+ self.source = source
+ self.offset = offset
+ self.limit = limit
+ offset_ptr = None
+ if offset is not None:
+ self._c_offset = ctypes.c_size_t(offset)
+ offset_ptr = ctypes.byref(self._c_offset)
+ limit_ptr = None
+ if limit is not None:
+ self._c_limit = ctypes.c_size_t(limit)
+ limit_ptr = ctypes.byref(self._c_limit)
+ source_ptr = None
+ if source is not None:
+ self._c_source = source.uuid
+ source_ptr = ctypes.byref(self._c_source)
+ source_tags_len = 0
+ source_tags_array_ptr = None
+ if source_tags is not None:
+ source_tags_ptr = (ctypes.c_char_p * len(source_tags))()
+ source_tags_len = len(source_tags)
+ for i in range(len(source_tags)):
+ source_tags_ptr[i] = source_tags[i].encode('utf-8')
+ source_tags_array_ptr = ctypes.cast(source_tags_ptr, ctypes.POINTER(ctypes.c_char_p))
+ self.handle = warpcore.BNWARPNewContainerSearchQuery(query, offset_ptr, limit_ptr, source_ptr, source_tags_array_ptr, source_tags_len)
+
+ def __del__(self):
+ if self.handle is not None:
+ warpcore.BNWARPFreeContainerSearchQueryReference(self.handle)
+
+ def __repr__(self):
+ # TODO: Display offset and limit in a pythonic way.
+ if self.source is None:
+ return f"<WarpContainerSearchQuery '{self.query}'>"
+ return f"<WarpContainerSearchQuery '{self.query}': '{self.source}'>"
+
+
+class WarpContainerSearchItem:
+ def __init__(self, handle: warpcore.BNWARPContainerSearchItem):
+ self.handle = handle
+
+ def __del__(self):
+ if self.handle is not None:
+ warpcore.BNWARPFreeContainerSearchItemReference(self.handle)
+
+ @property
+ def kind(self) -> WARPContainerSearchItemKind:
+ return WARPContainerSearchItemKind(warpcore.BNWARPContainerSearchItemGetKind(self.handle))
+
+ @property
+ def source(self) -> Source:
+ return Source(warpcore.BNWARPContainerSearchItemGetSource(self.handle))
+
+ @property
+ def name(self) -> str:
+ return warpcore.BNWARPContainerSearchItemGetName(self.handle)
+
+ def get_type(self, arch: Architecture) -> Optional[Type]:
+ ty = warpcore.BNWARPContainerSearchItemGetType(arch.handle, self.handle)
+ if not ty:
+ return None
+ return Type(ty)
+
+ @property
+ def function(self) -> Optional[WarpFunction]:
+ func = warpcore.BNWARPContainerSearchItemGetFunction(self.handle)
+ if not func:
+ return None
+ return WarpFunction(func)
+
+ def __repr__(self):
+ return f"<WarpContainerSearchItem '{self.name}': '{self.source}'>"
+
+
+class WarpContainerResponse:
+ def __init__(self, items: List[WarpContainerSearchItem], offset: int, total: int):
+ self.items = items
+ self.offset = offset
+ self.total = total
+
+ def __iter__(self):
+ return iter(self.items)
+
+ def __len__(self):
+ return len(self.items)
+
+ def __repr__(self):
+ return f"<WarpContainerResponse items={len(self.items)} offset={self.offset} total={self.total}>"
+
+ @staticmethod
+ def from_api(response: warpcore.BNWARPContainerSearchResponse) -> 'WarpContainerResponse':
+ try:
+ items = []
+ for i in range(response.count):
+ items.append(WarpContainerSearchItem(warpcore.BNWARPNewContainerSearchItemReference(response.items[i])))
+ return WarpContainerResponse(items=items, offset=response.offset, total=response.total)
+ finally:
+ warpcore.BNWARPFreeContainerSearchResponse(response)
+
+
class _WarpContainerMetaclass(type):
def __iter__(self):
binaryninja._init_plugins()
@@ -305,12 +408,19 @@ class WarpContainer(metaclass=_WarpContainerMetaclass):
core_guids[i] = guids[i].uuid
return warpcore.BNWARPContainerRemoveTypes(self.handle, source.uuid, core_guids, count)
- def fetch_functions(self, target: WarpTarget, guids: List[FunctionGUID]):
+ def fetch_functions(self, target: WarpTarget, guids: List[FunctionGUID], source_tags: Optional[List[str]] = None):
count = len(guids)
core_guids = (warpcore.BNWARPFunctionGUID * count)()
for i in range(count):
core_guids[i] = guids[i].uuid
- warpcore.BNWARPContainerFetchFunctions(self.handle, target.handle, core_guids, count)
+ if source_tags is None:
+ source_tags = []
+ source_tags_ptr = (ctypes.c_char_p * len(source_tags))()
+ source_tags_len = len(source_tags)
+ for i in range(len(source_tags)):
+ source_tags_ptr[i] = source_tags[i].encode('utf-8')
+ source_tags_array_ptr = ctypes.cast(source_tags_ptr, ctypes.POINTER(ctypes.c_char_p))
+ warpcore.BNWARPContainerFetchFunctions(self.handle, target.handle, source_tags_array_ptr, source_tags_len, core_guids, count)
def get_sources_with_function_guid(self, target: WarpTarget, guid: FunctionGUID) -> List[Source]:
count = ctypes.c_size_t()
@@ -346,7 +456,7 @@ class WarpContainer(metaclass=_WarpContainerMetaclass):
return result
def get_type_with_guid(self, arch: Architecture, source: Source, guid: TypeGUID) -> Optional[Type]:
- ty = warpcore.BNWARPContainerGetTypeWithGUID(self.handle, arch.handle, source.uuid, guid.uuid)
+ ty = warpcore.BNWARPContainerGetTypeWithGUID(arch.handle, self.handle, source.uuid, guid.uuid)
if not ty:
return None
return Type(ty)
@@ -362,6 +472,12 @@ class WarpContainer(metaclass=_WarpContainerMetaclass):
warpcore.BNWARPFreeUUIDList(guids, count.value)
return result
+ def search(self, query: WarpContainerSearchQuery) -> Optional[WarpContainerResponse]:
+ response = warpcore.BNWARPContainerSearch(self.handle, query.handle)
+ if not response:
+ return None
+ return WarpContainerResponse.from_api(response.contents)
+
def run_matcher(view: BinaryView):
warpcore.BNWARPRunMatcher(view.handle)
diff --git a/plugins/warp/api/python/warp_enums.py b/plugins/warp/api/python/warp_enums.py
index e47ff1c3..86be3a6c 100644
--- a/plugins/warp/api/python/warp_enums.py
+++ b/plugins/warp/api/python/warp_enums.py
@@ -1 +1,8 @@
import enum
+
+
+class WARPContainerSearchItemKind(enum.IntEnum):
+ WARPContainerSearchItemKindSource = 0
+ WARPContainerSearchItemKindFunction = 1
+ WARPContainerSearchItemKindType = 2
+ WARPContainerSearchItemKindSymbol = 3
diff --git a/plugins/warp/api/warp.cpp b/plugins/warp/api/warp.cpp
index 52c7ea9d..f6106605 100644
--- a/plugins/warp/api/warp.cpp
+++ b/plugins/warp/api/warp.cpp
@@ -5,6 +5,14 @@
using namespace Warp;
+std::optional<WarpUUID> WarpUUID::FromString(const std::string &str)
+{
+ BNWARPUUID uuid = {};
+ if (!BNWARPUUIDFromString(str.c_str(), &uuid))
+ return std::nullopt;
+ return WarpUUID(uuid);
+}
+
std::string WarpUUID::ToString() const
{
char *str = BNWARPUUIDGetString(&uuid);
@@ -133,6 +141,94 @@ void Function::RemoveMatch(const BinaryNinja::Function &function)
BNWARPFunctionApply(nullptr, function.m_object);
}
+ContainerSearchQuery::ContainerSearchQuery(BNWARPContainerSearchQuery *query)
+{
+ m_object = query;
+}
+
+ContainerSearchQuery::ContainerSearchQuery(const std::string &query)
+{
+ m_object = BNWARPNewContainerSearchQuery(query.c_str(), nullptr, nullptr, nullptr, nullptr, 0);
+}
+
+ContainerSearchQuery::ContainerSearchQuery(const std::string &query, const Source &source)
+{
+ m_object = BNWARPNewContainerSearchQuery(query.c_str(), nullptr, nullptr, source.Raw(), nullptr, 0);
+}
+
+ContainerSearchQuery::ContainerSearchQuery(const std::string &query, size_t offset, size_t limit, const std::optional<Source> &source, const std::vector<SourceTag> &tags)
+{
+ size_t tagCount = tags.size();
+ const char** rawTags = new const char*[tagCount];
+ for (size_t i = 0; i < tagCount; i++)
+ rawTags[i] = tags[i].c_str();
+ if (source)
+ m_object = BNWARPNewContainerSearchQuery(query.c_str(), &offset, &limit, source.value().Raw(), rawTags, tagCount);
+ else
+ m_object = BNWARPNewContainerSearchQuery(query.c_str(), &offset, &limit, nullptr, rawTags, tagCount);
+ delete[] rawTags;
+}
+
+ContainerSearchItem::ContainerSearchItem(BNWARPContainerSearchItem *item)
+{
+ m_object = item;
+}
+
+BNWARPContainerSearchItemKind ContainerSearchItem::GetKind() const
+{
+ return BNWARPContainerSearchItemGetKind(m_object);
+}
+
+Source ContainerSearchItem::GetSource() const
+{
+ return BNWARPContainerSearchItemGetSource(m_object);
+}
+
+BinaryNinja::Ref<BinaryNinja::Type> ContainerSearchItem::GetType(const BinaryNinja::Ref<BinaryNinja::Architecture> &arch) const
+{
+ BNType *type = BNWARPContainerSearchItemGetType(arch ? arch->m_object : nullptr, m_object);
+ if (!type)
+ return nullptr;
+ return new BinaryNinja::Type(type);
+}
+
+std::string ContainerSearchItem::GetName() const
+{
+ // NOTE: In the future we may want the name to be optional, see rust core side for more info.
+ char *rawName = BNWARPContainerSearchItemGetName(m_object);
+ std::string name = rawName;
+ BNFreeString(rawName);
+ return name;
+}
+
+Ref<Function> ContainerSearchItem::GetFunction() const
+{
+ BNWARPFunction *function = BNWARPContainerSearchItemGetFunction(m_object);
+ if (!function)
+ return nullptr;
+ return new Function(function);
+}
+
+
+ContainerSearchResponse::ContainerSearchResponse(std::vector<Ref<ContainerSearchItem>>&& items, size_t offset,
+ size_t total)
+{
+ this->items = std::move(items);
+ this->offset = offset;
+ this->total = total;
+}
+
+ContainerSearchResponse ContainerSearchResponse::FromAPIObject(BNWARPContainerSearchResponse *response)
+{
+ std::vector<Ref<ContainerSearchItem>> items;
+ items.reserve(response->count);
+ for (int i = 0; i < response->count; i++)
+ items.push_back(new ContainerSearchItem(BNWARPNewContainerSearchItemReference(response->items[i])));
+ ContainerSearchResponse resp = {std::move(items), response->offset, response->total};
+ BNWARPFreeContainerSearchResponse(response);
+ return resp;
+}
+
Container::Container(BNWARPContainer *container)
{
m_object = container;
@@ -249,14 +345,19 @@ bool Container::RemoveTypes(const Source &source, const std::vector<TypeGUID> &g
return result;
}
-void Container::FetchFunctions(const Target &target, const std::vector<FunctionGUID> &guids) const
+void Container::FetchFunctions(const Target &target, const std::vector<FunctionGUID> &guids, const std::vector<SourceTag> &tags) const
{
size_t count = guids.size();
BNWARPFunctionGUID *apiGuids = new BNWARPFunctionGUID[count];
for (size_t i = 0; i < count; i++)
apiGuids[i] = *guids[i].Raw();
- BNWARPContainerFetchFunctions(m_object, target.m_object, apiGuids, count);
+ size_t tagCount = tags.size();
+ const char** rawTags = new const char*[tagCount];
+ for (size_t i = 0; i < tagCount; i++)
+ rawTags[i] = tags[i].c_str();
+ BNWARPContainerFetchFunctions(m_object, target.m_object, rawTags, tagCount, apiGuids, count);
delete[] apiGuids;
+ delete[] rawTags;
}
std::vector<Source> Container::GetSourcesWithFunctionGUID(const Target& target, const FunctionGUID &guid) const
@@ -314,6 +415,14 @@ std::vector<TypeGUID> Container::GetTypeGUIDsWithName(const Source &source, cons
return result;
}
+std::optional<ContainerSearchResponse> Container::Search(const ContainerSearchQuery &query) const
+{
+ BNWARPContainerSearchResponse *response = BNWARPContainerSearch(m_object, query.m_object);
+ if (!response)
+ return std::nullopt;
+ return ContainerSearchResponse::FromAPIObject(response);
+}
+
void Warp::RunMatcher(const BinaryNinja::BinaryView &view)
{
BNWARPRunMatcher(view.m_object);
diff --git a/plugins/warp/api/warp.h b/plugins/warp/api/warp.h
index 807ba998..14be9a0d 100644
--- a/plugins/warp/api/warp.h
+++ b/plugins/warp/api/warp.h
@@ -68,7 +68,7 @@ namespace Warp {
{
T *m_obj;
#ifdef BN_REF_COUNT_DEBUG
- void* m_assignmentTrace = nullptr;
+ void *m_assignmentTrace = nullptr;
#endif
public:
@@ -230,8 +230,12 @@ namespace Warp {
public:
WarpUUID() = default;
-
- WarpUUID(BNWARPUUID uuid) : uuid(uuid) {}
+
+ WarpUUID(BNWARPUUID uuid) : uuid(uuid)
+ {
+ }
+
+ static std::optional<WarpUUID> FromString(const std::string &str);
std::string ToString() const;
@@ -245,12 +249,12 @@ namespace Warp {
return !(*this == other);
}
- BNWARPUUID* RawMut()
+ BNWARPUUID *RawMut()
{
return &uuid;
}
- const BNWARPUUID* Raw() const
+ const BNWARPUUID *Raw() const
{
return &uuid;
}
@@ -261,6 +265,7 @@ namespace Warp {
typedef WarpUUID FunctionGUID;
typedef WarpUUID ConstraintGUID;
typedef WarpUUID TypeGUID;
+ typedef std::string SourceTag;
class Target : public WarpRefCountObject<BNWARPTarget, BNWARPNewTargetReference,
BNWARPFreeTargetReference>
@@ -268,9 +273,9 @@ namespace Warp {
public:
explicit Target(BNWARPTarget *target);
- static Ref<Target> FromPlatform(const BinaryNinja::Platform& platform);
+ static Ref<Target> FromPlatform(const BinaryNinja::Platform &platform);
};
-
+
struct Constraint
{
ConstraintGUID guid;
@@ -278,7 +283,7 @@ namespace Warp {
Constraint(ConstraintGUID guid, std::optional<int64_t> offset);
- static Constraint FromAPIObject(BNWARPConstraint* constraint);
+ static Constraint FromAPIObject(BNWARPConstraint *constraint);
};
struct FunctionComment
@@ -288,7 +293,7 @@ namespace Warp {
FunctionComment(std::string text, int64_t offset);
- static FunctionComment FromAPIObject(BNWARPFunctionComment* comment);
+ static FunctionComment FromAPIObject(BNWARPFunctionComment *comment);
};
class Function : public WarpRefCountObject<BNWARPFunction, BNWARPNewFunctionReference, BNWARPFreeFunctionReference>
@@ -322,6 +327,51 @@ namespace Warp {
static void RemoveMatch(const BinaryNinja::Function &function);
};
+ class ContainerSearchQuery : public WarpRefCountObject<BNWARPContainerSearchQuery,
+ BNWARPNewContainerSearchQueryReference,
+ BNWARPFreeContainerSearchQueryReference>
+ {
+ public:
+ explicit ContainerSearchQuery(BNWARPContainerSearchQuery *query);
+
+ explicit ContainerSearchQuery(const std::string &query);
+
+ ContainerSearchQuery(const std::string &query, const Source &source);
+
+ ContainerSearchQuery(const std::string &query, size_t offset, size_t limit,
+ const std::optional<Source> &source = std::nullopt,
+ const std::vector<SourceTag> &tags = {});
+ };
+
+ class ContainerSearchItem : public WarpRefCountObject<BNWARPContainerSearchItem,
+ BNWARPNewContainerSearchItemReference,
+ BNWARPFreeContainerSearchItemReference>
+ {
+ public:
+ explicit ContainerSearchItem(BNWARPContainerSearchItem *item);
+
+ BNWARPContainerSearchItemKind GetKind() const;
+
+ Source GetSource() const;
+
+ BinaryNinja::Ref<BinaryNinja::Type> GetType(const BinaryNinja::Ref<BinaryNinja::Architecture> &arch) const;
+
+ std::string GetName() const;
+
+ Ref<Function> GetFunction() const;
+ };
+
+ struct ContainerSearchResponse
+ {
+ std::vector<Ref<ContainerSearchItem> > items;
+ size_t offset;
+ size_t total;
+
+ ContainerSearchResponse(std::vector<Ref<ContainerSearchItem> > &&items, size_t offset, size_t total);
+
+ static ContainerSearchResponse FromAPIObject(BNWARPContainerSearchResponse *response);
+ };
+
class Container : public WarpRefCountObject<BNWARPContainer, BNWARPNewContainerReference,
BNWARPFreeContainerReference>
{
@@ -345,36 +395,50 @@ namespace Warp {
std::optional<std::string> SourcePath(const Source &source) const;
- bool AddFunctions(const Target &target, const Source &source, const std::vector<Ref<Function> > &functions) const;
+ bool AddFunctions(const Target &target, const Source &source,
+ const std::vector<Ref<Function> > &functions) const;
bool AddTypes(const BinaryNinja::BinaryView &view, const Source &source,
const std::vector<BinaryNinja::Ref<BinaryNinja::Type> > &types) const;
- bool RemoveFunctions(const Target &target, const Source &source, const std::vector<Ref<Function> > &functions) const;
+ bool RemoveFunctions(const Target &target, const Source &source,
+ const std::vector<Ref<Function> > &functions) const;
bool RemoveTypes(const Source &source, const std::vector<TypeGUID> &guids) const;
- void FetchFunctions(const Target& target, const std::vector<FunctionGUID> &guids) const;
+ void FetchFunctions(const Target &target, const std::vector<FunctionGUID> &guids, const std::vector<SourceTag> &tags = {}) const;
- std::vector<Source> GetSourcesWithFunctionGUID(const Target& target, const FunctionGUID &guid) const;
+ std::vector<Source> GetSourcesWithFunctionGUID(const Target &target, const FunctionGUID &guid) const;
std::vector<Source> GetSourcesWithTypeGUID(const TypeGUID &guid) const;
- std::vector<Ref<Function> > GetFunctionsWithGUID(const Target& target, const Source &source, const FunctionGUID &guid) const;
+ std::vector<Ref<Function> > GetFunctionsWithGUID(const Target &target, const Source &source,
+ const FunctionGUID &guid) const;
BinaryNinja::Ref<BinaryNinja::Type> GetTypeWithGUID(const BinaryNinja::Architecture &arch, const Source &source,
const TypeGUID &guid) const;
std::vector<TypeGUID> GetTypeGUIDsWithName(const Source &source, const std::string &name) const;
+
+ std::optional<ContainerSearchResponse> Search(const ContainerSearchQuery &query) const;
};
- void RunMatcher(const BinaryNinja::BinaryView& view);
+ void RunMatcher(const BinaryNinja::BinaryView &view);
bool IsInstructionVariant(const BinaryNinja::LowLevelILFunction &function, BinaryNinja::ExprId idx);
bool IsInstructionBlacklisted(const BinaryNinja::LowLevelILFunction &function, BinaryNinja::ExprId idx);
-
+
std::optional<FunctionGUID> GetAnalysisFunctionGUID(const BinaryNinja::Function &function);
std::optional<BasicBlockGUID> GetBasicBlockGUID(const BinaryNinja::BasicBlock &basicBlock);
}
+
+template<> struct std::hash<Warp::WarpUUID>
+{
+ size_t operator()(Warp::WarpUUID const& item) const noexcept
+ {
+ // TODO: Use the raw bytes instead.
+ return std::hash<std::string>()(item.ToString());
+ }
+}; \ No newline at end of file
diff --git a/plugins/warp/api/warpcore.h b/plugins/warp/api/warpcore.h
index 20a59b7b..b62f2427 100644
--- a/plugins/warp/api/warpcore.h
+++ b/plugins/warp/api/warpcore.h
@@ -61,6 +61,7 @@ extern "C"
};
char* BNWARPUUIDGetString(const BNWARPUUID* uuid);
+ bool BNWARPUUIDFromString(const char* str, BNWARPUUID* uuid);
bool BNWARPUUIDEqual(const BNWARPUUID* a, const BNWARPUUID* b);
void BNWARPFreeUUIDList(BNWARPUUID* uuids, size_t count);
@@ -74,6 +75,24 @@ extern "C"
typedef struct BNWARPContainer BNWARPContainer;
typedef struct BNWARPFunction BNWARPFunction;
typedef struct BNWARPConstraint BNWARPConstraint;
+ typedef struct BNWARPContainerSearchQuery BNWARPContainerSearchQuery;
+ typedef struct BNWARPContainerSearchItem BNWARPContainerSearchItem;
+
+ enum BNWARPContainerSearchItemKind
+ {
+ WARPContainerSearchItemKindSource = 0,
+ WARPContainerSearchItemKindFunction = 1,
+ WARPContainerSearchItemKindType = 2,
+ WARPContainerSearchItemKindSymbol = 3,
+ };
+
+ struct BNWARPContainerSearchResponse
+ {
+ size_t count;
+ BNWARPContainerSearchItem** items;
+ size_t offset;
+ size_t total;
+ };
struct BNWARPConstraint
{
@@ -108,7 +127,7 @@ extern "C"
WARP_FFI_API bool BNWARPContainerRemoveFunctions(BNWARPContainer* container, const BNWARPTarget* target, const BNWARPSource* source, BNWARPFunction** functions, size_t count);
WARP_FFI_API bool BNWARPContainerRemoveTypes(BNWARPContainer* container, const BNWARPSource* source, BNWARPTypeGUID* types, size_t count);
- WARP_FFI_API void BNWARPContainerFetchFunctions(BNWARPContainer* container, BNWARPTarget* target, const BNWARPTypeGUID* guids, size_t count);
+ WARP_FFI_API void BNWARPContainerFetchFunctions(BNWARPContainer* container, BNWARPTarget* target, const char** sourceTags, size_t sourceTagCount, const BNWARPTypeGUID* guids, size_t count);
WARP_FFI_API BNWARPSource* BNWARPContainerGetSourcesWithFunctionGUID(BNWARPContainer* container, const BNWARPTarget* target, const BNWARPFunctionGUID* guid, size_t* count);
WARP_FFI_API BNWARPSource* BNWARPContainerGetSourcesWithTypeGUID(BNWARPContainer* container, const BNWARPTypeGUID* guid, size_t* count);
@@ -120,6 +139,25 @@ extern "C"
WARP_FFI_API void BNWARPFreeContainerReference(BNWARPContainer* container);
WARP_FFI_API void BNWARPFreeContainerList(BNWARPContainer** containers, size_t count);
+ WARP_FFI_API BNWARPContainerSearchQuery* BNWARPNewContainerSearchQuery(const char* query, const size_t* offset, const size_t* limit, const BNWARPSource* source, const char** sourceTags, size_t sourceTagCount);
+
+ WARP_FFI_API BNWARPContainerSearchResponse* BNWARPContainerSearch(BNWARPContainer* container, const BNWARPContainerSearchQuery* query);
+
+ WARP_FFI_API BNWARPContainerSearchItemKind BNWARPContainerSearchItemGetKind(BNWARPContainerSearchItem* item);
+ WARP_FFI_API BNWARPSource BNWARPContainerSearchItemGetSource(BNWARPContainerSearchItem* item);
+ WARP_FFI_API BNType* BNWARPContainerSearchItemGetType(BNArchitecture* arch, BNWARPContainerSearchItem* item);
+ WARP_FFI_API char* BNWARPContainerSearchItemGetName(BNWARPContainerSearchItem* item);
+ WARP_FFI_API BNWARPFunction* BNWARPContainerSearchItemGetFunction(BNWARPContainerSearchItem* item);
+
+ WARP_FFI_API BNWARPContainerSearchQuery* BNWARPNewContainerSearchQueryReference(BNWARPContainerSearchQuery* query);
+ WARP_FFI_API void BNWARPFreeContainerSearchQueryReference(BNWARPContainerSearchQuery* query);
+
+ WARP_FFI_API BNWARPContainerSearchItem* BNWARPNewContainerSearchItemReference(BNWARPContainerSearchItem* item);
+ WARP_FFI_API void BNWARPFreeContainerSearchItemReference(BNWARPContainerSearchItem* item);
+ WARP_FFI_API void BNWARPFreeContainerSearchItemList(BNWARPContainerSearchItem** items, size_t count);
+
+ WARP_FFI_API void BNWARPFreeContainerSearchResponse(BNWARPContainerSearchResponse* response);
+
WARP_FFI_API void BNWARPFunctionApply(BNWARPFunction* function, BNFunction* analysisFunction);
WARP_FFI_API BNWARPFunctionGUID BNWARPFunctionGetGUID(BNWARPFunction* function);
WARP_FFI_API BNSymbol* BNWARPFunctionGetSymbol(BNWARPFunction* function, BNFunction* analysisFunction);
diff --git a/plugins/warp/benches/function.rs b/plugins/warp/benches/function.rs
index 526d97c4..299ed1eb 100644
--- a/plugins/warp/benches/function.rs
+++ b/plugins/warp/benches/function.rs
@@ -21,7 +21,7 @@ pub fn function_benchmark(c: &mut Criterion) {
c.bench_function("signature all functions", |b| {
b.iter(|| {
for func in &functions {
- let _ = build_function(&func, &func.lifted_il().unwrap(), false);
+ let _ = build_function(&func, || func.lifted_il().ok(), false);
}
})
});
@@ -32,8 +32,11 @@ pub fn function_benchmark(c: &mut Criterion) {
functions
.par_iter()
.filter_map(|func| {
- let llil = func.lifted_il().ok()?;
- Some(build_function(func.as_ref(), llil.as_ref(), false))
+ Some(build_function(
+ func.as_ref(),
+ || func.lifted_il().ok(),
+ false,
+ ))
})
.collect::<Vec<_>>()
})
diff --git a/plugins/warp/src/container.rs b/plugins/warp/src/container.rs
index 4cabc60e..8c76bc8f 100644
--- a/plugins/warp/src/container.rs
+++ b/plugins/warp/src/container.rs
@@ -1,5 +1,5 @@
use crate::container::disk::NAMESPACE_DISK_SOURCE;
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display};
use std::hash::Hash;
use std::io;
@@ -10,6 +10,7 @@ use uuid::Uuid;
use warp::r#type::guid::TypeGUID;
use warp::r#type::{ComputedType, Type};
use warp::signature::function::{Function, FunctionGUID};
+use warp::symbol::Symbol;
use warp::target::Target;
pub mod disk;
@@ -123,6 +124,77 @@ impl Display for SourcePath {
}
}
+/// A tag associated with a source in a container.
+///
+/// Tags can be used to categorize and filter sources when querying the container.
+pub type SourceTag = compact_str::CompactString;
+
+/// A search query for finding items in a container.
+///
+/// This struct represents a search request that can be used to find functions, types and any other
+/// items associated with the container.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct ContainerSearchQuery {
+ /// The search query string to match against items.
+ pub query: String,
+ /// Optional offset into the results for pagination.
+ pub offset: Option<usize>,
+ /// Optional maximum number of results to return.
+ pub limit: Option<usize>,
+ /// Optional source ID to restrict the search to.
+ pub source: Option<SourceId>,
+ /// Optional list of tags to restrict the search to.
+ pub tags: Vec<SourceTag>,
+ // TODO: Add field for function guid? conceivable someone wants to filter through those.
+}
+
+impl ContainerSearchQuery {
+ pub fn new(query: String) -> Self {
+ Self {
+ query,
+ offset: None,
+ limit: None,
+ source: None,
+ tags: Vec::new(),
+ }
+ }
+}
+
+/// An item returned from a container search.
+///
+/// Contains the source ID where the item was found and the specific kind of item.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ContainerSearchItem {
+ /// The source ID where this item was found
+ pub source: SourceId,
+ /// The specific kind of item that was found
+ pub kind: ContainerSearchItemKind,
+}
+
+/// The kind of item found in a container search.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ContainerSearchItemKind {
+ /// A source identified by its ID
+ Source { path: SourcePath, id: SourceId },
+ /// A function definition
+ Function(Function),
+ /// A type definition
+ Type(Type),
+ /// A symbol definition
+ Symbol(Symbol),
+}
+
+/// Response containing the results of a container search.
+#[derive(Debug, Clone, PartialEq, Eq, Default)]
+pub struct ContainerSearchResponse {
+ /// The matching items found in the search
+ pub items: Vec<ContainerSearchItem>,
+ /// Total number of matching items available
+ pub total: usize,
+ /// Starting offset of these results in the total set
+ pub offset: usize,
+}
+
/// Storage for WARP information.
///
/// Containers are made up of sources, see [`SourceId`] for more details.
@@ -170,6 +242,9 @@ pub trait Container: Send + Sync + Display + Debug {
/// that a source has uncommitted changes.
fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool>;
+ /// Retrieves the set of [`SourceTag`] for the given source.
+ fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>>;
+
/// Retrieve the [`SourcePath`] for the given source.
///
/// NOTE: This does not have to be a filesystem path, its representation is dictated
@@ -216,6 +291,7 @@ pub trait Container: Send + Sync + Display + Debug {
fn fetch_functions(
&mut self,
_target: &Target,
+ _tags: &[SourceTag],
_functions: &[FunctionGUID],
) -> ContainerResult<()> {
Ok(())
@@ -250,7 +326,6 @@ pub trait Container: Send + Sync + Display + Debug {
guid: &FunctionGUID,
) -> ContainerResult<Vec<SourceId>>;
- // TODO: Allocating with Vec is not good.
/// Plural version of [`Container::sources_with_function_guid`].
///
/// Each source will have a list of the containing GUID's so that when looking up a source you give
@@ -276,4 +351,15 @@ pub trait Container: Send + Sync + Display + Debug {
) -> ContainerResult<bool> {
Ok(!self.functions_with_guid(target, source, guid)?.is_empty())
}
+
+ /// Perform a paginated search over the container contents.
+ ///
+ /// The container implementation is responsible for interpreting [`ContainerSearchQuery::query`]
+ /// for example, locally you may not have the capabilities to perform a sane fuzzy search, so the query
+ /// is exact, whereas a database-backed container may opt to instead perform a fuzzy search.
+ ///
+ /// NOTE: This is intended for user-performed actions, as the query may look up over the network.
+ fn search(&self, _query: &ContainerSearchQuery) -> ContainerResult<ContainerSearchResponse> {
+ Ok(ContainerSearchResponse::default())
+ }
}
diff --git a/plugins/warp/src/container/disk.rs b/plugins/warp/src/container/disk.rs
index ca685a5f..02da0f30 100644
--- a/plugins/warp/src/container/disk.rs
+++ b/plugins/warp/src/container/disk.rs
@@ -1,5 +1,7 @@
-use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath};
-use std::collections::HashMap;
+use crate::container::{
+ Container, ContainerError, ContainerResult, SourceId, SourcePath, SourceTag,
+};
+use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
@@ -20,11 +22,12 @@ pub const NAMESPACE_DISK_SOURCE: Uuid = uuid!("ea89e8ab-a27a-432b-8fbd-77b026cd5
pub struct DiskContainer {
pub name: String,
pub sources: HashMap<SourceId, DiskContainerSource>,
+ pub writable: bool,
}
impl DiskContainer {
pub fn new(name: String, sources: HashMap<SourceId, DiskContainerSource>) -> Self {
- Self { name, sources }
+ Self { name, sources, writable: true }
}
pub fn new_from_dir(dir_path: PathBuf) -> Self {
@@ -56,6 +59,22 @@ impl DiskContainer {
Self::new(name, sources)
}
+
+ pub fn insert_source(&mut self, id: SourceId, path: SourcePath) -> ContainerResult<()> {
+ if !self.writable || self.sources.contains_key(&id) {
+ return Err(ContainerError::SourceAlreadyExists(path));
+ }
+ // NOTE: We let anyone add a file from anywhere on the file system because of this.
+ let disk_source = match path.0.exists() {
+ true => DiskContainerSource::new_from_path(path.clone())?,
+ false => {
+ let file = WarpFile::new(WarpFileHeader::new(), vec![]);
+ DiskContainerSource::new(path, file)
+ }
+ };
+ self.sources.insert(id, disk_source);
+ Ok(())
+ }
}
impl Container for DiskContainer {
@@ -66,23 +85,8 @@ impl Container for DiskContainer {
fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId> {
// Disk sources have there source id computed from the path.
let source_id = path.to_source_id();
- if self.sources.contains_key(&source_id) {
- return Err(ContainerError::SourceAlreadyExists(path));
- }
- // NOTE: We let anyone add a file from anywhere on the file system because of this.
- match path.0.exists() {
- true => {
- let disk_source = DiskContainerSource::new_from_path(path.clone())?;
- self.sources.insert(source_id, disk_source);
- Ok(source_id)
- }
- false => {
- let file = WarpFile::new(WarpFileHeader::new(), vec![]);
- let disk_source = DiskContainerSource::new(path, file);
- self.sources.insert(source_id, disk_source);
- Ok(source_id)
- }
- }
+ self.insert_source(source_id, path)?;
+ Ok(source_id)
}
fn commit_source(&mut self, source: &SourceId) -> ContainerResult<bool> {
@@ -99,8 +103,7 @@ impl Container for DiskContainer {
.sources
.get(source)
.ok_or(ContainerError::SourceNotFound(*source))?;
- // TODO: I think this should be up to the container. (cant write to bundled files)
- Ok(true)
+ Ok(self.writable)
}
fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool> {
@@ -111,6 +114,14 @@ impl Container for DiskContainer {
Ok(disk_source.uncommitted)
}
+ fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>> {
+ let disk_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ Ok(disk_source.tags.clone())
+ }
+
fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> {
let disk_source = self
.sources
@@ -277,6 +288,7 @@ impl Debug for DiskContainer {
pub struct DiskContainerSource {
pub path: SourcePath,
+ pub tags: HashSet<SourceTag>,
file: WarpFile<'static>,
uncommitted: bool,
}
@@ -285,6 +297,7 @@ impl DiskContainerSource {
pub fn new(path: SourcePath, file: WarpFile<'static>) -> Self {
Self {
path,
+ tags: HashSet::new(),
file,
uncommitted: false,
}
diff --git a/plugins/warp/src/container/memory.rs b/plugins/warp/src/container/memory.rs
index cf53390e..51628d6c 100644
--- a/plugins/warp/src/container/memory.rs
+++ b/plugins/warp/src/container/memory.rs
@@ -1,5 +1,7 @@
-use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath};
-use std::collections::HashMap;
+use crate::container::{
+ Container, ContainerError, ContainerResult, SourceId, SourcePath, SourceTag,
+};
+use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use warp::r#type::guid::TypeGUID;
use warp::r#type::{ComputedType, Type};
@@ -73,6 +75,15 @@ impl Container for MemoryContainer {
Ok(false)
}
+ fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>> {
+ let _memory_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ // NOTE: Memory containers do not have a notion of tags.
+ Ok(HashSet::default())
+ }
+
fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> {
Err(ContainerError::SourcePathUnavailable(*source))
}
diff --git a/plugins/warp/src/container/network.rs b/plugins/warp/src/container/network.rs
index 2a2d7c65..11f4db6b 100644
--- a/plugins/warp/src/container/network.rs
+++ b/plugins/warp/src/container/network.rs
@@ -1,7 +1,12 @@
use crate::container::disk::DiskContainer;
-use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath};
-use std::collections::HashMap;
+use crate::container::{
+ Container, ContainerError, ContainerResult, ContainerSearchQuery, ContainerSearchResponse,
+ SourceId, SourcePath, SourceTag,
+};
+use directories::ProjectDirs;
+use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display, Formatter};
+use std::path::PathBuf;
use warp::chunk::{Chunk, ChunkKind, CompressionType};
use warp::r#type::guid::TypeGUID;
use warp::r#type::{ComputedType, Type};
@@ -12,6 +17,7 @@ use warp::{WarpFile, WarpFileHeader};
pub mod client;
+use crate::container::ContainerError::CannotCreateSource;
pub use client::NetworkClient;
/// This is the id on the server for the [`Target`], we can get it via [`NetworkClient::query_target_id`].
@@ -22,23 +28,42 @@ pub struct NetworkContainer {
/// This is the store that the interface will write to; then we have special functions for pulling
/// and pushing to the network source.
cache: DiskContainer,
+ /// Where to place newly created sources.
+ ///
+ /// This is typically a directory inside [`NetworkContainer::root_cache_location`].
+ cache_path: PathBuf,
/// Populated when targets are queried.
known_targets: HashMap<Target, Option<NetworkTargetId>>,
- /// Populated with function sources are queried.
+ /// Populated when function sources are queried.
known_function_sources: HashMap<FunctionGUID, Vec<SourceId>>,
/// Populated when user adds function, this is used for writing back to the server.
added_chunks: HashMap<SourceId, Vec<Chunk<'static>>>,
+ /// Populated when connecting to the server, this is used to determine which sources are writable.
+ ///
+ /// NOTE: This is only populated when logged in, as guest users do not have write permissions.
+ writable_sources: HashSet<SourceId>,
}
impl NetworkContainer {
- pub fn new(client: NetworkClient) -> Self {
- Self {
- cache: DiskContainer::new("Network Container".to_string(), HashMap::new()),
+ pub fn new(client: NetworkClient, cache_path: PathBuf, writable_sources: &[SourceId]) -> Self {
+ let mut container = Self {
+ cache: DiskContainer::new_from_dir(cache_path.clone()),
+ cache_path,
client,
known_targets: HashMap::new(),
known_function_sources: HashMap::new(),
added_chunks: HashMap::new(),
+ writable_sources: writable_sources.into_iter().copied().collect(),
+ };
+
+ // TODO: Because of this little hack, methinks we should move writable sources to after the
+ // TODO: container is actually created, but before it is moved into the global container cache.
+ // Probe all writable sources, so the container knows about them properly.
+ for source in writable_sources {
+ container.probe_source(*source);
}
+
+ container
}
/// Gets the network id for the `target`, this will be used in later function queries.
@@ -72,6 +97,7 @@ impl NetworkContainer {
pub fn get_unseen_functions_source(
&mut self,
target: Option<&Target>,
+ tags: &[SourceTag],
guids: &[FunctionGUID],
) -> HashMap<SourceId, Vec<FunctionGUID>> {
let Some(target_id) = target.and_then(|t| self.get_target_id(t)) else {
@@ -88,9 +114,9 @@ impl NetworkContainer {
let mut result: HashMap<SourceId, Vec<FunctionGUID>> = HashMap::new();
// Only query server for unknown guids if we have any.
if !unknown.is_empty() {
- if let Some(queried_results) = self
- .client
- .query_functions_source(Some(target_id), &unknown)
+ if let Some(queried_results) =
+ self.client
+ .query_functions_source(Some(target_id), tags, &unknown)
{
// Cache the new results, this means we will not try and contact the server for that guids source.
// NOTE: Here we do not just simply list the queried results because we also
@@ -139,6 +165,8 @@ impl NetworkContainer {
match &chunk.kind {
ChunkKind::Signature(sc) => {
let functions: Vec<_> = sc.functions().collect();
+ // Probe the source before attempting to access it, as it might not exist locally.
+ self.probe_source(*source);
match self.cache.add_functions(target, source, &functions) {
Ok(_) => log::debug!(
"Added {} functions into cached source '{}'",
@@ -164,7 +192,46 @@ impl NetworkContainer {
///
/// **This is blocking**
pub fn push_file(&mut self, source_id: SourceId, file: &WarpFile) {
- self.client.push_file(source_id, file);
+ // TODO: We need a better name for the commit. I would like to derive it automatically from
+ // TODO: something instead of having the user give it TBH.
+ self.client.push_file(source_id, file, "commit");
+ }
+
+ /// Probe the source to make sure it exists in the cache. Retrieving the name from the server.
+ ///
+ /// **This is blocking**
+ pub fn probe_source(&mut self, source_id: SourceId) {
+ if !self.cache.source_path(&source_id).is_ok() {
+ // Add the source to the cache. Using the source id and source name as the source path.
+ match self.client.source_name(source_id) {
+ Ok(source_name) => {
+ // To prevent two sources with the same name colliding, we add the source id to the source name.
+ let source_path = self
+ .cache_path
+ .join(source_id.to_string())
+ .join(source_name);
+ let _ = self.cache.insert_source(source_id, SourcePath(source_path));
+ }
+ Err(e) => {
+ log::error!("Failed to probe source '{}': {}", source_id, e);
+ }
+ }
+ }
+ }
+
+ pub fn root_cache_location() -> PathBuf {
+ // - Windows: %LOCALAPPDATA%\<org>\<app>\cache
+ // - macOS: ~/Library/Caches/<org>.<app>
+ // - Linux: $XDG_CACHE_HOME/<app> or ~/.cache/<app>
+ if let Some(proj_dirs) = ProjectDirs::from("", "Vector35", "Binary Ninja") {
+ proj_dirs.cache_dir().to_path_buf()
+ } else {
+ // Fallback if OS dirs cannot be determined
+ std::env::current_dir()
+ .unwrap_or_else(|_| PathBuf::from("."))
+ .join(".cache")
+ .join("binaryninja")
+ }
}
}
@@ -174,9 +241,16 @@ impl Container for NetworkContainer {
}
fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId> {
- // TODO: How do we want to let users create new sources?
- log::error!("NetworkContainer::add_source not allowed");
- Err(ContainerError::CannotCreateSource(path))
+ // Send a **blocking** request to the server to create the source.
+ // NOTE: The user must be logged in for this to work.
+ // TODO: Some better error handling to alert the user that they are not logged in / creating existing sources.
+ let source = self
+ .client
+ .create_source(&path.to_string())
+ .map_err(|_| CannotCreateSource(path))?;
+ // Must probe the source before attempting to access it, as it does not exist locally.
+ self.probe_source(source);
+ Ok(source)
}
fn commit_source(&mut self, source: &SourceId) -> ContainerResult<bool> {
@@ -184,21 +258,26 @@ impl Container for NetworkContainer {
.added_chunks
.remove(source)
.ok_or(ContainerError::SourceNotFound(source.clone()))?;
- let file = WarpFile::new(WarpFileHeader::new(), chunks);
+ // Because each add operation is its own chunk, we should merge them into larger chunks before sending.
+ let merged_chunks = Chunk::merge(&chunks, CompressionType::Zstd);
+ let file = WarpFile::new(WarpFileHeader::new(), merged_chunks);
self.push_file(*source, &file);
Ok(true)
}
fn is_source_writable(&self, source: &SourceId) -> ContainerResult<bool> {
- // TODO: This is retrievable from /users/me/sources we will grab it when connecting.
- log::error!("NetworkContainer::is_source_writable not allowed");
- Err(ContainerError::SourceNotWritable(source.clone()))
+ // Assume that all writable_sources are also in the cache (through `probe_source`).
+ Ok(self.writable_sources.contains(source))
}
fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool> {
Ok(self.added_chunks.contains_key(source))
}
+ fn source_tags(&self, source: &SourceId) -> ContainerResult<HashSet<SourceTag>> {
+ self.cache.source_tags(source)
+ }
+
fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> {
self.cache.source_path(source)
}
@@ -246,10 +325,12 @@ impl Container for NetworkContainer {
fn fetch_functions(
&mut self,
target: &Target,
+ tags: &[SourceTag],
functions: &[FunctionGUID],
) -> ContainerResult<()> {
// NOTE: Blocking request to get the mapped function sources.
- let mapped_unseen_functions = self.get_unseen_functions_source(Some(&target), functions);
+ let mapped_unseen_functions =
+ self.get_unseen_functions_source(Some(&target), tags, functions);
// Actually get the function data for the unseen guids, we really only want to do this once per
// session, anymore, and this is annoying!
@@ -308,16 +389,28 @@ impl Container for NetworkContainer {
) -> ContainerResult<Vec<Function>> {
self.cache.functions_with_guid(target, source, guid)
}
+
+ fn search(&self, query: &ContainerSearchQuery) -> ContainerResult<ContainerSearchResponse> {
+ // TODO: Give this an actual network error.
+ self.client
+ .search(query)
+ .ok_or(ContainerError::CorruptedData(
+ "search query failed to validate",
+ ))
+ }
}
impl Debug for NetworkContainer {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("NetworkContainer").finish()
+ f.debug_struct("NetworkContainer")
+ .field("client", &self.client)
+ .field("cache_path", &self.cache_path)
+ .finish()
}
}
impl Display for NetworkContainer {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("NetworkContainer").finish()
+ Display::fmt(&self.client.server_url, f)
}
}
diff --git a/plugins/warp/src/container/network/client.rs b/plugins/warp/src/container/network/client.rs
index f77f1118..39e7640a 100644
--- a/plugins/warp/src/container/network/client.rs
+++ b/plugins/warp/src/container/network/client.rs
@@ -1,12 +1,20 @@
use crate::container::network::NetworkTargetId;
-use crate::container::SourceId;
+use crate::container::{
+ ContainerSearchItem, ContainerSearchItemKind, ContainerSearchQuery, ContainerSearchResponse,
+ SourceId, SourcePath, SourceTag,
+};
use reqwest::blocking::Client;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use reqwest::StatusCode;
+use serde::Deserialize;
use serde_json::json;
use std::collections::HashMap;
use std::str::FromStr;
-use warp::signature::function::FunctionGUID;
+use uuid::Uuid;
+use warp::chunk::ChunkKind;
+use warp::r#type::guid::TypeGUID;
+use warp::r#type::{ComputedType, Type};
+use warp::signature::function::{Function, FunctionGUID};
use warp::target::Target;
use warp::WarpFile;
@@ -16,7 +24,7 @@ use warp::WarpFile;
#[derive(Clone, Debug)]
pub struct NetworkClient {
client: Client,
- server_url: String,
+ pub server_url: String,
}
impl NetworkClient {
@@ -65,6 +73,116 @@ impl NetworkClient {
Ok(resp.status())
}
+ /// Query the logged in user.
+ ///
+ /// NOTE: **THIS IS BLOCKING**
+ ///
+ /// Route: `api/v1/users/me` (TODO: Comment about the query)
+ pub fn current_user(&self) -> reqwest::Result<(i32, String)> {
+ let current_user_url = format!("{}/api/v1/users/me", self.server_url);
+
+ #[derive(Deserialize)]
+ struct CurrentUser {
+ username: String,
+ id: i32,
+ }
+
+ let resp = self
+ .client
+ .get(&current_user_url)
+ .send()?
+ .error_for_status()?;
+ let user: CurrentUser = resp.json()?;
+ Ok((user.id, user.username))
+ }
+
+ /// Query the logged in user.
+ ///
+ /// NOTE: **THIS IS BLOCKING**
+ ///
+ /// Route: `api/v1/users/me` (TODO: Comment about the query)
+ pub fn source_name(&self, id: SourceId) -> reqwest::Result<String> {
+ let source_url = format!("{}/api/v1/sources/{}", self.server_url, id);
+
+ #[derive(Deserialize)]
+ struct Source {
+ name: String,
+ }
+
+ let resp = self.client.get(&source_url).send()?.error_for_status()?;
+ let src: Source = resp.json()?;
+ Ok(src.name)
+ }
+
+ /// Create a new source with the given name.
+ ///
+ /// The current user will be added to the source.
+ ///
+ /// NOTE: You must be logged in to create a source.
+ ///
+ /// NOTE: **THIS IS BLOCKING**
+ ///
+ /// Route: `api/v1/sources/`
+ pub fn create_source(&self, name: &str) -> reqwest::Result<SourceId> {
+ let source_url = format!("{}/api/v1/sources", self.server_url);
+
+ let body = json!({
+ "name": name,
+ // Passing nothing here will add the current user to the source.
+ "user_ids": []
+ });
+
+ #[derive(Deserialize)]
+ struct CreateSourceResponse {
+ id: Uuid,
+ }
+
+ let resp = self
+ .client
+ .post(&source_url)
+ .json(&body)
+ .send()?
+ .error_for_status()?;
+
+ let parsed: CreateSourceResponse = resp.json()?;
+ Ok(SourceId(parsed.id))
+ }
+
+ /// Query the [`SourceId`]s for the given user.
+ ///
+ /// NOTE: **THIS IS BLOCKING**
+ ///
+ /// Route: `api/v1/sources/query` (TODO: Comment about the query)
+ pub fn query_sources(&self, user_id: Option<i32>) -> reqwest::Result<Vec<SourceId>> {
+ let sources_url = format!("{}/api/v1/sources/query", self.server_url);
+
+ #[derive(Deserialize)]
+ struct SourceItem {
+ id: Uuid,
+ }
+
+ #[derive(Deserialize)]
+ struct SourcesQueryResponse {
+ items: Vec<SourceItem>,
+ }
+
+ let mut query = HashMap::new();
+ if let Some(user_id) = user_id {
+ query.insert("user_id", user_id);
+ }
+ let query_str = json!(query).to_string();
+ let resp = self
+ .client
+ .post(&sources_url)
+ .body(query_str)
+ .header("Content-Type", "application/json")
+ .send()?
+ .error_for_status()?;
+
+ let parsed: SourcesQueryResponse = resp.json()?;
+ Ok(parsed.items.into_iter().map(|it| SourceId(it.id)).collect())
+ }
+
/// Query the [`NetworkTargetId`] for the given [`Target`].
///
/// NOTE: **THIS IS BLOCKING**
@@ -78,25 +196,36 @@ impl NetworkClient {
query.insert("platform", platform);
}
if let Some(architecture) = &target.architecture {
- query.insert("architecture", architecture);
+ query.insert("arch", architecture);
+ }
+ let query_str = json!(query).to_string();
+
+ #[derive(Deserialize)]
+ struct TargetQueryResponse {
+ id: NetworkTargetId,
}
// NOTE: This is blocking.
- let target_id: NetworkTargetId = self
+ let response = self
.client
- .get(query_target_url)
- .query(&query)
+ .post(query_target_url)
+ .body(query_str)
+ .header("Content-Type", "application/json")
.send()
- .ok()?
- .json::<NetworkTargetId>()
.ok()?;
- Some(target_id)
+ // Assuming the first response is the one we want.
+ // TODO: Handle multiple responses, or error out.
+ let json_response: Vec<TargetQueryResponse> = response.json().ok()?;
+ let first_response = json_response.first()?;
+
+ Some(first_response.id)
}
fn query_functions_body(
target: Option<NetworkTargetId>,
source: Option<SourceId>,
+ source_tags: &[SourceTag],
guids: &[FunctionGUID],
) -> serde_json::Value {
let guids_str: Vec<String> = guids.iter().map(|g| g.to_string()).collect();
@@ -112,6 +241,9 @@ impl NetworkClient {
if let Some(source_id) = source {
body["source_id"] = json!(source_id.to_string());
}
+ if !source_tags.is_empty() {
+ body["source_tags"] = json!(source_tags);
+ }
body
}
@@ -127,7 +259,9 @@ impl NetworkClient {
guids: &[FunctionGUID],
) -> Option<WarpFile<'static>> {
let query_functions_url = format!("{}/api/v1/functions/query", self.server_url);
- let payload = Self::query_functions_body(target, source, guids);
+ // TODO: Allow for source tags? We really only need this in query_functions_source as that
+ // TODO: is what prevents a undesired source from being "known" to the container.
+ let payload = Self::query_functions_body(target, source, &[], guids);
// Make the POST request
let response = self
@@ -154,11 +288,12 @@ impl NetworkClient {
pub fn query_functions_source(
&self,
target: Option<NetworkTargetId>,
+ tags: &[SourceTag],
guids: &[FunctionGUID],
) -> Option<HashMap<SourceId, Vec<FunctionGUID>>> {
let query_functions_source_url =
format!("{}/api/v1/functions/query/source", self.server_url);
- let payload = Self::query_functions_body(target, None, guids);
+ let payload = Self::query_functions_body(target, None, tags, guids);
// Make the POST request
let response = self
@@ -194,20 +329,24 @@ impl NetworkClient {
/// NOTE: **THIS IS BLOCKING**
///
/// Route: `api/v1/files/{source}`
- pub fn push_file(&self, source_id: SourceId, file: &WarpFile) -> bool {
- let push_file_url = format!("{}/api/v1/files/{}", self.server_url, source_id.to_string());
+ pub fn push_file(&self, source_id: SourceId, file: &WarpFile, name: &str) -> bool {
+ let push_file_url = format!("{}/api/v1/files", self.server_url);
// Convert WarpFile to bytes
let file_bytes = file.to_bytes();
- // Create the form part with the file
- let form = reqwest::blocking::multipart::Form::new().part(
- "file",
- reqwest::blocking::multipart::Part::bytes(file_bytes)
- .file_name("data.warp")
- .mime_str("application/octet-stream")
- .unwrap(),
- );
+ let Ok(file_part) = reqwest::blocking::multipart::Part::bytes(file_bytes)
+ .file_name("data.warp")
+ .mime_str("application/octet-stream")
+ else {
+ log::error!("Failed to create file part");
+ return false;
+ };
+
+ let form = reqwest::blocking::multipart::Form::new()
+ .part("file", file_part)
+ .text("name", name.to_string())
+ .text("source", source_id.to_string());
// Send the request
match self.client.post(&push_file_url).multipart(form).send() {
@@ -225,4 +364,226 @@ impl NetworkClient {
}
}
}
+
+ pub fn function_data(&self, id: i32) -> Option<Function> {
+ let function_data_url = format!("{}/api/v1/functions/{}/data", self.server_url, id);
+ let response = self.client.get(&function_data_url).send().ok()?;
+ if !response.status().is_success() {
+ log::error!(
+ "Failed to fetch function data for {}: {}",
+ id,
+ response.status()
+ );
+ return None;
+ }
+ let bytes = response.bytes().ok()?;
+ Function::from_bytes(bytes.as_ref())
+ }
+
+ pub fn function_datas(&self, ids: &[i32]) -> Option<Vec<Function>> {
+ if ids.is_empty() {
+ return Some(Vec::new());
+ }
+ let function_data_url = format!("{}/api/v1/functions/data", self.server_url);
+ let body = json!({
+ "ids": ids,
+ });
+ let response = self
+ .client
+ .post(&function_data_url)
+ .json(&body)
+ .send()
+ .ok()?;
+ if !response.status().is_success() {
+ log::error!("Failed to fetch function data: {}", response.status());
+ return None;
+ }
+ let bytes = response.bytes().ok()?;
+ let file = WarpFile::from_bytes(bytes.as_ref())?;
+ let mut functions = Vec::with_capacity(ids.len());
+ for chunk in file.chunks {
+ let ChunkKind::Signature(sc) = chunk.kind else {
+ continue;
+ };
+ functions.extend(sc.functions());
+ }
+ Some(functions)
+ }
+
+ pub fn type_data(&self, guid: TypeGUID) -> Option<Type> {
+ let type_data_url = format!("{}/api/v1/types/{}/data", self.server_url, guid.to_string());
+ let response = self.client.get(&type_data_url).send().ok()?;
+ if !response.status().is_success() {
+ log::error!(
+ "Failed to fetch type data for {}: {}",
+ guid.to_string(),
+ response.status()
+ );
+ return None;
+ }
+ let bytes = response.bytes().ok()?;
+ Type::from_bytes(bytes.as_ref())
+ }
+
+ pub fn type_datas(&self, guids: &[TypeGUID]) -> Option<Vec<ComputedType>> {
+ if guids.is_empty() {
+ return Some(Vec::new());
+ }
+ let type_data_url = format!("{}/api/v1/types/data", self.server_url);
+ let body = json!({
+ "ids": guids.iter().map(|g| g.to_string()).collect::<Vec<_>>(),
+ });
+ let response = self.client.post(&type_data_url).json(&body).send().ok()?;
+ if !response.status().is_success() {
+ log::error!("Failed to fetch type data: {}", response.status());
+ return None;
+ }
+ let bytes = response.bytes().ok()?;
+ let file = WarpFile::from_bytes(bytes.as_ref())?;
+ let mut types = Vec::with_capacity(guids.len());
+ for chunk in file.chunks {
+ let ChunkKind::Type(tc) = chunk.kind else {
+ continue;
+ };
+ types.extend(tc.types());
+ }
+ Some(types)
+ }
+
+ pub fn search(&self, query: &ContainerSearchQuery) -> Option<ContainerSearchResponse> {
+ let search_url = format!("{}/api/v1/search", self.server_url);
+
+ #[derive(serde::Serialize)]
+ struct SearchRequest<'a> {
+ #[serde(rename = "q")]
+ q: &'a str,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ limit: Option<usize>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ offset: Option<usize>,
+ #[serde(rename = "source_id", skip_serializing_if = "Option::is_none")]
+ source_id: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ source_tags: Option<Vec<SourceTag>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ retrieve_data: Option<bool>,
+ }
+
+ #[derive(serde::Deserialize)]
+ struct SearchResponse {
+ items: Vec<SearchItem>,
+ offset: usize,
+ total: usize,
+ }
+
+ #[derive(serde::Deserialize)]
+ struct SearchItem {
+ id: String,
+ kind: String,
+ #[serde(default)]
+ name: Option<String>,
+ #[serde(default)]
+ source_id: Option<Uuid>,
+ #[serde(default)]
+ data: Option<Vec<u8>>,
+ }
+
+ let source_id_str = query.source.map(|s| s.to_string());
+ let request = SearchRequest {
+ q: &query.query,
+ limit: query.limit,
+ offset: query.offset,
+ source_id: source_id_str,
+ source_tags: match query.tags.is_empty() {
+ true => None,
+ false => Some(query.tags.clone()),
+ },
+ // This must be passed to retrieve the function and type data.
+ retrieve_data: Some(true),
+ };
+
+ let resp = match self.client.get(search_url).query(&request).send() {
+ Ok(r) => r,
+ Err(err) => {
+ log::error!("Failed to send search request: {}", err);
+ return None;
+ }
+ };
+
+ let Ok(parsed) = resp.json::<SearchResponse>() else {
+ log::error!("Failed to parse search response");
+ return None;
+ };
+
+ // TODO: This is quite scuffed, but it works for now. (Mostly just that it looks bad and queries a lot)
+ // TODO: Here I think would be a good place to sort it so sources always come first.
+ // TODO: Users searching will want to get to the source first, likely to whitelist or blacklist.
+ let mut items = Vec::with_capacity(parsed.items.len());
+ for item in parsed.items {
+ let Some(source_uuid) = item.source_id else {
+ // Currently not interested in items without a source id.
+ // Things like symbols do not have a source id.
+ continue;
+ };
+
+ let kind = match item.kind.as_str() {
+ "function" => {
+ let Some(data) = &item.data else {
+ log::warn!(
+ "Function item {} has no data from network, skipping...",
+ item.id
+ );
+ continue;
+ };
+ let Some(func) = Function::from_bytes(&data) else {
+ log::warn!(
+ "Function item {} has invalid data from network, skipping...",
+ item.id
+ );
+ continue;
+ };
+ ContainerSearchItemKind::Function(func)
+ }
+ "source" => ContainerSearchItemKind::Source {
+ path: match item.name {
+ None => {
+ log::warn!("Source item {} has no name", item.id);
+ continue;
+ }
+ Some(name) => SourcePath(format!("{}/{}", self.server_url, name).into()),
+ },
+ id: SourceId(source_uuid),
+ },
+ "type" => {
+ let Some(data) = &item.data else {
+ log::warn!(
+ "Type item {} has no data from network, skipping...",
+ item.id
+ );
+ continue;
+ };
+ let Some(ty) = Type::from_bytes(&data) else {
+ log::warn!(
+ "Type item {} has invalid data from network, skipping...",
+ item.id
+ );
+ continue;
+ };
+ ContainerSearchItemKind::Type(ty)
+ }
+ _ => continue,
+ };
+
+ items.push(ContainerSearchItem {
+ source: SourceId(source_uuid),
+ kind,
+ });
+ }
+
+ Some(ContainerSearchResponse {
+ items,
+ total: parsed.total,
+ offset: parsed.offset,
+ })
+ }
}
diff --git a/plugins/warp/src/convert/types.rs b/plugins/warp/src/convert/types.rs
index 1dfe5fa9..2401a039 100644
--- a/plugins/warp/src/convert/types.rs
+++ b/plugins/warp/src/convert/types.rs
@@ -279,9 +279,10 @@ pub fn to_bn_calling_convention<A: BNArchitecture>(
arch.get_default_calling_convention().unwrap()
}
-pub fn to_bn_type<A: BNArchitecture>(arch: &A, ty: &Type) -> BNRef<BNType> {
+// Always pass the architecture unless you know what you're doing!
+pub fn to_bn_type<A: BNArchitecture + Copy>(arch: Option<A>, ty: &Type) -> BNRef<BNType> {
let bits_to_bytes = |val: u64| (val / 8);
- let addr_size = arch.address_size() as u64;
+ let addr_size = arch.map(|a| a.address_size()).unwrap_or(8) as u64;
match &ty.class {
TypeClass::Void => BNType::void(),
TypeClass::Boolean(_) => BNType::bool(),
@@ -438,8 +439,8 @@ pub fn to_bn_type<A: BNArchitecture>(arch: &A, ty: &Type) -> BNRef<BNType> {
// TODO: Variable arguments
let variable_args = false;
// If we have a calling convention we run the extended function type creation.
- match c.calling_convention.as_ref() {
- Some(cc) => {
+ match (c.calling_convention.as_ref(), arch.as_ref()) {
+ (Some(cc), Some(arch)) => {
let calling_convention = to_bn_calling_convention(arch, cc);
BNType::function_with_opts(
&return_type,
@@ -449,7 +450,7 @@ pub fn to_bn_type<A: BNArchitecture>(arch: &A, ty: &Type) -> BNRef<BNType> {
BNConf::new(0, 0),
)
}
- None => BNType::function(&return_type, params, variable_args),
+ (_, _) => BNType::function(&return_type, params, variable_args),
}
}
TypeClass::Referrer(c) => {
diff --git a/plugins/warp/src/matcher.rs b/plugins/warp/src/matcher.rs
index 52a5b04f..d97b4a18 100644
--- a/plugins/warp/src/matcher.rs
+++ b/plugins/warp/src/matcher.rs
@@ -100,21 +100,21 @@ impl Matcher {
// TODO: I would really like for WARP types to be added in a seperate type container, so that we don't
// TODO: just add them as system or user types.
- pub fn add_type_to_view<A: BNArchitecture>(
+ pub fn add_type_to_view<A: BNArchitecture + Copy>(
&self,
container: &dyn Container,
source: &SourceId,
view: &BinaryView,
- arch: &A,
+ arch: A,
ty: &Type,
) where
Self: Sized,
{
- fn inner_add_type_to_view<A: BNArchitecture>(
+ fn inner_add_type_to_view<A: BNArchitecture + Copy>(
container: &dyn Container,
source: &SourceId,
view: &BinaryView,
- arch: &A,
+ arch: A,
visited_refs: &mut HashSet<String>,
ty: &Type,
) {
@@ -251,7 +251,7 @@ impl Matcher {
view.define_auto_type_with_id(
name,
&guid.to_string(),
- &to_bn_type(arch, &ref_ty),
+ &to_bn_type(Some(arch), &ref_ty),
);
}
(Some(_guid), Some(_name), None) => {
diff --git a/plugins/warp/src/plugin.rs b/plugins/warp/src/plugin.rs
index 7a2bdcff..229b0ae0 100644
--- a/plugins/warp/src/plugin.rs
+++ b/plugins/warp/src/plugin.rs
@@ -14,10 +14,11 @@ use binaryninja::command::{
};
use binaryninja::is_ui_enabled;
use binaryninja::logger::Logger;
-use binaryninja::settings::Settings;
+use binaryninja::settings::{QueryOptions, Settings};
use log::LevelFilter;
use reqwest::StatusCode;
+mod commit;
mod create;
mod debug;
mod ffi;
@@ -31,17 +32,21 @@ mod workflow;
fn load_bundled_signatures() {
let global_bn_settings = Settings::new();
- let plugin_settings = PluginSettings::from_settings(&global_bn_settings);
+ let plugin_settings =
+ PluginSettings::from_settings(&global_bn_settings, &mut QueryOptions::new());
// We want to load all the bundled directories into the container cache.
let background_task = BackgroundTask::new("Loading WARP files...", false);
let start = Instant::now();
if plugin_settings.load_bundled_files {
- let core_disk_container = DiskContainer::new_from_dir(core_signature_dir());
+ let mut core_disk_container = DiskContainer::new_from_dir(core_signature_dir());
+ core_disk_container.name = "Bundled".to_string();
+ core_disk_container.writable = false;
log::debug!("{:#?}", core_disk_container);
add_cached_container(core_disk_container);
}
if plugin_settings.load_user_files {
- let user_disk_container = DiskContainer::new_from_dir(user_signature_dir());
+ let mut user_disk_container = DiskContainer::new_from_dir(user_signature_dir());
+ user_disk_container.name = "User".to_string();
log::debug!("{:#?}", user_disk_container);
add_cached_container(user_disk_container);
}
@@ -51,36 +56,77 @@ fn load_bundled_signatures() {
fn load_network_container() {
let global_bn_settings = Settings::new();
- let plugin_settings = PluginSettings::from_settings(&global_bn_settings);
- let background_task = BackgroundTask::new("Initializing WARP server...", false);
- let start = Instant::now();
- if plugin_settings.enable_server {
- let server_url = plugin_settings.server_url.clone();
- let server_api_key = plugin_settings.server_api_key.clone();
+
+ let add_network_container = |url: String, api_key: Option<String>| {
let https_proxy_str = global_bn_settings.get_string("network.httpsProxy");
let https_proxy = if https_proxy_str.is_empty() {
None
} else {
Some(https_proxy_str)
};
- match NetworkClient::new(server_url.clone(), server_api_key, https_proxy) {
+ match NetworkClient::new(url.clone(), api_key.clone(), https_proxy) {
Ok(network_client) => {
// Before constructing the container, let's make sure that the server is OK.
if let Ok(StatusCode::OK) = network_client.status() {
- let network_container = NetworkContainer::new(network_client);
+ // Check if the user is logged in. If so, we should collect the writable sources.
+ let mut writable_sources = Vec::new();
+ match network_client.current_user() {
+ Ok((id, username)) => {
+ log::info!(
+ "Server '{}' connected, logged in as user '{}'",
+ url,
+ username
+ );
+ match network_client.query_sources(Some(id)) {
+ Ok(sources) => {
+ writable_sources = sources;
+ }
+ Err(e) => {
+ log::error!(
+ "Server '{}' failed to get sources for user: {}",
+ url,
+ e
+ );
+ }
+ }
+ }
+ Err(e) if api_key.is_some() => {
+ log::error!(
+ "Server '{}' failed to authenticate with provided API key: {}",
+ url,
+ e
+ );
+ }
+ Err(_) => {
+ log::info!("Server '{}' connected, logged in as guest", url);
+ }
+ }
+
+ // TODO: Make the cache path include the domain or url, so that we can have multiple servers.
+ let main_cache_path = NetworkContainer::root_cache_location().join("main");
+ let network_container =
+ NetworkContainer::new(network_client, main_cache_path, &writable_sources);
log::debug!("{:#?}", network_container);
add_cached_container(network_container);
} else {
- log::error!(
- "Server '{}' is not reachable, disabling container...",
- server_url
- );
+ log::error!("Server '{}' is not reachable, disabling container...", url);
}
}
Err(e) => {
log::error!("Failed to add networked container: {}", e);
}
}
+ };
+
+ let plugin_settings =
+ PluginSettings::from_settings(&global_bn_settings, &mut QueryOptions::new());
+ let background_task = BackgroundTask::new("Initializing WARP server...", false);
+ let start = Instant::now();
+ if plugin_settings.enable_server {
+ add_network_container(plugin_settings.server_url, plugin_settings.server_api_key);
+ if let Some(second_server_url) = plugin_settings.second_server_url {
+ add_network_container(second_server_url, plugin_settings.second_server_api_key);
+ }
}
log::debug!("Initializing warp server took {:?}", start.elapsed());
background_task.finish();
@@ -156,6 +202,12 @@ pub extern "C" fn CorePluginInit() -> bool {
load::LoadSignatureFile {},
);
+ register_command(
+ "WARP\\Commit File",
+ "Commit file to a source",
+ commit::CommitFile {},
+ );
+
register_command_for_function(
"WARP\\Include Function",
"Add current function to the list of functions to add to the signature file",
diff --git a/plugins/warp/src/plugin/commit.rs b/plugins/warp/src/plugin/commit.rs
new file mode 100644
index 00000000..2af5fc3e
--- /dev/null
+++ b/plugins/warp/src/plugin/commit.rs
@@ -0,0 +1,150 @@
+//! Commit file to a source.
+
+use crate::cache::container::cached_containers;
+use crate::container::{SourceId, SourcePath};
+use crate::plugin::create::OpenFileField;
+use binaryninja::binary_view::BinaryView;
+use binaryninja::command::Command;
+use binaryninja::interaction::{Form, FormInputField};
+use warp::chunk::ChunkKind;
+use warp::WarpFile;
+
+pub struct SelectedSourceField {
+ sources: Vec<(SourceId, SourcePath)>,
+}
+
+impl SelectedSourceField {
+ pub fn field(&self) -> FormInputField {
+ FormInputField::Choice {
+ prompt: "Selected Source".to_string(),
+ choices: self
+ .sources
+ .iter()
+ .map(|(id, path)| {
+ // For display purposes we only want to show the last path item.
+ let path_name = path
+ .to_string()
+ .rsplit_once('/')
+ .map_or(path.to_string(), |(_, last_path_item)| {
+ last_path_item.to_string()
+ });
+ // TODO: Probably have a truncation limit here, this is just for display after all.
+ format!("{} ({})", path_name, id)
+ })
+ .collect(),
+ default: None,
+ value: 0,
+ }
+ }
+
+ pub fn from_form(&self, form: &Form) -> Option<SourceId> {
+ let field = form.get_field_with_name("Selected Source")?;
+ let field_value = field.try_value_index()?;
+ self.sources.get(field_value).map(|(id, _)| *id)
+ }
+}
+
+pub struct CommitFile;
+
+impl CommitFile {
+ pub fn selected_source_field() -> SelectedSourceField {
+ let mut writable_sources = Vec::new();
+ for container in cached_containers() {
+ if let Ok(container) = container.read() {
+ for source in container.sources().unwrap_or_default() {
+ if let Ok(true) = container.is_source_writable(&source) {
+ if let Ok(source_path) = container.source_path(&source) {
+ writable_sources.push((source, source_path));
+ }
+ }
+ }
+ }
+ }
+ SelectedSourceField {
+ sources: writable_sources,
+ }
+ }
+
+ pub fn execute() -> Option<()> {
+ let mut form = Form::new("Commit File");
+
+ // Users are going to get confused between this and adding functions to a source then commiting.
+ // So we should make it clear with a label, and also probably deprecate this command and replace it with "add functions to source" and "commit source".
+ form.add_field(FormInputField::Label {
+ prompt: "Commits a WARP file to an existing source, this is primarily used for committing to network containers".to_string()
+ });
+
+ form.add_field(OpenFileField::field());
+ let source_field = Self::selected_source_field();
+ form.add_field(source_field.field());
+
+ if !form.prompt() {
+ return None;
+ }
+
+ let open_file_path = OpenFileField::from_form(&form)?;
+ let source_id = source_field.from_form(&form)?;
+ log::info!("Committing file to source: {}", source_id);
+
+ let bytes = std::fs::read(open_file_path).ok()?;
+ let Some(warp_file) = WarpFile::from_bytes(&bytes) else {
+ log::error!("Failed to parse warp file!");
+ return None;
+ };
+
+ for container in cached_containers() {
+ let Ok(mut container) = container.write() else {
+ continue;
+ };
+
+ if let Ok(true) = container.is_source_writable(&source_id) {
+ // TODO: We need to find a sane way to do this procedure through the FFI.
+ for chunk in &warp_file.chunks {
+ match &chunk.kind {
+ ChunkKind::Signature(sc) => {
+ let functions: Vec<_> = sc.functions().collect();
+ log::info!(
+ "Adding {} functions to source: {}",
+ functions.len(),
+ source_id
+ );
+ if let Err(e) = container.add_functions(
+ &chunk.header.target,
+ &source_id,
+ &functions,
+ ) {
+ log::error!("Failed to add functions to source: {}", e);
+ }
+ }
+ ChunkKind::Type(sc) => {
+ let types: Vec<_> = sc.types().collect();
+ log::info!("Adding {} types to source: {}", types.len(), source_id);
+ if let Err(e) = container.add_computed_types(&source_id, &types) {
+ log::error!("Failed to add types to source: {}", e);
+ }
+ }
+ }
+ }
+ if let Err(e) = container.commit_source(&source_id) {
+ log::error!("Failed to commit source: {}", e);
+ }
+ log::info!("Committed file to source: {}", source_id);
+ return Some(());
+ }
+ }
+
+ Some(())
+ }
+}
+
+impl Command for CommitFile {
+ fn action(&self, _view: &BinaryView) {
+ std::thread::spawn(move || {
+ Self::execute();
+ });
+ }
+
+ fn valid(&self, _view: &BinaryView) -> bool {
+ true
+ }
+}
diff --git a/plugins/warp/src/plugin/ffi.rs b/plugins/warp/src/plugin/ffi.rs
index 2826ed34..c1f5acb8 100644
--- a/plugins/warp/src/plugin/ffi.rs
+++ b/plugins/warp/src/plugin/ffi.rs
@@ -1,4 +1,5 @@
mod container;
+mod file;
mod function;
use binaryninjacore_sys::{
@@ -88,6 +89,17 @@ pub unsafe extern "C" fn BNWARPUUIDGetString(uuid: *const Uuid) -> *mut c_char {
}
#[no_mangle]
+pub unsafe extern "C" fn BNWARPUUIDFromString(uuid_str: *mut c_char, uuid: *mut Uuid) -> bool {
+ if let Ok(uuid_str) = std::ffi::CStr::from_ptr(uuid_str).to_str() {
+ if let Some(parsed_uuid) = Uuid::parse_str(uuid_str).ok() {
+ *uuid = parsed_uuid;
+ return true;
+ }
+ }
+ false
+}
+
+#[no_mangle]
pub unsafe extern "C" fn BNWARPUUIDEqual(a: *const Uuid, b: *const Uuid) -> bool {
(*a) == (*b)
}
diff --git a/plugins/warp/src/plugin/ffi/container.rs b/plugins/warp/src/plugin/ffi/container.rs
index 4de6bab3..72c85366 100644
--- a/plugins/warp/src/plugin/ffi/container.rs
+++ b/plugins/warp/src/plugin/ffi/container.rs
@@ -1,5 +1,7 @@
use crate::cache::container::cached_containers;
-use crate::container::SourcePath;
+use crate::container::{
+ ContainerSearchItem, ContainerSearchItemKind, ContainerSearchQuery, SourcePath, SourceTag,
+};
use crate::convert::{from_bn_type, to_bn_type};
use crate::plugin::ffi::{
BNWARPContainer, BNWARPFunction, BNWARPFunctionGUID, BNWARPSource, BNWARPTarget, BNWARPTypeGUID,
@@ -12,7 +14,163 @@ use binaryninja::types::Type;
use binaryninjacore_sys::{BNArchitecture, BNBinaryView, BNType};
use std::ffi::{c_char, CStr};
use std::mem::ManuallyDrop;
+use std::ops::Deref;
use std::sync::Arc;
+use warp::r#type::guid::TypeGUID;
+
+pub type BNWARPContainerSearchQuery = ContainerSearchQuery;
+pub type BNWARPContainerSearchItem = ContainerSearchItem;
+
+#[repr(C)]
+pub enum BNWARPContainerSearchItemKind {
+ Source = 0,
+ Function = 1,
+ Type = 2,
+ Symbol = 3,
+}
+
+#[repr(C)]
+pub struct BNWARPContainerSearchResponse {
+ pub count: usize,
+ pub items: *mut *mut BNWARPContainerSearchItem,
+ pub offset: usize,
+ pub total: usize,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewContainerSearchQuery(
+ query: *mut c_char,
+ offset: *const usize,
+ limit: *const usize,
+ source: *const BNWARPSource,
+ source_tags: *mut *mut c_char,
+ source_tags_count: usize,
+) -> *mut BNWARPContainerSearchQuery {
+ let query_cstr = unsafe { CStr::from_ptr(query) };
+ let Ok(query) = query_cstr.to_str() else {
+ return std::ptr::null_mut();
+ };
+ let mut search_query = ContainerSearchQuery::new(query.to_string());
+ if !offset.is_null() {
+ search_query.offset = Some(*offset);
+ }
+ if !limit.is_null() {
+ search_query.limit = Some(*limit);
+ }
+ if !source.is_null() {
+ search_query.source = Some(*source);
+ }
+ if !source_tags.is_null() {
+ let source_tags_raw = unsafe { std::slice::from_raw_parts(source_tags, source_tags_count) };
+ let source_tags: Vec<SourceTag> = source_tags_raw
+ .iter()
+ .filter_map(|&ptr| CStr::from_ptr(ptr).to_str().ok())
+ .map(|s| s.into())
+ .collect();
+ search_query.tags = source_tags;
+ }
+ Box::into_raw(Box::new(search_query))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerSearchItemGetKind(
+ item: *mut BNWARPContainerSearchItem,
+) -> BNWARPContainerSearchItemKind {
+ let item = ManuallyDrop::new(Arc::from_raw(item));
+ match &item.kind {
+ ContainerSearchItemKind::Source { .. } => BNWARPContainerSearchItemKind::Source,
+ ContainerSearchItemKind::Function(_) => BNWARPContainerSearchItemKind::Function,
+ ContainerSearchItemKind::Type(_) => BNWARPContainerSearchItemKind::Type,
+ ContainerSearchItemKind::Symbol(_) => BNWARPContainerSearchItemKind::Symbol,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerSearchItemGetSource(
+ item: *mut BNWARPContainerSearchItem,
+) -> BNWARPSource {
+ let item = ManuallyDrop::new(Arc::from_raw(item));
+ item.source
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerSearchItemGetType(
+ arch: *mut BNArchitecture,
+ item: *mut BNWARPContainerSearchItem,
+) -> *mut BNType {
+ // NOTE: to convert the type, we must have an architecture.
+ let arch = match !arch.is_null() {
+ true => Some(CoreArchitecture::from_raw(arch)),
+ false => None,
+ };
+
+ let item = ManuallyDrop::new(Arc::from_raw(item));
+ match &item.kind {
+ ContainerSearchItemKind::Source { .. } => std::ptr::null_mut(),
+ ContainerSearchItemKind::Function(func) => {
+ match &func.ty {
+ None => std::ptr::null_mut(),
+ Some(ty) => {
+ let bn_ty = to_bn_type(arch, &ty);
+ // NOTE: The type ref has been pre-incremented for the caller.
+ unsafe { Ref::into_raw(bn_ty) }.handle
+ }
+ }
+ }
+ ContainerSearchItemKind::Type(ty) => {
+ let bn_ty = to_bn_type(arch, &ty);
+ // NOTE: The type ref has been pre-incremented for the caller.
+ unsafe { Ref::into_raw(bn_ty) }.handle
+ }
+ ContainerSearchItemKind::Symbol(_) => std::ptr::null_mut(),
+ }
+}
+
+// NOTE: In the future we should allow for the possibility of this returning a null pointer.
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerSearchItemGetName(
+ item: *mut BNWARPContainerSearchItem,
+) -> *mut c_char {
+ let item = ManuallyDrop::new(Arc::from_raw(item));
+ match &item.kind {
+ ContainerSearchItemKind::Source { path, .. } => {
+ let bn_name = BnString::new(path.to_string());
+ BnString::into_raw(bn_name)
+ }
+ ContainerSearchItemKind::Function(func) => {
+ let bn_name = BnString::new(func.symbol.name.clone());
+ BnString::into_raw(bn_name)
+ }
+ ContainerSearchItemKind::Type(ty) => {
+ // TODO: Maybe un-named types should return std::ptr::null_mut()?
+ let ty_name = ty
+ .name
+ .clone()
+ .unwrap_or_else(|| TypeGUID::from(ty).to_string());
+ let bn_name = BnString::new(ty_name);
+ BnString::into_raw(bn_name)
+ }
+ ContainerSearchItemKind::Symbol(sym) => {
+ let bn_name = BnString::new(sym.name.clone());
+ BnString::into_raw(bn_name)
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerSearchItemGetFunction(
+ item: *mut BNWARPContainerSearchItem,
+) -> *mut BNWARPFunction {
+ let item = ManuallyDrop::new(Arc::from_raw(item));
+ match &item.kind {
+ ContainerSearchItemKind::Source { .. } => std::ptr::null_mut(),
+ ContainerSearchItemKind::Function(func) => {
+ Arc::into_raw(Arc::new(func.clone())) as *mut BNWARPFunction
+ }
+ ContainerSearchItemKind::Type(_) => std::ptr::null_mut(),
+ ContainerSearchItemKind::Symbol(_) => std::ptr::null_mut(),
+ }
+}
#[no_mangle]
pub unsafe extern "C" fn BNWARPGetContainers(count: *mut usize) -> *mut *mut BNWARPContainer {
@@ -39,6 +197,8 @@ pub unsafe extern "C" fn BNWARPContainerGetName(container: *mut BNWARPContainer)
pub unsafe extern "C" fn BNWARPContainerFetchFunctions(
container: *mut BNWARPContainer,
target: *mut BNWARPTarget,
+ source_tags: *mut *mut c_char,
+ source_tags_count: usize,
guids: *const BNWARPFunctionGUID,
count: usize,
) {
@@ -49,9 +209,16 @@ pub unsafe extern "C" fn BNWARPContainerFetchFunctions(
let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) };
+ let source_tags_raw = unsafe { std::slice::from_raw_parts(source_tags, source_tags_count) };
+ let source_tags: Vec<SourceTag> = source_tags_raw
+ .iter()
+ .filter_map(|&ptr| CStr::from_ptr(ptr).to_str().ok())
+ .map(|s| s.into())
+ .collect();
+
let guids = unsafe { std::slice::from_raw_parts(guids, count) };
- if let Err(e) = container.fetch_functions(&target, guids) {
+ if let Err(e) = container.fetch_functions(&target, &source_tags, guids) {
log::error!("Failed to fetch functions: {}", e);
}
}
@@ -373,7 +540,7 @@ pub unsafe extern "C" fn BNWARPContainerGetTypeWithGUID(
let Some(ty) = container.type_with_guid(&source, &guid).unwrap_or_default() else {
return std::ptr::null_mut();
};
- let function_type = to_bn_type(&arch, &ty);
+ let function_type = to_bn_type(Some(arch), &ty);
// NOTE: The type ref has been pre-incremented for the caller.
unsafe { Ref::into_raw(function_type) }.handle
}
@@ -405,6 +572,45 @@ pub unsafe extern "C" fn BNWARPContainerGetTypeGUIDsWithName(
}
#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerSearch(
+ container: *mut BNWARPContainer,
+ query: *mut BNWARPContainerSearchQuery,
+) -> *mut BNWARPContainerSearchResponse {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null_mut();
+ };
+
+ let query = unsafe { ManuallyDrop::new(Arc::from_raw(query)) };
+
+ let result = match container.search(&query) {
+ Ok(result) => result,
+ Err(err) => {
+ log::error!("Failed to search container {:?}: {}", query.deref(), err);
+ return std::ptr::null_mut();
+ }
+ };
+
+ let boxed_raw_items: Box<[_]> = result
+ .items
+ .into_iter()
+ .map(Arc::new)
+ .map(Arc::into_raw)
+ .collect();
+ let count = boxed_raw_items.len();
+ // NOTE: Leak the functions to be freed by BNWARPFreeContainerSearchItemList
+ let leaked_raw_items = Box::into_raw(boxed_raw_items) as *mut *mut BNWARPContainerSearchItem;
+ let raw_result = BNWARPContainerSearchResponse {
+ count,
+ items: leaked_raw_items,
+ total: result.total,
+ offset: result.offset,
+ };
+ // NOTE: Leak the result to be freed by BNWARPFreeContainerSearchResult
+ Box::into_raw(Box::new(raw_result))
+}
+
+#[no_mangle]
pub unsafe extern "C" fn BNWARPNewContainerReference(
container: *mut BNWARPContainer,
) -> *mut BNWARPContainer {
@@ -431,3 +637,62 @@ pub unsafe extern "C" fn BNWARPFreeContainerList(
BNWARPFreeContainerReference(container);
}
}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewContainerSearchQueryReference(
+ query: *mut BNWARPContainerSearchQuery,
+) -> *mut BNWARPContainerSearchQuery {
+ Arc::increment_strong_count(query);
+ query
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeContainerSearchQueryReference(
+ query: *mut BNWARPContainerSearchQuery,
+) {
+ if query.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(query);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewContainerSearchItemReference(
+ item: *mut BNWARPContainerSearchItem,
+) -> *mut BNWARPContainerSearchItem {
+ Arc::increment_strong_count(item);
+ item
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeContainerSearchItemReference(
+ item: *mut BNWARPContainerSearchItem,
+) {
+ if item.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(item);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeContainerSearchItemList(
+ items: *mut *mut BNWARPContainerSearchItem,
+ count: usize,
+) {
+ let items_ptr = std::ptr::slice_from_raw_parts_mut(items, count);
+ let items = unsafe { Box::from_raw(items_ptr) };
+ for item in items {
+ BNWARPFreeContainerSearchItemReference(item);
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeContainerSearchResponse(
+ response: *mut BNWARPContainerSearchResponse,
+) {
+ if response.is_null() {
+ return;
+ }
+ let response = unsafe { Box::from_raw(response) };
+ BNWARPFreeContainerSearchItemList(response.items, response.count);
+}
diff --git a/plugins/warp/src/plugin/ffi/file.rs b/plugins/warp/src/plugin/ffi/file.rs
new file mode 100644
index 00000000..951b5eb2
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi/file.rs
@@ -0,0 +1,38 @@
+use std::ffi::c_char;
+use std::sync::Arc;
+use warp::WarpFile;
+
+pub type BNWARPFile = WarpFile<'static>;
+
+// TODO: At some point we may want to expose chunks directly. For now we will just enumerate all of them.
+// pub type BNWARPChunk = warp::chunk::Chunk<'static>;
+
+// TODO: From bytes as well.
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewFileFromPath(path: *mut c_char) -> *mut BNWARPFile {
+ let path_cstr = unsafe { std::ffi::CStr::from_ptr(path) };
+ let Ok(path) = path_cstr.to_str() else {
+ return std::ptr::null_mut();
+ };
+ let Ok(bytes) = std::fs::read(path) else {
+ return std::ptr::null_mut();
+ };
+ let Some(file) = WarpFile::from_owned_bytes(bytes) else {
+ return std::ptr::null_mut();
+ };
+ Arc::into_raw(Arc::new(file)) as *mut BNWARPFile
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewFileReference(file: *mut BNWARPFile) -> *mut BNWARPFile {
+ Arc::increment_strong_count(file);
+ file
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeFileReference(file: *mut BNWARPFile) {
+ if file.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(file);
+}
diff --git a/plugins/warp/src/plugin/ffi/function.rs b/plugins/warp/src/plugin/ffi/function.rs
index b37d613b..e3f6a079 100644
--- a/plugins/warp/src/plugin/ffi/function.rs
+++ b/plugins/warp/src/plugin/ffi/function.rs
@@ -124,7 +124,7 @@ pub unsafe extern "C" fn BNWARPFunctionGetType(
match &function.ty {
Some(func_ty) => {
let arch = analysis_function.arch();
- let function_type = to_bn_type(&arch, func_ty);
+ let function_type = to_bn_type(Some(arch), func_ty);
// NOTE: The type ref has been pre-incremented for the caller.
unsafe { Ref::into_raw(function_type) }.handle
}
diff --git a/plugins/warp/src/plugin/load.rs b/plugins/warp/src/plugin/load.rs
index cef6ea11..6f8b562e 100644
--- a/plugins/warp/src/plugin/load.rs
+++ b/plugins/warp/src/plugin/load.rs
@@ -40,16 +40,15 @@ pub struct RunMatcherField;
impl RunMatcherField {
pub fn field() -> FormInputField {
- FormInputField::Choice {
- prompt: "Rerun Initial Matcher".to_string(),
- choices: vec!["No".to_string(), "Yes".to_string()],
- default: Some(1),
- value: 0,
+ FormInputField::Checkbox {
+ prompt: "Rerun Matcher".to_string(),
+ default: Some(true),
+ value: false,
}
}
pub fn from_form(form: &Form) -> Option<bool> {
- let field = form.get_field_with_name("Rerun Initial Matcher")?;
+ let field = form.get_field_with_name("Rerun Matcher")?;
let field_value = field.try_value_index()?;
match field_value {
1 => Some(true),
diff --git a/plugins/warp/src/plugin/settings.rs b/plugins/warp/src/plugin/settings.rs
index 489deb23..39092177 100644
--- a/plugins/warp/src/plugin/settings.rs
+++ b/plugins/warp/src/plugin/settings.rs
@@ -1,4 +1,4 @@
-use binaryninja::settings::Settings as BNSettings;
+use binaryninja::settings::{QueryOptions, Settings as BNSettings};
use serde_json::json;
use std::string::ToString;
@@ -20,6 +20,12 @@ pub struct PluginSettings {
///
/// This is set to [PluginSettings::SERVER_API_KEY_DEFAULT] by default.
pub server_api_key: Option<String>,
+ pub second_server_url: Option<String>,
+ pub second_server_api_key: Option<String>,
+ /// A source must have at least one of these tags to be considered a valid source.
+ ///
+ /// This is set to [PluginSettings::SOURCE_TAGS_DEFAULT] by default.
+ pub whitelisted_source_tags: Vec<String>,
/// Whether to allow networked WARP requests. Turning this off will not disable local WARP functionality.
///
/// This is set to [PluginSettings::ENABLE_SERVER_DEFAULT] by default.
@@ -27,6 +33,8 @@ pub struct PluginSettings {
}
impl PluginSettings {
+ pub const WHITELISTED_SOURCE_TAGS_DEFAULT: Vec<String> = vec![];
+ pub const WHITELISTED_SOURCE_TAGS_SETTING: &'static str = "analysis.warp.whitelistedSourceTags";
pub const LOAD_BUNDLED_FILES_DEFAULT: bool = true;
pub const LOAD_BUNDLED_FILES_SETTING: &'static str = "analysis.warp.loadBundledFiles";
pub const LOAD_USER_FILES_DEFAULT: bool = true;
@@ -35,10 +43,25 @@ impl PluginSettings {
pub const SERVER_URL_SETTING: &'static str = "analysis.warp.serverUrl";
pub const SERVER_API_KEY_DEFAULT: Option<String> = None;
pub const SERVER_API_KEY_SETTING: &'static str = "analysis.warp.serverApiKey";
+ pub const SECONDARY_SERVER_URL_DEFAULT: Option<String> = None;
+ pub const SECONDARY_SERVER_URL_SETTING: &'static str = "analysis.warp.secondServerUrl";
+ pub const SECONDARY_SERVER_API_KEY_DEFAULT: Option<String> = None;
+ pub const SECONDARY_SERVER_API_KEY_SETTING: &'static str = "analysis.warp.secondServerApiKey";
pub const ENABLE_SERVER_DEFAULT: bool = false;
pub const ENABLE_SERVER_SETTING: &'static str = "network.enableWARP";
pub fn register(bn_settings: &mut BNSettings) {
+ let whitelisted_source_tags_prop = json!({
+ "title" : "Blacklisted Sources",
+ "type" : "array",
+ "default" : Self::WHITELISTED_SOURCE_TAGS_DEFAULT,
+ "description" : "Add a sources UUID to this list to blacklist it from being considered a valid source. This is useful for sources that are known to be false positives.",
+ "ignore" : [],
+ });
+ bn_settings.register_setting_json(
+ Self::WHITELISTED_SOURCE_TAGS_SETTING,
+ &whitelisted_source_tags_prop.to_string(),
+ );
let load_bundled_files_prop = json!({
"title" : "Load Bundled Files",
"type" : "boolean",
@@ -85,6 +108,31 @@ impl PluginSettings {
Self::SERVER_API_KEY_SETTING,
&server_api_key_prop.to_string(),
);
+ let second_server_url_prop = json!({
+ "title" : "Secondary Server URL",
+ "type" : "string",
+ "default" : Self::SECONDARY_SERVER_URL_DEFAULT,
+ "description" : "",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"],
+ "requiresRestart" : true
+ });
+ bn_settings.register_setting_json(
+ Self::SECONDARY_SERVER_URL_SETTING,
+ &second_server_url_prop.to_string(),
+ );
+ let second_server_api_key_prop = json!({
+ "title" : "Secondary Server API Key",
+ "type" : "string",
+ "default" : Self::SECONDARY_SERVER_API_KEY_DEFAULT,
+ "description" : "",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"],
+ "hidden": true,
+ "requiresRestart" : true
+ });
+ bn_settings.register_setting_json(
+ Self::SECONDARY_SERVER_API_KEY_SETTING,
+ &second_server_api_key_prop.to_string(),
+ );
let server_enabled_prop = json!({
"title" : "Enable WARP",
"type" : "boolean",
@@ -100,7 +148,7 @@ impl PluginSettings {
}
/// Retrieve plugin settings from [`BNSettings`].
- pub fn from_settings(bn_settings: &BNSettings) -> Self {
+ pub fn from_settings(bn_settings: &BNSettings, query_opts: &mut QueryOptions) -> Self {
let mut settings = PluginSettings::default();
if bn_settings.contains(Self::LOAD_BUNDLED_FILES_SETTING) {
settings.load_bundled_files = bn_settings.get_bool(Self::LOAD_BUNDLED_FILES_SETTING);
@@ -117,9 +165,30 @@ impl PluginSettings {
settings.server_api_key = Some(server_api_key_str);
}
}
+ if bn_settings.contains(Self::SECONDARY_SERVER_URL_SETTING) {
+ let server_api_key_str = bn_settings.get_string(Self::SECONDARY_SERVER_URL_SETTING);
+ if !server_api_key_str.is_empty() {
+ settings.second_server_url = Some(server_api_key_str);
+ }
+ }
+ if bn_settings.contains(Self::SECONDARY_SERVER_API_KEY_SETTING) {
+ let server_api_key_str = bn_settings.get_string(Self::SECONDARY_SERVER_API_KEY_SETTING);
+ if !server_api_key_str.is_empty() {
+ settings.second_server_api_key = Some(server_api_key_str);
+ }
+ }
if bn_settings.contains(Self::ENABLE_SERVER_SETTING) {
settings.enable_server = bn_settings.get_bool(Self::ENABLE_SERVER_SETTING);
}
+
+ if bn_settings.contains(Self::WHITELISTED_SOURCE_TAGS_SETTING) {
+ let whitelisted_source_tags_str = bn_settings
+ .get_string_list_with_opts(Self::WHITELISTED_SOURCE_TAGS_SETTING, query_opts);
+ settings.whitelisted_source_tags = whitelisted_source_tags_str
+ .iter()
+ .map(|s| s.to_string())
+ .collect();
+ }
settings
}
}
@@ -127,10 +196,13 @@ impl PluginSettings {
impl Default for PluginSettings {
fn default() -> Self {
Self {
+ whitelisted_source_tags: PluginSettings::WHITELISTED_SOURCE_TAGS_DEFAULT,
load_bundled_files: PluginSettings::LOAD_BUNDLED_FILES_DEFAULT,
load_user_files: PluginSettings::LOAD_USER_FILES_DEFAULT,
server_url: PluginSettings::SERVER_URL_DEFAULT.to_string(),
server_api_key: PluginSettings::SERVER_API_KEY_DEFAULT,
+ second_server_url: PluginSettings::SECONDARY_SERVER_URL_DEFAULT,
+ second_server_api_key: PluginSettings::SECONDARY_SERVER_API_KEY_DEFAULT,
enable_server: PluginSettings::ENABLE_SERVER_DEFAULT,
}
}
diff --git a/plugins/warp/src/plugin/workflow.rs b/plugins/warp/src/plugin/workflow.rs
index 9871f5d2..08d07c52 100644
--- a/plugins/warp/src/plugin/workflow.rs
+++ b/plugins/warp/src/plugin/workflow.rs
@@ -162,11 +162,7 @@ pub fn run_matcher(view: &BinaryView) {
log::info!("Matcher was cancelled by user, you may run it again by running the 'Run Matcher' command.");
}
- // It is noisy to show this every time, so we only show it in cases where a user can reasonably perceive.
- let elapsed = start.elapsed();
- if elapsed > std::time::Duration::from_secs(1) {
- log::info!("Function matching took {:?}", elapsed);
- }
+ log::info!("Function matching took {:?}", start.elapsed());
background_task.finish();
// Now we want to trigger re-analysis.
@@ -185,7 +181,7 @@ pub fn insert_workflow() -> Result<(), ()> {
// otherwise we will wipe over user type info.
if !function.has_user_type() {
if let Some(func_ty) = &matched_function.ty {
- function.set_auto_type(&to_bn_type(&function.arch(), func_ty));
+ function.set_auto_type(&to_bn_type(Some(function.arch()), func_ty));
}
}
if let Some(mlil) = ctx.mlil_function() {
@@ -206,7 +202,7 @@ pub fn insert_workflow() -> Result<(), ()> {
continue;
}
let decl_ty = match variable.ty {
- Some(decl_ty) => to_bn_type(&function.arch(), &decl_ty),
+ Some(decl_ty) => to_bn_type(Some(function.arch()), &decl_ty),
None => {
let Some(existing_var) = function.variable_type(&decl_var) else {
continue;
diff --git a/plugins/warp/tests/determinism.rs b/plugins/warp/tests/determinism.rs
index e0568794..5100b436 100644
--- a/plugins/warp/tests/determinism.rs
+++ b/plugins/warp/tests/determinism.rs
@@ -32,8 +32,8 @@ fn insta_signatures() {
.functions()
.iter()
.map(|f| {
- let guid = cached_function_guid(&f, &f.lifted_il().unwrap());
- (f.start(), guid)
+ let guid = cached_function_guid(&f, || f.lifted_il().ok());
+ (f.start(), guid.unwrap())
})
.collect();
insta::assert_debug_snapshot!(file_name, functions);
diff --git a/plugins/warp/tests/matcher.rs b/plugins/warp/tests/matcher.rs
index 13e2ce01..880112d0 100644
--- a/plugins/warp/tests/matcher.rs
+++ b/plugins/warp/tests/matcher.rs
@@ -169,7 +169,7 @@ fn test_add_type_to_view() {
.guid(struct_type_guid)
.build();
let ref_type = Type::builder().name("my_ref").class(ref_class).build();
- matcher.add_type_to_view(&container, &source, &view, &arch, &ref_type);
+ matcher.add_type_to_view(&container, &source, &view, arch, &ref_type);
println!("{:#?}", view.types().to_vec());
diff --git a/plugins/warp/ui/CMakeLists.txt b/plugins/warp/ui/CMakeLists.txt
index c3e5bcf6..88cc6a60 100644
--- a/plugins/warp/ui/CMakeLists.txt
+++ b/plugins/warp/ui/CMakeLists.txt
@@ -9,7 +9,10 @@ file(GLOB SOURCES CONFIGURE_DEPENDS
shared/misc.cpp shared/misc.h
shared/constraint.cpp shared/constraint.h
shared/function.cpp shared/function.h
- shared/container.cpp shared/container.h)
+ containers.cpp containers.h
+ shared/search.cpp shared/search.h
+ shared/fetcher.cpp shared/fetcher.h
+ shared/fetchdialog.cpp shared/fetchdialog.h)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
diff --git a/plugins/warp/ui/containers.cpp b/plugins/warp/ui/containers.cpp
new file mode 100644
index 00000000..689d695f
--- /dev/null
+++ b/plugins/warp/ui/containers.cpp
@@ -0,0 +1,274 @@
+#include "containers.h"
+
+QVariant WarpSourcesModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return {};
+ if (index.row() < 0 || index.row() >= rowCount())
+ return {};
+
+ const auto &r = m_rows[static_cast<size_t>(index.row())];
+
+ // Build a small two-dot status icon (left: writable, right: uncommitted)
+ auto statusIcon = [](bool writable, bool uncommitted) -> QIcon {
+ static QIcon cache[2][2]; // [writable][uncommitted]
+ QIcon &cached = cache[writable ? 1 : 0][uncommitted ? 1 : 0];
+ if (!cached.isNull())
+ return cached;
+
+ const int w = 16, h = 12, radius = 4;
+ QPixmap pm(w, h);
+ pm.fill(Qt::transparent);
+ QPainter p(&pm);
+ p.setRenderHint(QPainter::Antialiasing, true);
+
+ // Colors
+ QColor writableOn(76, 175, 80); // green
+ QColor writableOff(158, 158, 158); // grey
+ QColor uncommittedOn(255, 193, 7); // amber
+ QColor uncommittedOff(158, 158, 158); // grey
+
+ // Left dot: writable
+ p.setBrush(writable ? writableOn : writableOff);
+ p.setPen(Qt::NoPen);
+ p.drawEllipse(QPoint(4, h / 2), radius, radius);
+
+ // Right dot: uncommitted
+ p.setBrush(uncommitted ? uncommittedOn : uncommittedOff);
+ p.drawEllipse(QPoint(w - 6, h / 2), radius, radius);
+
+ p.end();
+ cached = QIcon(pm);
+ return cached;
+ };
+
+ if (role == Qt::DecorationRole && index.column() == PathCol)
+ {
+ return statusIcon(r.writable, r.uncommitted);
+ }
+
+ if (role == Qt::ToolTipRole && index.column() == PathCol)
+ {
+ QStringList parts;
+ parts << (r.writable ? "Writable" : "Read-only");
+ parts << (r.uncommitted ? "Uncommitted changes" : "No uncommitted changes");
+ return parts.join(" • ");
+ }
+
+ if (role == Qt::DisplayRole)
+ {
+ switch (index.column())
+ {
+ case GuidCol: return r.guid;
+ case PathCol: return r.path;
+ case WritableCol: return r.writable ? "Yes" : "No";
+ case UncommittedCol: return r.uncommitted ? "Yes" : "No";
+ default: return {};
+ }
+ }
+
+ if (role == Qt::CheckStateRole)
+ {
+ // Optional: expose as checkboxes if someone ever shows these columns
+ switch (index.column())
+ {
+ case WritableCol: return r.writable ? Qt::Checked : Qt::Unchecked;
+ case UncommittedCol: return r.uncommitted ? Qt::Checked : Qt::Unchecked;
+ default: break;
+ }
+ }
+
+ return {};
+}
+
+WarpContainerWidget::WarpContainerWidget(Warp::Ref<Warp::Container> container, QWidget *parent) : QWidget(parent)
+{
+ m_container = std::move(container);
+ auto *layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ m_tabs = new QTabWidget(this);
+ layout->addWidget(m_tabs);
+
+ // Sources tab
+ m_sourcesPage = new QWidget(this);
+ auto *sourcesLayout = new QVBoxLayout(m_sourcesPage);
+ m_sourcesView = new QTableView(m_sourcesPage);
+ m_sourcesModel = new WarpSourcesModel(m_sourcesPage);
+ m_sourcesModel->setContainer(m_container);
+ m_sourcesView->setModel(m_sourcesModel);
+ m_sourcesView->horizontalHeader()->setStretchLastSection(true);
+ m_sourcesView->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_sourcesView->setSelectionMode(QAbstractItemView::SingleSelection);
+
+ // Make the table look like a simple list that shows only the source path
+ m_sourcesView->setShowGrid(false);
+ m_sourcesView->verticalHeader()->setVisible(false);
+ m_sourcesView->horizontalHeader()->setVisible(false);
+ m_sourcesView->setAlternatingRowColors(false);
+ m_sourcesView->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_sourcesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_sourcesView->setWordWrap(false);
+ m_sourcesView->setIconSize(QSize(16, 12));
+ // Ensure long paths truncate from the left: "...tail/of/the/path"
+ m_sourcesView->setTextElideMode(Qt::ElideLeft);
+ // Hide GUID column, keep only the Path column visible
+ m_sourcesView->setColumnHidden(WarpSourcesModel::GuidCol, true);
+ // Also hide boolean columns; their state is shown as an icon next to the path
+ m_sourcesView->setColumnHidden(WarpSourcesModel::WritableCol, true);
+ m_sourcesView->setColumnHidden(WarpSourcesModel::UncommittedCol, true);
+ // Ensure the remaining (Path) column fills the width
+ m_sourcesView->horizontalHeader()->setSectionResizeMode(WarpSourcesModel::PathCol, QHeaderView::Stretch);
+
+ // Per-item context menu
+ m_sourcesView->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(m_sourcesView, &QWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
+ QMenu menu(m_sourcesView);
+ const QModelIndex index = m_sourcesView->indexAt(pos);
+
+ if (!index.isValid())
+ {
+ QAction *actAdd = menu.addAction(tr("Add Source"));
+ QAction *chosen = menu.exec(m_sourcesView->viewport()->mapToGlobal(pos));
+ if (!chosen)
+ return;
+ if (chosen == actAdd)
+ {
+ std::string sourceName;
+ if (!BinaryNinja::GetTextLineInput(sourceName, "Source name:", "Add Source"))
+ return;
+ if (const auto sourceId = m_container->AddSource(sourceName); !sourceId.has_value())
+ {
+ BinaryNinja::LogAlertF("Failed to add source: {}", sourceName);
+ return;
+ }
+ m_sourcesModel->reload();
+ }
+ } else
+ {
+ m_sourcesView->setCurrentIndex(index.sibling(index.row(), WarpSourcesModel::PathCol));
+
+ const int row = index.row();
+ const QModelIndex pathIdx = m_sourcesModel->index(row, WarpSourcesModel::PathCol);
+ const QModelIndex guidIdx = m_sourcesModel->index(row, WarpSourcesModel::GuidCol);
+ const QString path = m_sourcesModel->data(pathIdx, Qt::DisplayRole).toString();
+ const QFileInfo fi(path);
+
+ const QString guid = m_sourcesModel->data(guidIdx, Qt::DisplayRole).toString();
+
+ QAction *actReveal = menu.addAction(tr("Reveal in File Browser"));
+ actReveal->setEnabled(fi.exists());
+ QAction *actCopyPath = menu.addAction(tr("Copy Path"));
+ QAction *actCopyGuid = menu.addAction(tr("Copy GUID"));
+
+ QAction *chosen = menu.exec(m_sourcesView->viewport()->mapToGlobal(pos));
+ if (!chosen)
+ return;
+ if (chosen == actCopyPath)
+ QGuiApplication::clipboard()->setText(path);
+ else if (chosen == actCopyGuid)
+ QGuiApplication::clipboard()->setText(guid);
+ else if (chosen == actReveal)
+ QDesktopServices::openUrl(QUrl::fromLocalFile(fi.absoluteFilePath()));
+ }
+ });
+
+
+ sourcesLayout->addWidget(m_sourcesView);
+ m_tabs->addTab(m_sourcesPage, tr("Sources"));
+
+ // Search tab
+ m_searchTab = new WarpSearchWidget(m_container, this);
+ m_tabs->addTab(m_searchTab, tr("Search"));
+
+ // Periodic refresh timer for the Sources view
+ m_refreshTimer = new QTimer(this);
+ m_refreshTimer->setInterval(5000);
+ connect(m_refreshTimer, &QTimer::timeout, this, [this]() {
+ // Only refresh if the widget and the Sources page are actually visible
+ if (!this->isVisible() || !m_sourcesPage || !m_sourcesPage->isVisible())
+ return;
+
+ // Preserve selection by GUID across reloads
+ QString currentGuid;
+ if (const QModelIndex currentIdx = m_sourcesView->currentIndex(); currentIdx.isValid())
+ {
+ const int row = currentIdx.row();
+ const QModelIndex guidIdx = m_sourcesModel->index(row, WarpSourcesModel::GuidCol);
+ currentGuid = m_sourcesModel->data(guidIdx, Qt::DisplayRole).toString();
+ }
+
+ m_sourcesModel->reload();
+
+ if (!currentGuid.isEmpty())
+ {
+ for (int r = 0; r < m_sourcesModel->rowCount(); ++r)
+ {
+ const QModelIndex gIdx = m_sourcesModel->index(r, WarpSourcesModel::GuidCol);
+ if (m_sourcesModel->data(gIdx, Qt::DisplayRole).toString() == currentGuid)
+ {
+ m_sourcesView->setCurrentIndex(m_sourcesModel->index(r, WarpSourcesModel::PathCol));
+ break;
+ }
+ }
+ }
+ });
+ m_refreshTimer->start();
+
+ // Optional: force a refresh when switching back to the Sources tab
+ connect(m_tabs, &QTabWidget::currentChanged, this, [this](const int idx) {
+ QWidget *w = m_tabs->widget(idx);
+ if (w == m_sourcesPage)
+ m_sourcesModel->reload();
+ });
+}
+
+WarpContainersPane::WarpContainersPane(QWidget *parent) : QWidget(parent)
+{
+ auto *splitter = new QSplitter(Qt::Vertical, this);
+ splitter->setContentsMargins(0, 0, 0, 0);
+ auto *mainLayout = new QVBoxLayout(this);
+ mainLayout->setContentsMargins(0, 0, 0, 0);
+ mainLayout->setSpacing(0);
+ mainLayout->addWidget(splitter);
+ auto newPalette = palette();
+ newPalette.setColor(QPalette::Window, getThemeColor(SidebarWidgetBackgroundColor));
+ setAutoFillBackground(true);
+ setPalette(newPalette);
+
+ // List on top
+ m_list = new QListWidget(splitter);
+ m_list->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_list->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+ m_list->setUniformItemSizes(true);
+
+ // Make names larger and show end of long strings (elide at the start)
+ {
+ QFont f = m_list->font();
+ f.setPointSizeF(f.pointSizeF() + 2.0); // bump size
+ m_list->setFont(f);
+ m_list->setTextElideMode(Qt::ElideLeft);
+ }
+
+ // Container view (tabs) below
+ m_stack = new QStackedWidget(splitter);
+ m_stack->setContentsMargins(0, 0, 0, 0);
+
+ splitter->setStretchFactor(0, 0); // list: minimal growth
+ splitter->setStretchFactor(1, 1); // stack: takes remaining space
+ splitter->setCollapsible(0, false);
+ splitter->setCollapsible(1, false);
+
+ populate();
+
+ connect(m_list, &QListWidget::currentRowChanged, this, [this](int row) {
+ if (row >= 0 && row < m_stack->count())
+ m_stack->setCurrentIndex(row);
+ });
+
+ // Select the first container if available
+ if (m_list->count() > 0)
+ {
+ m_list->setCurrentRow(0);
+ }
+}
diff --git a/plugins/warp/ui/containers.h b/plugins/warp/ui/containers.h
new file mode 100644
index 00000000..80ffa12f
--- /dev/null
+++ b/plugins/warp/ui/containers.h
@@ -0,0 +1,175 @@
+#pragma once
+
+#include <QWidget>
+#include <optional>
+#include <QClipboard>
+#include <QDesktopServices>
+#include <QInputDialog>
+#include <QListWidget>
+#include "shared/search.h"
+
+#include "theme.h"
+#include "warp.h"
+#include "../../../../ui/mainwindow.h"
+
+class WarpSourcesModel final : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ enum Columns : int
+ {
+ GuidCol = 0,
+ PathCol,
+ WritableCol,
+ UncommittedCol,
+ ColumnCount
+ };
+
+ explicit WarpSourcesModel(QObject *parent = nullptr)
+ : QAbstractTableModel(parent)
+ {
+ }
+
+ void setContainer(Warp::Ref<Warp::Container> container)
+ {
+ m_container = std::move(container);
+ reload();
+ }
+
+ void reload()
+ {
+ // Fetch synchronously (can be adapted to async if needed)
+ beginResetModel();
+ m_rows.clear();
+ for (const auto &src: m_container->GetSources())
+ {
+ QString guid = QString::fromStdString(src.ToString());
+ QString path = QString::fromStdString(m_container->SourcePath(src).value_or(std::string{}));
+ bool writable = m_container->IsSourceWritable(src);
+ bool uncommitted = m_container->IsSourceUncommitted(src);
+ m_rows.push_back({guid, path, writable, uncommitted});
+ }
+ endResetModel();
+ }
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override
+ {
+ if (parent.isValid()) return 0;
+ return static_cast<int>(m_rows.size());
+ }
+
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override
+ {
+ Q_UNUSED(parent);
+ return ColumnCount;
+ }
+
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override
+ {
+ if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
+ {
+ switch (section)
+ {
+ case GuidCol: return "Source GUID";
+ case PathCol: return "Path";
+ case WritableCol: return "Writable";
+ case UncommittedCol: return "Uncommitted";
+ default: return {};
+ }
+ }
+ return {};
+ }
+
+private:
+ struct Row
+ {
+ QString guid;
+ QString path;
+ bool writable;
+ bool uncommitted;
+ };
+
+ std::vector<Row> m_rows;
+ Warp::Ref<Warp::Container> m_container;
+};
+
+class WarpContainerWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit WarpContainerWidget(Warp::Ref<Warp::Container> container, QWidget *parent = nullptr);
+
+private:
+ Warp::Ref<Warp::Container> m_container;
+
+ QTabWidget *m_tabs = nullptr;
+
+ // Sources
+ QTableView *m_sourcesView = nullptr;
+ WarpSourcesModel *m_sourcesModel = nullptr;
+ QWidget* m_sourcesPage = nullptr;
+ QTimer* m_refreshTimer = nullptr;
+
+ // Search
+ WarpSearchWidget *m_searchTab = nullptr;
+};
+
+class WarpContainersPane : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit WarpContainersPane(QWidget *parent = nullptr);
+
+ void refresh()
+ {
+ // Clear and repopulate from current container list
+ m_list->clear();
+ while (m_stack->count() > 0)
+ {
+ QWidget *w = m_stack->widget(0);
+ m_stack->removeWidget(w);
+ delete w;
+ }
+ m_containers.clear();
+ populate();
+ if (m_list->count() > 0)
+ m_list->setCurrentRow(0);
+ }
+
+private:
+ void populate()
+ {
+ // Retrieve all available containers
+ const auto all = Warp::Container::All();
+ m_containers = all; // copy vector<Ref<Container>>
+
+ for (const auto &c: m_containers)
+ {
+ const QString name = QString::fromStdString(c->GetName());
+ auto *item = new QListWidgetItem(name, m_list);
+ item->setSizeHint(QSize(item->sizeHint().width(), itemHeightPx()));
+ auto *widget = new WarpContainerWidget(c, m_stack);
+ m_stack->addWidget(widget);
+ }
+
+ // Visual style: behave like a vertical tab bar
+ // m_list->setFrameShape(QFrame::NoFrame);
+ // m_list->setSpacing(0);
+ }
+
+ static int itemHeightPx()
+ {
+ // A reasonable, readable height per entry
+ return 28;
+ }
+
+private:
+ QListWidget *m_list = nullptr;
+ QStackedWidget *m_stack = nullptr;
+ std::vector<Warp::Ref<Warp::Container> > m_containers;
+};
diff --git a/plugins/warp/ui/matches.cpp b/plugins/warp/ui/matches.cpp
index 7e22e224..2345a7ea 100644
--- a/plugins/warp/ui/matches.cpp
+++ b/plugins/warp/ui/matches.cpp
@@ -11,12 +11,12 @@
#include "warp.h"
#include "shared/misc.h"
-WarpCurrentFunctionWidget::WarpCurrentFunctionWidget(FunctionRef current)
+WarpCurrentFunctionWidget::WarpCurrentFunctionWidget()
{
- // NOTE: Might be nullptr if the no selected function.
- m_current = current;
+ // We must explicitly support no current function.
+ m_current = nullptr;
- m_logger = new BinaryNinja::Logger("WARP");
+ m_logger = new BinaryNinja::Logger("WARP UI");
// Create the QT stuff
QGridLayout *layout = new QGridLayout(this);
@@ -107,6 +107,18 @@ WarpCurrentFunctionWidget::WarpCurrentFunctionWidget(FunctionRef current)
});
}
+void WarpCurrentFunctionWidget::SetFetcher(std::shared_ptr<WarpFetcher> fetcher)
+{
+ m_fetcher = fetcher;
+ // TODO: We need to remove the completion callback from the previously set fetcher.
+ m_fetcher->AddCompletionCallback([this]() {
+ // TODO: This is a little bit underspecified, we may end up updating more than strictly necessary.
+ // Once this function has been fetched, we need to update the matches for this widget.
+ UpdateMatches();
+ return KeepCallback;
+ });
+}
+
void WarpCurrentFunctionWidget::SetCurrentFunction(FunctionRef current)
{
if (m_current == current)
@@ -114,21 +126,18 @@ void WarpCurrentFunctionWidget::SetCurrentFunction(FunctionRef current)
m_current = current;
m_infoWidget->SetAnalysisFunction(m_current);
- if (current)
+ // If we have a fetcher we should also let it know to try and fetch the possible functions from the containers.
+ if (current && m_fetcher)
{
- // Add the function to the processing list only if we have no already done so.
- // If a user goes to a function, then navigates away, they do not want to
- // have us try and send a network request!
+ m_fetcher->AddPendingFunction(current);
+
+ // TODO: Automatically fetch the function, I need to figure out how to make this debounce correctly so that all requests are processed in line.
+ if (!m_fetcher->m_requestInProgress.exchange(true))
{
- std::lock_guard<std::mutex> lock(m_requestMutex);
- uint64_t funcStart = current->GetStart();
- if (m_processedFunctions.find(funcStart) == m_processedFunctions.end()) {
- m_pendingRequests.push_back(current);
- }
- }
- if (!m_requestInProgress.exchange(true)) {
BinaryNinja::WorkerPriorityEnqueue([this]() {
- ProcessPendingFetchRequests();
+ BinaryNinja::Ref bgTask = new BinaryNinja::BackgroundTask("Fetching WARP Functions...", true);
+ m_fetcher->FetchPendingFunctions();
+ bgTask->Finish();
});
}
}
@@ -185,51 +194,3 @@ void WarpCurrentFunctionWidget::UpdateMatches()
m_tableWidget->SetFunctions(matches);
}
-
-void WarpCurrentFunctionWidget::ProcessPendingFetchRequests()
-{
- std::vector<FunctionRef> requests;
- {
- std::lock_guard<std::mutex> lock(m_requestMutex);
- requests = std::move(m_pendingRequests);
- m_pendingRequests.clear();
- }
-
- if (requests.empty()) {
- m_requestInProgress = false;
- return;
- }
-
- auto start_time = std::chrono::high_resolution_clock::now();
-
- std::vector<Warp::FunctionGUID> guids;
- Warp::Ref<Warp::Target> target;
- for (const auto& func : requests) {
- // TODO: Need to send multiple requests if there is multiple targets.
- if (!target)
- target = Warp::Target::FromPlatform(*func->GetPlatform());
- if (const auto guid = Warp::GetAnalysisFunctionGUID(*func); guid.has_value())
- guids.push_back(guid.value());
- }
-
- // Actually fetch the data!
- if (!guids.empty())
- for (const auto &container: Warp::Container::All())
- container->FetchFunctions(*target, guids);
-
- {
- std::lock_guard<std::mutex> lock(m_requestMutex);
- for (const auto& func : requests) {
- m_processedFunctions.insert(func->GetStart());
- }
- }
-
- // TODO: Update the matches, make sure there was stuff added first lol.
- // TODO: UpdateMatches();
-
- const auto end_time = std::chrono::high_resolution_clock::now();
- const std::chrono::duration<double> elapsed_time = end_time - start_time;
- m_logger->LogDebug("ProcessPendingRequests took %f seconds", elapsed_time.count());
-
- m_requestInProgress = false;
-}
diff --git a/plugins/warp/ui/matches.h b/plugins/warp/ui/matches.h
index 251dcb89..88926add 100644
--- a/plugins/warp/ui/matches.h
+++ b/plugins/warp/ui/matches.h
@@ -4,6 +4,7 @@
#include "filter.h"
#include "render.h"
+#include "shared/fetcher.h"
#include "shared/function.h"
class WarpCurrentFunctionWidget : public QWidget
@@ -18,21 +19,18 @@ class WarpCurrentFunctionWidget : public QWidget
LoggerRef m_logger;
- std::mutex m_requestMutex;
- std::vector<FunctionRef> m_pendingRequests;
- std::atomic<bool> m_requestInProgress {false};
- std::unordered_set<uint64_t> m_processedFunctions;
+ std::shared_ptr<WarpFetcher> m_fetcher;
public:
- explicit WarpCurrentFunctionWidget(FunctionRef current);
+ explicit WarpCurrentFunctionWidget();
~WarpCurrentFunctionWidget() override = default;
+ void SetFetcher(std::shared_ptr<WarpFetcher> fetcher);
+
void SetCurrentFunction(FunctionRef current);
FunctionRef GetCurrentFunction() { return m_current; };
void UpdateMatches();
-
- void ProcessPendingFetchRequests();
};
diff --git a/plugins/warp/ui/plugin.cpp b/plugins/warp/ui/plugin.cpp
index dccea1f1..388fea3c 100644
--- a/plugins/warp/ui/plugin.cpp
+++ b/plugins/warp/ui/plugin.cpp
@@ -6,6 +6,7 @@
#include "matches.h"
#include "symbollist.h"
#include "viewframe.h"
+#include "shared/fetchdialog.h"
using namespace BinaryNinja;
@@ -32,11 +33,35 @@ Ref<BackgroundTask> GetMatcherTask()
return matcherTask;
}
+void ShowNetworkNotice()
+{
+ // By default, network access is disabled for WARP, this function will show the user a notice to enable it and restart.
+ const auto settings = Settings::Instance();
+ const bool networkNoticeShown = QSettings().value("warp/NetworkNoticeShown", false).toBool();
+ QSettings().setValue("warp/NetworkNoticeShown", true);
+ if (!networkNoticeShown && settings->Contains("network.enableWARP") && !settings->Get<bool>("network.enableWARP"))
+ {
+ const bool enable = ShowMessageBox("Enable WARP Network Access?",
+ "Network access is disabled by default. Enable WARP network features now?\n\n"
+ "You can change this later in Settings.",
+ YesNoButtonSet, InformationIcon) == YesButton;
+ settings->Set("network.enableWARP", enable);
+ // TODO: Add a notifyRestartRequired call here
+ if (enable)
+ ShowMessageBox("WARP Network Enabled", "Please restart Binary Ninja to allow WARP to make requests to the server.", OKButtonSet, InformationIcon);
+ else
+ ShowMessageBox("WARP Network Disabled", "WARP network access will remain disabled. You can enable it later from Settings.", OKButtonSet, InformationIcon);
+ }
+}
+
WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP"), m_data(data)
{
- m_logger = LogRegistry::CreateLogger("WARPUI");
+ m_logger = LogRegistry::CreateLogger("WARP UI");
m_currentFrame = nullptr;
+ // If not already shown, opening the sidebar will give notice.
+ ShowNetworkNotice();
+
m_headerWidget = new QWidget();
QHBoxLayout *headerLayout = new QHBoxLayout();
headerLayout->setContentsMargins(0, 0, 0, 0);
@@ -46,20 +71,22 @@ WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP")
headerToolbar->setContentsMargins(0, 0, 0, 0);
headerToolbar->setIconSize(QSize(20, 20));
- static auto matcherStopIcon = GetColoredIcon(":/icons/images/stop.png", getThemeColor(RedStandardHighlightColor));
- static auto matcherStartIcon = GetColoredIcon(":/icons/images/start.png",
- getThemeColor(GreenStandardHighlightColor));
- m_matcherAction = headerToolbar->addAction(matcherStartIcon, "Run Matcher", [this]() {
+ auto fetchIcon = GetColoredIcon(":/icons/images/arrow-pull.png", getThemeColor(BlueStandardHighlightColor));
+ auto fetchAction = headerToolbar->addAction(fetchIcon, "Fetch data from WARP containers", [this]() {
UIActionHandler *handler = m_currentFrame->getCurrentViewInterface()->actionHandler();
- if (Ref<BackgroundTask> matcherTask = GetMatcherTask())
- matcherTask->Cancel();
- else if (!isMatcherRunning)
- {
- handler->executeAction("WARP\\Run Matcher");
- setMatcherActionIcon(true);
- }
+ handler->executeAction("WARP\\Fetch");
});
- m_matcherAction->setToolTip("Run the matcher on all functions");
+ fetchAction->setToolTip("Fetch data from WARP containers");
+
+ auto commitIcon = GetColoredIcon(":/icons/images/arrow-push.png", getThemeColor(BlueStandardHighlightColor));
+ auto commitAction = headerToolbar->addAction(commitIcon, "Commit a WARP file to a source", [this]() {
+ UIActionHandler *handler = m_currentFrame->getCurrentViewInterface()->actionHandler();
+ handler->executeAction("WARP\\Commit File");
+ });
+ commitAction->setToolTip("Commit a WARP file to a source");
+
+ // We want to make it clear that the container actions for fetching and pushing are seperate.
+ headerToolbar->addSeparator();
auto loadIcon = GetColoredIcon(":/icons/images/file-add.png", getThemeColor(BlueStandardHighlightColor));
auto loadAction = headerToolbar->addAction(loadIcon, "Load Signature File", [this]() {
@@ -75,21 +102,36 @@ WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP")
});
saveAction->setToolTip("Save data to a signature file");
+ headerToolbar->addSeparator();
+
+ static auto matcherStopIcon = GetColoredIcon(":/icons/images/stop.png", getThemeColor(RedStandardHighlightColor));
+ static auto matcherStartIcon = GetColoredIcon(":/icons/images/start.png",
+ getThemeColor(GreenStandardHighlightColor));
+ m_matcherAction = headerToolbar->addAction(matcherStartIcon, "Run Matcher", [this]() {
+ UIActionHandler *handler = m_currentFrame->getCurrentViewInterface()->actionHandler();
+ if (Ref<BackgroundTask> matcherTask = GetMatcherTask())
+ matcherTask->Cancel();
+ else if (!isMatcherRunning)
+ {
+ handler->executeAction("WARP\\Run Matcher");
+ setMatcherActionIcon(true);
+ }
+ });
+ m_matcherAction->setToolTip("Run the matcher on all functions");
+
auto refreshIcon = GetColoredIcon(":/icons/images/refresh.png", getThemeColor(BlueStandardHighlightColor));
auto refreshAction = headerToolbar->addAction(refreshIcon, "Refresh the view data", [this]() {
Update();
});
refreshAction->setToolTip("Refresh the sidebar data");
- // TODO: Add action for pushing to network sources.
-
// Push the toolbar to the right using a stretch space.
headerLayout->addStretch();
headerLayout->addWidget(headerToolbar, 0);
m_headerWidget->setLayout(headerLayout);
QFrame *currentFunctionFrame = new QFrame(this);
- m_currentFunctionWidget = new WarpCurrentFunctionWidget(nullptr);
+ m_currentFunctionWidget = new WarpCurrentFunctionWidget();
QVBoxLayout *currentFunctionLayout = new QVBoxLayout();
currentFunctionLayout->setContentsMargins(0, 0, 0, 0);
currentFunctionLayout->setSpacing(0);
@@ -104,6 +146,14 @@ WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP")
matchedLayout->addWidget(m_matchedWidget);
matchedFrame->setLayout(matchedLayout);
+ QFrame *containerFrame = new QFrame(this);
+ m_containerWidget = new WarpContainersPane();
+ QVBoxLayout *containerLayout = new QVBoxLayout();
+ containerLayout->setContentsMargins(0, 0, 0, 0);
+ containerLayout->setSpacing(0);
+ containerLayout->addWidget(m_containerWidget);
+ containerFrame->setLayout(containerLayout);
+
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
@@ -111,6 +161,7 @@ WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP")
auto tabWidget = new QTabWidget(this);
tabWidget->addTab(currentFunctionFrame, "Current Function");
tabWidget->addTab(matchedFrame, "Matched Functions");
+ tabWidget->addTab(containerFrame, "Containers");
m_analysisEvent = new AnalysisCompletionEvent(m_data, [this]() {
ExecuteOnMainThread([this]() {
@@ -120,6 +171,9 @@ WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP")
layout->addWidget(tabWidget);
this->setLayout(layout);
+
+ // NOTE: This fetcher is shared with the fetch dialog that is constructed on initialization of this plugin.
+ m_currentFunctionWidget->SetFetcher(WarpFetcher::Global());
}
WarpSidebarWidget::~WarpSidebarWidget()
@@ -133,7 +187,9 @@ void WarpSidebarWidget::focus()
void WarpSidebarWidget::Update()
{
+ m_currentFunctionWidget->UpdateMatches();
m_matchedWidget->Update();
+ // TODO: Obviously this probably should not be called here.
setMatcherActionIcon(false);
}
@@ -180,6 +236,7 @@ void WarpSidebarWidget::notifyViewLocationChanged(View *view, const ViewLocation
WarpSidebarWidgetType::WarpSidebarWidgetType() : SidebarWidgetType(QImage(":/icons/images/warp.png"), "WARP")
{
+
}
@@ -194,6 +251,7 @@ BINARYNINJAPLUGIN void CorePluginDependencies()
BINARYNINJAPLUGIN bool UIPluginInit()
{
+ RegisterWarpFetchFunctionsCommand();
Sidebar::addSidebarWidgetType(new WarpSidebarWidgetType());
return true;
}
diff --git a/plugins/warp/ui/plugin.h b/plugins/warp/ui/plugin.h
index 18525218..56b31711 100644
--- a/plugins/warp/ui/plugin.h
+++ b/plugins/warp/ui/plugin.h
@@ -4,6 +4,7 @@
#include "matches.h"
#include "sidebar.h"
#include "sidebarwidget.h"
+#include "containers.h"
class WarpSidebarWidget : public SidebarWidget
{
@@ -19,6 +20,7 @@ class WarpSidebarWidget : public SidebarWidget
WarpCurrentFunctionWidget *m_currentFunctionWidget;
WarpMatchedWidget *m_matchedWidget;
+ WarpContainersPane *m_containerWidget;
public:
explicit WarpSidebarWidget(BinaryViewRef data);
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;
+};