summaryrefslogtreecommitdiff
path: root/plugins/warp
diff options
context:
space:
mode:
authorMason Reed <mason@vector35.com>2026-03-13 12:24:18 -0700
committerMason Reed <35282038+emesare@users.noreply.github.com>2026-03-24 18:46:48 -0700
commit3c88b11e5df33116580ac008e36092775df66135 (patch)
treecd64fbe149f05683ddabcccd0db7b87356f1f8cf /plugins/warp
parentf325aa7b6026a1daef84931baeb1f50d7da10c10 (diff)
[WARP] Improved UX and API
- Exposes WARP type objects directly - Adds processor API (for generating warp files directly) - Adds file and chunk API - Misc cleanup - Simplified the amount of commands - Replaced the "Create" commands with a purpose built processor dialog - Added a native QT viewer for WARP files - Simplified committing to a remote with a purpose built commit dialog
Diffstat (limited to 'plugins/warp')
-rw-r--r--plugins/warp/Cargo.toml8
-rw-r--r--plugins/warp/api/python/generator.cpp5
-rw-r--r--plugins/warp/api/python/warp.py176
-rw-r--r--plugins/warp/api/python/warp_enums.py13
-rw-r--r--plugins/warp/api/warp.cpp210
-rw-r--r--plugins/warp/api/warp.h92
-rw-r--r--plugins/warp/api/warpcore.h82
-rw-r--r--plugins/warp/build.rs4
-rw-r--r--plugins/warp/demo/Cargo.toml8
-rw-r--r--plugins/warp/demo/build.rs4
-rw-r--r--plugins/warp/examples/headless/src/main.rs11
-rw-r--r--plugins/warp/src/container.rs24
-rw-r--r--plugins/warp/src/lib.rs1
-rw-r--r--plugins/warp/src/plugin.rs77
-rw-r--r--plugins/warp/src/plugin/commit.rs149
-rw-r--r--plugins/warp/src/plugin/create.rs265
-rw-r--r--plugins/warp/src/plugin/debug.rs49
-rw-r--r--plugins/warp/src/plugin/ffi.rs3
-rw-r--r--plugins/warp/src/plugin/ffi/container.rs61
-rw-r--r--plugins/warp/src/plugin/ffi/file.rs137
-rw-r--r--plugins/warp/src/plugin/ffi/function.rs19
-rw-r--r--plugins/warp/src/plugin/ffi/processor.rs187
-rw-r--r--plugins/warp/src/plugin/ffi/ty.rs75
-rw-r--r--plugins/warp/src/plugin/file.rs41
-rw-r--r--plugins/warp/src/plugin/function.rs102
-rw-r--r--plugins/warp/src/plugin/project.rs286
-rw-r--r--plugins/warp/src/processor.rs345
-rw-r--r--plugins/warp/src/report.rs185
-rw-r--r--plugins/warp/src/templates/file.html37
-rw-r--r--plugins/warp/src/templates/file.json21
-rw-r--r--plugins/warp/src/templates/file.md15
-rw-r--r--plugins/warp/tests/processor.rs6
-rw-r--r--plugins/warp/ui/CMakeLists.txt8
-rw-r--r--plugins/warp/ui/containers.cpp195
-rw-r--r--plugins/warp/ui/containers.h94
-rw-r--r--plugins/warp/ui/matched.cpp11
-rw-r--r--plugins/warp/ui/matches.cpp35
-rw-r--r--plugins/warp/ui/plugin.cpp77
-rw-r--r--plugins/warp/ui/plugin.h2
-rw-r--r--plugins/warp/ui/shared/chunk.cpp159
-rw-r--r--plugins/warp/ui/shared/chunk.h28
-rw-r--r--plugins/warp/ui/shared/commitdialog.cpp141
-rw-r--r--plugins/warp/ui/shared/commitdialog.h74
-rw-r--r--plugins/warp/ui/shared/fetchdialog.cpp69
-rw-r--r--plugins/warp/ui/shared/fetchdialog.h2
-rw-r--r--plugins/warp/ui/shared/fetcher.cpp3
-rw-r--r--plugins/warp/ui/shared/fetcher.h1
-rw-r--r--plugins/warp/ui/shared/file.cpp71
-rw-r--r--plugins/warp/ui/shared/file.h25
-rw-r--r--plugins/warp/ui/shared/function.cpp7
-rw-r--r--plugins/warp/ui/shared/function.h7
-rw-r--r--plugins/warp/ui/shared/misc.cpp74
-rw-r--r--plugins/warp/ui/shared/misc.h22
-rw-r--r--plugins/warp/ui/shared/processordialog.cpp451
-rw-r--r--plugins/warp/ui/shared/processordialog.h127
-rw-r--r--plugins/warp/ui/shared/search.cpp22
-rw-r--r--plugins/warp/ui/shared/selectprojectfilesdialog.cpp141
-rw-r--r--plugins/warp/ui/shared/selectprojectfilesdialog.h26
-rw-r--r--plugins/warp/ui/shared/source.cpp204
-rw-r--r--plugins/warp/ui/shared/source.h116
60 files changed, 2886 insertions, 2004 deletions
diff --git a/plugins/warp/Cargo.toml b/plugins/warp/Cargo.toml
index 13516c50..553a0c8f 100644
--- a/plugins/warp/Cargo.toml
+++ b/plugins/warp/Cargo.toml
@@ -27,19 +27,11 @@ 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"
directories = "6.0"
compact_str = { version = "0.9.0", features = ["serde"] }
base64 = "0.22"
serde_qs = "0.15"
-# For reports
-minijinja = "2.10.2"
-minijinja-embed = "2.10.2"
-
-[build-dependencies]
-minijinja-embed = "2.10.2"
-
[dev-dependencies]
criterion = "0.6"
insta = { version = "1.42", features = ["yaml"] }
diff --git a/plugins/warp/api/python/generator.cpp b/plugins/warp/api/python/generator.cpp
index 0fd231fe..9e72c2ec 100644
--- a/plugins/warp/api/python/generator.cpp
+++ b/plugins/warp/api/python/generator.cpp
@@ -321,6 +321,11 @@ int main(int argc, char* argv[])
fprintf(out, "from binaryninja._binaryninjacore import BNType, BNTypeHandle\n");
continue;
}
+ if (name == "BNDataBuffer")
+ {
+ fprintf(out, "from binaryninja._binaryninjacore import BNDataBuffer, BNDataBufferHandle\n");
+ continue;
+ }
if (i.second->GetClass() == StructureTypeClass)
{
fprintf(out, "class %s(ctypes.Structure):\n", name.c_str());
diff --git a/plugins/warp/api/python/warp.py b/plugins/warp/api/python/warp.py
index 2ba1c681..b10b2c09 100644
--- a/plugins/warp/api/python/warp.py
+++ b/plugins/warp/api/python/warp.py
@@ -4,11 +4,11 @@ import uuid
from typing import List, Optional, Union
import binaryninja
-from binaryninja import BinaryView, Function, BasicBlock, Architecture, Platform, Type, Symbol, LowLevelILInstruction, LowLevelILFunction
+from binaryninja import BinaryView, Function, BasicBlock, Architecture, Platform, Type, Symbol, LowLevelILInstruction, LowLevelILFunction, DataBuffer, Project, ProjectFile
from binaryninja._binaryninjacore import BNFreeString, BNAllocString, BNType
from . import _warpcore as warpcore
-from .warp_enums import WARPContainerSearchItemKind
+from .warp_enums import WARPContainerSearchItemKind, WARPProcessorIncludedData, WARPProcessorIncludedFunctions
class WarpUUID:
@@ -74,6 +74,30 @@ class TypeGUID(WarpUUID):
return f"<TypeGUID '{str(self)}'>"
+class WarpType:
+ def __init__(self, handle: warpcore.BNWARPType):
+ self.handle = handle
+
+ def __del__(self):
+ if self.handle is not None:
+ warpcore.BNWARPFreeTypeReference(self.handle)
+
+ def __repr__(self):
+ return f"<WarpType name: '{self.name}' confidence: '{self.confidence}'>"
+
+ @property
+ def name(self) -> str:
+ return warpcore.BNWARPTypeGetName(self.handle)
+
+ @property
+ def confidence(self) -> int:
+ return warpcore.BNWARPTypeGetConfidence(self.handle)
+
+ def analysis_type(self, arch: Optional[Architecture] = None) -> Type:
+ if arch is None:
+ return Type.create(handle=warpcore.BNWARPTypeGetAnalysisType(None, self.handle))
+ return Type.create(handle=warpcore.BNWARPTypeGetAnalysisType(arch.handle, self.handle))
+
@dataclasses.dataclass
class WarpFunctionComment:
text: str
@@ -155,11 +179,12 @@ class WarpFunction:
symbol_handle = warpcore.BNWARPFunctionGetSymbol(self.handle, function.handle)
return Symbol(symbol_handle)
- def get_type(self, function: Function) -> Optional[Type]:
- type_handle = warpcore.BNWARPFunctionGetType(self.handle, function.handle)
+ @property
+ def type(self) -> Optional[WarpType]:
+ type_handle = warpcore.BNWARPFunctionGetType(self.handle)
if not type_handle:
return None
- return Type(type_handle)
+ return WarpType(type_handle)
@property
def constraints(self) -> List[WarpConstraint]:
@@ -259,11 +284,12 @@ class WarpContainerSearchItem:
def name(self) -> str:
return warpcore.BNWARPContainerSearchItemGetName(self.handle)
- def get_type(self, arch: Architecture) -> Optional[Type]:
- ty = warpcore.BNWARPContainerSearchItemGetType(arch.handle, self.handle)
+ @property
+ def type(self) -> Optional[WarpType]:
+ ty = warpcore.BNWARPContainerSearchItemGetType(self.handle)
if not ty:
return None
- return Type(ty)
+ return WarpType(ty)
@property
def function(self) -> Optional[WarpFunction]:
@@ -405,12 +431,12 @@ class WarpContainer(metaclass=_WarpContainerMetaclass):
core_funcs[i] = functions[i].handle
return warpcore.BNWARPContainerAddFunctions(self.handle, target.handle, source.uuid, core_funcs, count)
- def add_types(self, view: BinaryView, source: Source, types: List[Type]) -> bool:
+ def add_types(self, source: Source, types: List[WarpType]) -> bool:
count = len(types)
- core_types = (ctypes.POINTER(BNType) * count)()
+ core_types = (ctypes.POINTER(warpcore.BNWARPType) * count)()
for i in range(count):
core_types[i] = types[i].handle
- return warpcore.BNWARPContainerAddTypes(view.handle, self.handle, source.uuid, core_types, count)
+ return warpcore.BNWARPContainerAddTypes(self.handle, source.uuid, core_types, count)
def remove_functions(self, target: WarpTarget, source: Source, functions: List[Function]) -> bool:
count = len(functions)
@@ -479,11 +505,11 @@ class WarpContainer(metaclass=_WarpContainerMetaclass):
warpcore.BNWARPFreeFunctionList(funcs, count.value)
return result
- def get_type_with_guid(self, arch: Architecture, source: Source, guid: TypeGUID) -> Optional[Type]:
- ty = warpcore.BNWARPContainerGetTypeWithGUID(arch.handle, self.handle, source.uuid, guid.uuid)
+ def get_type_with_guid(self, source: Source, guid: TypeGUID) -> Optional[WarpType]:
+ ty = warpcore.BNWARPContainerGetTypeWithGUID(self.handle, source.uuid, guid.uuid)
if not ty:
return None
- return Type(ty)
+ return WarpType(ty)
def get_type_guids_with_name(self, source: Source, name: str) -> List[TypeGUID]:
count = ctypes.c_size_t()
@@ -503,6 +529,128 @@ class WarpContainer(metaclass=_WarpContainerMetaclass):
return WarpContainerResponse.from_api(response.contents)
+class WarpChunk:
+ def __init__(self, handle: warpcore.BNWARPChunk):
+ self.handle = handle
+
+ def __del__(self):
+ if self.handle is not None:
+ warpcore.BNWARPFreeChunkReference(self.handle)
+
+ def __repr__(self):
+ return f"<WarpChunk functions: '{len(self.functions)}' types: '{len(self.types)}'>"
+
+ @property
+ def functions(self) -> List[WarpFunction]:
+ count = ctypes.c_size_t()
+ funcs = warpcore.BNWARPChunkGetFunctions(self.handle, count)
+ if not funcs:
+ return []
+ result = []
+ for i in range(count.value):
+ result.append(WarpFunction(warpcore.BNWARPNewFunctionReference(funcs[i])))
+ warpcore.BNWARPFreeFunctionList(funcs, count.value)
+ return result
+
+ @property
+ def types(self) -> List[WarpType]:
+ count = ctypes.c_size_t()
+ types = warpcore.BNWARPChunkGetTypes(self.handle, count)
+ if not types:
+ return []
+ result = []
+ for i in range(count.value):
+ result.append(WarpType(warpcore.BNWARPNewTypeReference(types[i])))
+ warpcore.BNWARPFreeTypeList(types, count.value)
+ return result
+
+class WarpFile:
+ def __init__(self, handle: Union[warpcore.BNWARPFileHandle, str]):
+ if isinstance(handle, str):
+ self.handle = warpcore.BNWARPNewFileFromPath(handle)
+ else:
+ self.handle = handle
+
+ def __del__(self):
+ if self.handle is not None:
+ warpcore.BNWARPFreeFileReference(self.handle)
+
+ def __repr__(self):
+ return f"<WarpFile chunks: '{len(self.chunks)}'>"
+
+ @property
+ def chunks(self) -> List[WarpChunk]:
+ count = ctypes.c_size_t()
+ chunks = warpcore.BNWARPFileGetChunks(self.handle, count)
+ if not chunks:
+ return []
+ result = []
+ for i in range(count.value):
+ result.append(WarpChunk(warpcore.BNWARPNewChunkReference(chunks[i])))
+ warpcore.BNWARPFreeChunkList(chunks, count.value)
+ return result
+
+ def to_data_buffer(self) -> DataBuffer:
+ return DataBuffer(handle=warpcore.BNWARPFileToDataBuffer(self.handle))
+
+
+@dataclasses.dataclass
+class WarpProcessorState:
+ cancelled: bool = False
+ unprocessed_file_count: int = 0
+ processed_file_count: int = 0
+ analyzing_files: List[str] = dataclasses.field(default_factory=list)
+ processing_files: List[str] = dataclasses.field(default_factory=list)
+
+ @staticmethod
+ def from_api(state: warpcore.BNWARPProcessorState) -> 'WarpProcessorState':
+ analyzing_files = []
+ processing_files = []
+ for i in range(state.analyzing_files_count):
+ analyzing_files.append(state.analyzing_files[i])
+ for i in range(state.processing_files_count):
+ processing_files.append(state.processing_files[i])
+ return WarpProcessorState(
+ cancelled=state.cancelled,
+ unprocessed_file_count=state.unprocessed_file_count,
+ processed_file_count=state.processed_file_count,
+ analyzing_files=analyzing_files,
+ processing_files=processing_files
+ )
+
+class WarpProcessor:
+ def __init__(self, included_data: WARPProcessorIncludedData = WARPProcessorIncludedData.WARPProcessorIncludedDataAll,
+ included_functions: WARPProcessorIncludedFunctions = WARPProcessorIncludedFunctions.WARPProcessorIncludedFunctionsAnnotated,
+ worker_count: int = 1):
+ self.handle = warpcore.BNWARPNewProcessor(ctypes.c_int(included_data), ctypes.c_int(included_functions), worker_count)
+
+ def __del__(self):
+ if self.handle is not None:
+ warpcore.BNWARPFreeProcessor(self.handle)
+
+ def add_path(self, path: str):
+ warpcore.BNWARPProcessorAddPath(self.handle, path)
+
+ def add_project(self, project: Project):
+ warpcore.BNWARPProcessorAddProject(self.handle, project.handle)
+
+ def add_project_file(self, project_file: ProjectFile):
+ warpcore.BNWARPProcessorAddProjectFile(self.handle, project_file.handle)
+
+ def add_binary_view(self, view: BinaryView):
+ warpcore.BNWARPProcessorAddBinaryView(self.handle, view.handle)
+
+ def start(self) -> Optional[WarpFile]:
+ file = warpcore.BNWARPProcessorStart(self.handle)
+ if not file:
+ return None
+ return WarpFile(file)
+
+ def state(self) -> WarpProcessorState:
+ state_raw = warpcore.BNWARPProcessorGetState(self.handle)
+ warpcore.BNWARPFreeProcessorState(state_raw)
+ return WarpProcessorState.from_api(state_raw)
+
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 86be3a6c..d3f9f393 100644
--- a/plugins/warp/api/python/warp_enums.py
+++ b/plugins/warp/api/python/warp_enums.py
@@ -6,3 +6,16 @@ class WARPContainerSearchItemKind(enum.IntEnum):
WARPContainerSearchItemKindFunction = 1
WARPContainerSearchItemKindType = 2
WARPContainerSearchItemKindSymbol = 3
+
+
+class WARPProcessorIncludedData(enum.IntEnum):
+ WARPProcessorIncludedDataSymbols = 0
+ WARPProcessorIncludedDataSignatures = 1
+ WARPProcessorIncludedDataTypes = 2
+ WARPProcessorIncludedDataAll = 3
+
+
+class WARPProcessorIncludedFunctions(enum.IntEnum):
+ WARPProcessorIncludedFunctionsSelected = 0
+ WARPProcessorIncludedFunctionsAnnotated = 1
+ WARPProcessorIncludedFunctionsAll = 2
diff --git a/plugins/warp/api/warp.cpp b/plugins/warp/api/warp.cpp
index debbbc00..8c932602 100644
--- a/plugins/warp/api/warp.cpp
+++ b/plugins/warp/api/warp.cpp
@@ -34,6 +34,41 @@ Ref<Target> Target::FromPlatform(const BinaryNinja::Platform &platform)
return new Target(result);
}
+Type::Type(BNWARPType *type)
+{
+ m_object = type;
+}
+
+std::optional<std::string> Type::GetName() const
+{
+ char *name = BNWARPTypeGetName(m_object);
+ if (!name)
+ return std::nullopt;
+ std::string result = name;
+ BNFreeString(name);
+ return result;
+}
+
+uint8_t Type::GetConfidence() const
+{
+ return BNWARPTypeGetConfidence(m_object);
+}
+
+Ref<Type> Type::FromAnalysisType(const BinaryNinja::Type &type, uint8_t confidence)
+{
+ BNWARPType* ty = BNWARPGetType(type.m_object, confidence);
+ // TODO: Assert always should convert.
+ return new Type(ty);
+}
+
+BinaryNinja::Ref<BinaryNinja::Type> Type::GetAnalysisType(BinaryNinja::Architecture* arch) const
+{
+ BNType* ty = BNWARPTypeGetAnalysisType(arch ? arch->m_object : nullptr, m_object);
+ if (!ty)
+ return nullptr;
+ return new BinaryNinja::Type(ty);
+}
+
Constraint::Constraint(ConstraintGUID guid, std::optional<int64_t> offset)
{
this->guid = guid;
@@ -83,17 +118,17 @@ BinaryNinja::Ref<BinaryNinja::Symbol> Function::GetSymbol(const BinaryNinja::Fun
return new BinaryNinja::Symbol(symbol);
}
-BinaryNinja::Ref<BinaryNinja::Type> Function::GetType(const BinaryNinja::Function &function) const
+Ref<Type> Function::GetType() const
{
- BNType *type = BNWARPFunctionGetType(m_object, function.m_object);
+ BNWARPType *type = BNWARPFunctionGetType(m_object);
if (!type)
return nullptr;
- return new BinaryNinja::Type(type);
+ return new Type(type);
}
std::vector<Constraint> Function::GetConstraints() const
{
- size_t count;
+ size_t count = 0;
BNWARPConstraint *constraints = BNWARPFunctionGetConstraints(m_object, &count);
std::vector<Constraint> result;
result.reserve(count);
@@ -105,7 +140,7 @@ std::vector<Constraint> Function::GetConstraints() const
std::vector<FunctionComment> Function::GetComments() const
{
- size_t count;
+ size_t count = 0;
BNWARPFunctionComment *comments = BNWARPFunctionGetComments(m_object, &count);
std::vector<FunctionComment> result;
result.reserve(count);
@@ -184,12 +219,12 @@ Source ContainerSearchItem::GetSource() const
return BNWARPContainerSearchItemGetSource(m_object);
}
-BinaryNinja::Ref<BinaryNinja::Type> ContainerSearchItem::GetType(const BinaryNinja::Ref<BinaryNinja::Architecture> &arch) const
+Ref<Type> ContainerSearchItem::GetType() const
{
- BNType *type = BNWARPContainerSearchItemGetType(arch ? arch->m_object : nullptr, m_object);
+ BNWARPType *type = BNWARPContainerSearchItemGetType(m_object);
if (!type)
return nullptr;
- return new BinaryNinja::Type(type);
+ return new Type(type);
}
std::string ContainerSearchItem::GetName() const
@@ -236,7 +271,7 @@ Container::Container(BNWARPContainer *container)
std::vector<Ref<Container> > Container::All()
{
- size_t count;
+ size_t count = 0;
BNWARPContainer **containers = BNWARPGetContainers(&count);
std::vector<Ref<Container> > result;
result.reserve(count);
@@ -264,7 +299,7 @@ std::string Container::GetName() const
std::vector<Source> Container::GetSources() const
{
- size_t count;
+ size_t count = 0;
BNWARPSource *sources = BNWARPContainerGetSources(m_object, &count);
std::vector<Source> result;
result.reserve(count);
@@ -318,14 +353,13 @@ bool Container::AddFunctions(const Target &target, const Source &source, const s
return result;
}
-bool Container::AddTypes(const BinaryNinja::BinaryView &view, const Source &source,
- const std::vector<BinaryNinja::Ref<BinaryNinja::Type> > &types) const
+bool Container::AddTypes(const Source &source, const std::vector<Ref<Type>> &types) const
{
size_t count = types.size();
- BNType **apiTypes = new BNType *[count];
+ BNWARPType **apiTypes = new BNWARPType *[count];
for (size_t i = 0; i < count; i++)
apiTypes[i] = types[i]->m_object;
- const bool result = BNWARPContainerAddTypes(view.m_object, m_object, source.Raw(), apiTypes, count);
+ const bool result = BNWARPContainerAddTypes(m_object, source.Raw(), apiTypes, count);
delete[] apiTypes;
return result;
}
@@ -375,7 +409,7 @@ void Container::FetchFunctions(const Target &target, const std::vector<FunctionG
std::vector<Source> Container::GetSourcesWithFunctionGUID(const Target& target, const FunctionGUID &guid) const
{
- size_t count;
+ size_t count = 0;
BNWARPSource *sources = BNWARPContainerGetSourcesWithFunctionGUID(m_object, target.m_object, guid.Raw(), &count);
std::vector<Source> result;
result.reserve(count);
@@ -387,7 +421,7 @@ std::vector<Source> Container::GetSourcesWithFunctionGUID(const Target& target,
std::vector<Source> Container::GetSourcesWithTypeGUID(const TypeGUID &guid) const
{
- size_t count;
+ size_t count = 0;
BNWARPSource *sources = BNWARPContainerGetSourcesWithTypeGUID(m_object, guid.Raw(), &count);
std::vector<Source> result;
result.reserve(count);
@@ -399,7 +433,7 @@ std::vector<Source> Container::GetSourcesWithTypeGUID(const TypeGUID &guid) cons
std::vector<Ref<Function> > Container::GetFunctionsWithGUID(const Target& target, const Source &source, const FunctionGUID &guid) const
{
- size_t count;
+ size_t count = 0;
BNWARPFunction **functions = BNWARPContainerGetFunctionsWithGUID(m_object, target.m_object, source.Raw(), guid.Raw(), &count);
std::vector<Ref<Function> > result;
result.reserve(count);
@@ -409,16 +443,15 @@ std::vector<Ref<Function> > Container::GetFunctionsWithGUID(const Target& target
return result;
}
-BinaryNinja::Ref<BinaryNinja::Type> Container::GetTypeWithGUID(const BinaryNinja::Architecture &arch,
- const Source &source, const TypeGUID &guid) const
+Ref<Type> Container::GetTypeWithGUID(const Source &source, const TypeGUID &guid) const
{
- BNType *type = BNWARPContainerGetTypeWithGUID(arch.m_object, m_object, source.Raw(), guid.Raw());
- return new BinaryNinja::Type(type);
+ BNWARPType *type = BNWARPContainerGetTypeWithGUID(m_object, source.Raw(), guid.Raw());
+ return new Type(type);
}
std::vector<TypeGUID> Container::GetTypeGUIDsWithName(const Source &source, const std::string &name) const
{
- size_t count;
+ size_t count = 0;
BNWARPTypeGUID *guids = BNWARPContainerGetTypeGUIDsWithName(m_object, source.Raw(), name.c_str(), &count);
std::vector<TypeGUID> result;
result.reserve(count);
@@ -436,6 +469,139 @@ std::optional<ContainerSearchResponse> Container::Search(const ContainerSearchQu
return ContainerSearchResponse::FromAPIObject(response);
}
+Chunk::Chunk(BNWARPChunk *chunk)
+{
+ m_object = chunk;
+}
+
+Ref<Target> Chunk::GetTarget() const
+{
+ BNWARPTarget *target = BNWARPChunkGetTarget(m_object);
+ if (!target)
+ return nullptr;
+ return new Target(target);
+}
+
+std::vector<Ref<Function>> Chunk::GetFunctions() const
+{
+ size_t count = 0;
+ BNWARPFunction** functions = BNWARPChunkGetFunctions(m_object, &count);
+ std::vector<Ref<Function>> result;
+ result.reserve(count);
+ for (size_t i = 0; i < count; i++)
+ result.push_back(new Function(BNWARPNewFunctionReference(functions[i])));
+ BNWARPFreeFunctionList(functions, count);
+ return result;
+}
+
+std::vector<Ref<Type>> Chunk::GetTypes() const
+{
+ size_t count = 0;
+ BNWARPType** types = BNWARPChunkGetTypes(m_object, &count);
+ std::vector<Ref<Type>> result;
+ result.reserve(count);
+ for (size_t i = 0; i < count; i++)
+ result.push_back(new Type(BNWARPNewTypeReference(types[i])));
+ BNWARPFreeTypeList(types, count);
+ return result;
+}
+
+File::File(BNWARPFile *file)
+{
+ m_object = file;
+}
+
+Ref<File> File::FromPath(const std::string &path)
+{
+ BNWARPFile *result = BNWARPNewFileFromPath(path.c_str());
+ if (!result)
+ return nullptr;
+ return new File(result);
+}
+
+std::vector<Ref<Chunk>> File::GetChunks() const
+{
+ size_t count = 0;
+ BNWARPChunk **chunks = BNWARPFileGetChunks(m_object, &count);
+ std::vector<Ref<Chunk>> result;
+ result.reserve(count);
+ for (int i = 0; i < count; i++)
+ result.push_back(new Chunk(BNWARPNewChunkReference(chunks[i])));
+ BNWARPFreeChunkList(chunks, count);
+ return result;
+}
+
+BinaryNinja::DataBuffer File::ToDataBuffer() const
+{
+ return BinaryNinja::DataBuffer(BNWARPFileToDataBuffer(m_object));
+}
+
+ProcessorState ProcessorState::FromAPIObject(BNWARPProcessorState *state)
+{
+ ProcessorState result;
+ result.cancelled = state->cancelled;
+ result.unprocessedFilesCount = state->unprocessedFilesCount;
+ result.processedFilesCount = state->processedFilesCount;
+ result.analyzingFiles.reserve(state->analyzingFilesCount);
+ for (size_t i = 0; i < state->analyzingFilesCount; ++i)
+ result.analyzingFiles.emplace_back(state->analyzingFiles[i]);
+ result.processingFiles.reserve(state->processingFilesCount);
+ for (size_t i = 0; i < state->processingFilesCount; ++i)
+ result.processingFiles.emplace_back(state->processingFiles[i]);
+ return result;
+}
+
+Processor::Processor(BNWARPProcessorIncludedData includedData, BNWARPProcessorIncludedFunctions includedFunctions, size_t workerCount)
+{
+ m_object = BNWARPNewProcessor(includedData, includedFunctions, workerCount);
+}
+
+Processor::~Processor()
+{
+ BNWARPFreeProcessor(m_object);
+}
+
+void Processor::AddPath(const std::string &path) const
+{
+ BNWARPProcessorAddPath(m_object, path.c_str());
+}
+
+void Processor::AddProject(const BinaryNinja::Project &project) const
+{
+ BNWARPProcessorAddProject(m_object, project.m_object);
+}
+
+void Processor::AddProjectFile(const BinaryNinja::ProjectFile &projectFile) const
+{
+ BNWARPProcessorAddProjectFile(m_object, projectFile.m_object);
+}
+
+void Processor::AddBinaryView(const BinaryNinja::BinaryView &view) const
+{
+ BNWARPProcessorAddBinaryView(m_object, view.m_object);
+}
+
+Ref<File> Processor::Start() const
+{
+ BNWARPFile* file = BNWARPProcessorStart(m_object);
+ if (!file)
+ return nullptr;
+ return new File(file);
+}
+
+void Processor::Cancel() const
+{
+ BNWARPProcessorCancel(m_object);
+}
+
+ProcessorState Processor::GetState() const
+{
+ BNWARPProcessorState stateRaw = BNWARPProcessorGetState(m_object);
+ ProcessorState state = ProcessorState::FromAPIObject(&stateRaw);
+ BNWARPFreeProcessorState(stateRaw);
+ return state;
+}
+
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 ccb4da97..42e8ac70 100644
--- a/plugins/warp/api/warp.h
+++ b/plugins/warp/api/warp.h
@@ -276,9 +276,23 @@ namespace Warp {
static Ref<Target> FromPlatform(const BinaryNinja::Platform &platform);
};
+ class Type : public WarpRefCountObject<BNWARPType, BNWARPNewTypeReference,
+ BNWARPFreeTypeReference>
+ {
+ public:
+ explicit Type(BNWARPType *type);
+
+ [[nodiscard]] static Ref<Type> FromAnalysisType(const BinaryNinja::Type &type, uint8_t confidence);
+
+ std::optional<std::string> GetName() const;
+ uint8_t GetConfidence() const;
+
+ [[nodiscard]] BinaryNinja::Ref<BinaryNinja::Type> GetAnalysisType(BinaryNinja::Architecture* arch = nullptr) const;
+ };
+
struct Constraint
{
- ConstraintGUID guid;
+ ConstraintGUID guid {};
std::optional<int64_t> offset;
Constraint(ConstraintGUID guid, std::optional<int64_t> offset);
@@ -312,7 +326,7 @@ namespace Warp {
BinaryNinja::Ref<BinaryNinja::Symbol> GetSymbol(const BinaryNinja::Function &function) const;
- BinaryNinja::Ref<BinaryNinja::Type> GetType(const BinaryNinja::Function &function) const;
+ Ref<Type> GetType() const;
std::vector<Constraint> GetConstraints() const;
@@ -354,7 +368,7 @@ namespace Warp {
Source GetSource() const;
- BinaryNinja::Ref<BinaryNinja::Type> GetType(const BinaryNinja::Ref<BinaryNinja::Architecture> &arch) const;
+ Ref<Type> GetType() const;
std::string GetName() const;
@@ -363,7 +377,7 @@ namespace Warp {
struct ContainerSearchResponse
{
- std::vector<Ref<ContainerSearchItem> > items;
+ std::vector<Ref<ContainerSearchItem>> items;
size_t offset;
size_t total;
@@ -379,7 +393,7 @@ namespace Warp {
explicit Container(BNWARPContainer *container);
/// Retrieve all available containers.
- static std::vector<Ref<Container> > All();
+ static std::vector<Ref<Container>> All();
/// Add a new container with the given name.
static Ref<Container> Add(const std::string &name);
@@ -399,13 +413,12 @@ 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;
+ 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 AddTypes(const Source &source, const std::vector<Ref<Type>> &types) const;
bool RemoveFunctions(const Target &target, const Source &source,
- const std::vector<Ref<Function> > &functions) const;
+ const std::vector<Ref<Function>> &functions) const;
bool RemoveTypes(const Source &source, const std::vector<TypeGUID> &guids) const;
@@ -418,14 +431,71 @@ namespace Warp {
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;
+ Ref<Type> GetTypeWithGUID(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;
};
+ class Chunk : public WarpRefCountObject<BNWARPChunk, BNWARPNewChunkReference, BNWARPFreeChunkReference>
+ {
+ friend class File;
+
+ public:
+ explicit Chunk(BNWARPChunk *chunk);
+
+ Ref<Target> GetTarget() const;
+
+ [[nodiscard]] std::vector<Ref<Function>> GetFunctions() const;
+ [[nodiscard]] std::vector<Ref<Type>> GetTypes() const;
+ };
+
+ class File : public WarpRefCountObject<BNWARPFile, BNWARPNewFileReference, BNWARPFreeFileReference>
+ {
+ public:
+ explicit File(BNWARPFile *file);
+
+ static Ref<File> FromPath(const std::string &path);
+
+ [[nodiscard]] std::vector<Ref<Chunk>> GetChunks() const;
+ [[nodiscard]] BinaryNinja::DataBuffer ToDataBuffer() const;
+ };
+
+ class ProcessorState
+ {
+ public:
+ std::vector<std::string> analyzingFiles;
+ std::vector<std::string> processingFiles;
+ bool cancelled;
+ size_t unprocessedFilesCount;
+ size_t processedFilesCount;
+
+ ProcessorState() = default;
+
+ static ProcessorState FromAPIObject(BNWARPProcessorState *state);
+ };
+
+ class Processor
+ {
+ BNWARPProcessor* m_object;
+ public:
+ explicit Processor(BNWARPProcessorIncludedData includedData,
+ BNWARPProcessorIncludedFunctions includedFunctions, size_t workerCount);
+
+ ~Processor();
+
+ void AddPath(const std::string &path) const;
+ void AddProject(const BinaryNinja::Project &project) const;
+ void AddProjectFile(const BinaryNinja::ProjectFile &projectFile) const;
+ void AddBinaryView(const BinaryNinja::BinaryView &view) const;
+
+ Ref<File> Start() const;
+ void Cancel() const;
+
+ ProcessorState GetState() const;
+ };
+
void RunMatcher(const BinaryNinja::BinaryView &view);
bool IsInstructionVariant(const BinaryNinja::LowLevelILFunction &function, BinaryNinja::ExprId idx);
diff --git a/plugins/warp/api/warpcore.h b/plugins/warp/api/warpcore.h
index 21ef105b..c1762ae6 100644
--- a/plugins/warp/api/warpcore.h
+++ b/plugins/warp/api/warpcore.h
@@ -48,6 +48,9 @@ extern "C"
typedef struct BNFunction BNFunction;
typedef struct BNSymbol BNSymbol;
typedef struct BNType BNType;
+ typedef struct BNDataBuffer BNDataBuffer;
+ typedef struct BNProject BNProject;
+ typedef struct BNProjectFile BNProjectFile;
struct BNWARPUUID
{
@@ -71,20 +74,39 @@ extern "C"
typedef BNWARPUUID BNWARPFunctionGUID;
typedef BNWARPUUID BNWARPTypeGUID;
+ typedef struct BNWARPProcessor BNWARPProcessor;
+ typedef struct BNWARPFile BNWARPFile;
+ typedef struct BNWARPChunk BNWARPChunk;
typedef struct BNWARPTarget BNWARPTarget;
typedef struct BNWARPContainer BNWARPContainer;
typedef struct BNWARPFunction BNWARPFunction;
+ typedef struct BNWARPType BNWARPType;
typedef struct BNWARPConstraint BNWARPConstraint;
typedef struct BNWARPContainerSearchQuery BNWARPContainerSearchQuery;
typedef struct BNWARPContainerSearchItem BNWARPContainerSearchItem;
- enum BNWARPContainerSearchItemKind
+ typedef enum BNWARPProcessorIncludedData : uint8_t
+ {
+ WARPProcessorIncludedDataSymbols = 0,
+ WARPProcessorIncludedDataSignatures = 1,
+ WARPProcessorIncludedDataTypes = 2,
+ WARPProcessorIncludedDataAll = 3,
+ } BNWARPProcessorIncludedData;
+
+ typedef enum BNWARPProcessorIncludedFunctions : uint8_t
+ {
+ WARPProcessorIncludedFunctionsSelected = 0,
+ WARPProcessorIncludedFunctionsAnnotated = 1,
+ WARPProcessorIncludedFunctionsAll = 2,
+ } BNWARPProcessorIncludedFunctions;
+
+ typedef enum BNWARPContainerSearchItemKind
{
WARPContainerSearchItemKindSource = 0,
WARPContainerSearchItemKindFunction = 1,
WARPContainerSearchItemKindType = 2,
WARPContainerSearchItemKindSymbol = 3,
- };
+ } BNWARPContainerSearchItemKind;
struct BNWARPContainerSearchResponse
{
@@ -99,6 +121,28 @@ extern "C"
BNWARPConstraintGUID guid;
int64_t offset;
};
+
+ struct BNWARPProcessorState
+ {
+ bool cancelled;
+ size_t unprocessedFilesCount;
+ size_t processedFilesCount;
+ char** analyzingFiles;
+ size_t analyzingFilesCount;
+ char** processingFiles;
+ size_t processingFilesCount;
+ };
+
+ WARP_FFI_API BNWARPProcessor* BNWARPNewProcessor(BNWARPProcessorIncludedData includedData, BNWARPProcessorIncludedFunctions includedFunctions, size_t workerCount);
+ WARP_FFI_API void BNWARPProcessorAddPath(BNWARPProcessor* processor, const char* path);
+ WARP_FFI_API void BNWARPProcessorAddProject(BNWARPProcessor* processor, BNProject* project);
+ WARP_FFI_API void BNWARPProcessorAddProjectFile(BNWARPProcessor* processor, BNProjectFile* projectFile);
+ WARP_FFI_API void BNWARPProcessorAddBinaryView(BNWARPProcessor* processor, BNBinaryView* view);
+ WARP_FFI_API BNWARPFile* BNWARPProcessorStart(BNWARPProcessor* processor);
+ WARP_FFI_API void BNWARPProcessorCancel(BNWARPProcessor* processor);
+ WARP_FFI_API BNWARPProcessorState BNWARPProcessorGetState(BNWARPProcessor* processor);
+ WARP_FFI_API void BNWARPFreeProcessor(BNWARPProcessor* processor);
+ WARP_FFI_API void BNWARPFreeProcessorState(BNWARPProcessorState processorState);
WARP_FFI_API void BNWARPRunMatcher(BNBinaryView* view);
@@ -123,7 +167,7 @@ extern "C"
WARP_FFI_API char* BNWARPContainerGetSourcePath(BNWARPContainer* container, const BNWARPSource* source);
WARP_FFI_API bool BNWARPContainerAddFunctions(BNWARPContainer* container, const BNWARPTarget* target, const BNWARPSource* source, BNWARPFunction** functions, size_t count);
- WARP_FFI_API bool BNWARPContainerAddTypes(BNBinaryView* view, BNWARPContainer* container, const BNWARPSource* source, BNType** types, size_t count);
+ WARP_FFI_API bool BNWARPContainerAddTypes(BNWARPContainer* container, const BNWARPSource* source, BNWARPType** types, size_t count);
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);
@@ -133,7 +177,7 @@ extern "C"
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);
WARP_FFI_API BNWARPFunction** BNWARPContainerGetFunctionsWithGUID(BNWARPContainer* container, const BNWARPTarget* target, const BNWARPSource* source, const BNWARPFunctionGUID* guid, size_t* count);
- WARP_FFI_API BNType* BNWARPContainerGetTypeWithGUID(BNArchitecture* arch, BNWARPContainer* container, const BNWARPSource* source, const BNWARPTypeGUID* guid);
+ WARP_FFI_API BNWARPType* BNWARPContainerGetTypeWithGUID(BNWARPContainer* container, const BNWARPSource* source, const BNWARPTypeGUID* guid);
WARP_FFI_API BNWARPTypeGUID* BNWARPContainerGetTypeGUIDsWithName(BNWARPContainer* container, const BNWARPSource* source, const char* name, size_t* count);
WARP_FFI_API BNWARPContainer* BNWARPNewContainerReference(BNWARPContainer* container);
@@ -146,7 +190,7 @@ extern "C"
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 BNWARPType* BNWARPContainerSearchItemGetType(BNWARPContainerSearchItem* item);
WARP_FFI_API char* BNWARPContainerSearchItemGetName(BNWARPContainerSearchItem* item);
WARP_FFI_API BNWARPFunction* BNWARPContainerSearchItemGetFunction(BNWARPContainerSearchItem* item);
@@ -163,7 +207,7 @@ extern "C"
WARP_FFI_API BNWARPFunctionGUID BNWARPFunctionGetGUID(BNWARPFunction* function);
WARP_FFI_API BNSymbol* BNWARPFunctionGetSymbol(BNWARPFunction* function, BNFunction* analysisFunction);
WARP_FFI_API char* BNWARPFunctionGetSymbolName(BNWARPFunction* function);
- WARP_FFI_API BNType* BNWARPFunctionGetType(BNWARPFunction* function, BNFunction* analysisFunction);
+ WARP_FFI_API BNWARPType* BNWARPFunctionGetType(BNWARPFunction* function);
WARP_FFI_API BNWARPConstraint* BNWARPFunctionGetConstraints(BNWARPFunction* function, size_t* count);
WARP_FFI_API BNWARPFunctionComment* BNWARPFunctionGetComments(BNWARPFunction* function, size_t* count);
WARP_FFI_API bool BNWARPFunctionsEqual(BNWARPFunction* functionA, BNWARPFunction* functionB);
@@ -175,11 +219,37 @@ extern "C"
WARP_FFI_API void BNWARPFreeFunctionReference(BNWARPFunction* function);
WARP_FFI_API void BNWARPFreeFunctionList(BNWARPFunction** functions, size_t count);
+ WARP_FFI_API BNWARPType* BNWARPGetType(BNType* analysisType, uint8_t confidence);
+ WARP_FFI_API char* BNWARPTypeGetName(BNWARPType* ty);
+ WARP_FFI_API uint8_t BNWARPTypeGetConfidence(BNWARPType* ty);
+ WARP_FFI_API BNType* BNWARPTypeGetAnalysisType(BNArchitecture* arch, BNWARPType* ty);
+
+ WARP_FFI_API BNWARPType* BNWARPNewTypeReference(BNWARPType* ty);
+ WARP_FFI_API void BNWARPFreeTypeReference(BNWARPType* ty);
+
WARP_FFI_API BNWARPTarget* BNWARPGetTarget(BNPlatform* platform);
WARP_FFI_API BNWARPTarget* BNWARPNewTargetReference(BNWARPTarget* target);
WARP_FFI_API void BNWARPFreeTargetReference(BNWARPTarget* target);
+ WARP_FFI_API BNWARPFile* BNWARPNewFileFromPath(const char* path);
+
+ WARP_FFI_API BNWARPChunk** BNWARPFileGetChunks(BNWARPFile* file, size_t* count);
+ WARP_FFI_API BNDataBuffer* BNWARPFileToDataBuffer(BNWARPFile* file);
+
+ WARP_FFI_API BNWARPTarget* BNWARPChunkGetTarget(BNWARPChunk* chunk);
+ WARP_FFI_API BNWARPFunction** BNWARPChunkGetFunctions(BNWARPChunk* chunk, size_t* count);
+ WARP_FFI_API BNWARPType** BNWARPChunkGetTypes(BNWARPChunk* chunk, size_t* count);
+
+ WARP_FFI_API BNWARPFile* BNWARPNewFileReference(BNWARPFile* file);
+ WARP_FFI_API void BNWARPFreeFileReference(BNWARPFile* file);
+
+ WARP_FFI_API BNWARPChunk* BNWARPNewChunkReference(BNWARPChunk* chunk);
+ WARP_FFI_API void BNWARPFreeChunkReference(BNWARPChunk* chunk);
+
+ WARP_FFI_API void BNWARPFreeChunkList(BNWARPChunk** chunks, size_t count);
+ WARP_FFI_API void BNWARPFreeTypeList(BNWARPType** types, size_t count);
+
#ifdef __cplusplus
}
#endif
diff --git a/plugins/warp/build.rs b/plugins/warp/build.rs
index ba8cbb65..25546941 100644
--- a/plugins/warp/build.rs
+++ b/plugins/warp/build.rs
@@ -42,8 +42,4 @@ fn main() {
}
}
}
-
- println!("cargo::rerun-if-changed=src/templates");
- // Templates used for rendering reports.
- minijinja_embed::embed_templates!("src/templates");
}
diff --git a/plugins/warp/demo/Cargo.toml b/plugins/warp/demo/Cargo.toml
index 662b4131..7c9babad 100644
--- a/plugins/warp/demo/Cargo.toml
+++ b/plugins/warp/demo/Cargo.toml
@@ -27,19 +27,11 @@ 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"
directories = "6.0"
compact_str = { version = "0.9.0", features = ["serde"] }
base64 = "0.22"
serde_qs = "0.15"
-# For reports
-minijinja = "2.10.2"
-minijinja-embed = "2.10.2"
-
-[build-dependencies]
-minijinja-embed = "2.10.2"
-
[dev-dependencies]
criterion = "0.6"
insta = { version = "1.42", features = ["yaml"] }
diff --git a/plugins/warp/demo/build.rs b/plugins/warp/demo/build.rs
index d0912f90..4940fe76 100644
--- a/plugins/warp/demo/build.rs
+++ b/plugins/warp/demo/build.rs
@@ -22,8 +22,4 @@ fn main() {
lib_name
);
}
-
- println!("cargo::rerun-if-changed=../src/templates");
- // Templates used for rendering reports.
- minijinja_embed::embed_templates!("../src/templates");
}
diff --git a/plugins/warp/examples/headless/src/main.rs b/plugins/warp/examples/headless/src/main.rs
index 29d48f65..2040f84a 100644
--- a/plugins/warp/examples/headless/src/main.rs
+++ b/plugins/warp/examples/headless/src/main.rs
@@ -11,9 +11,8 @@ use tracing_indicatif::style::ProgressStyle;
use tracing_indicatif::IndicatifLayer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
-use warp_ninja::processor::{
- CompressionTypeField, ProcessingFileState, ProcessingState, WarpFileProcessor,
-};
+use warp_ninja::processor::{ProcessingFileState, ProcessingState, WarpFileProcessor};
+use warp_ninja::warp::chunk::CompressionType;
use warp_ninja::warp::WarpFile;
/// Generate WARP files using Binary Ninja
@@ -67,8 +66,8 @@ fn main() {
let args = Args::parse();
let compression_ty = match args.compressed {
- true => CompressionTypeField::Zstd,
- false => CompressionTypeField::None,
+ true => CompressionType::Zstd,
+ false => CompressionType::None,
};
let mut processor = WarpFileProcessor::new()
.with_skip_warp_files(args.skip_warp_files)
@@ -94,7 +93,7 @@ fn main() {
let outputs: HashMap<PathBuf, WarpFile<'static>> = args
.input
.into_iter()
- .filter_map(|i| match processor.process(i.clone()) {
+ .filter_map(|i| match processor.process_path(i.clone()) {
Ok(o) => Some((i, o)),
Err(err) => {
tracing::error!("{}", err);
diff --git a/plugins/warp/src/container.rs b/plugins/warp/src/container.rs
index 4feed24c..bb5d7fd4 100644
--- a/plugins/warp/src/container.rs
+++ b/plugins/warp/src/container.rs
@@ -7,12 +7,14 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use thiserror::Error;
use uuid::Uuid;
+use warp::chunk::{Chunk, ChunkKind};
use warp::r#type::guid::TypeGUID;
use warp::r#type::{ComputedType, Type};
use warp::signature::constraint::ConstraintGUID;
use warp::signature::function::{Function, FunctionGUID};
use warp::symbol::Symbol;
use warp::target::Target;
+use warp::WarpFile;
pub mod disk;
pub mod memory;
@@ -291,6 +293,28 @@ pub trait Container: Send + Sync + Display + Debug {
functions: &[Function],
) -> ContainerResult<()>;
+ /// Add the `chunk`s data to the provided `source` if it exists and is writable.
+ fn add_chunk(&mut self, source: &SourceId, chunk: &Chunk) -> ContainerResult<()> {
+ match &chunk.kind {
+ ChunkKind::Signature(sc) => {
+ let functions: Vec<_> = sc.functions().collect();
+ self.add_functions(&chunk.header.target, source, &functions)
+ }
+ ChunkKind::Type(tc) => {
+ let types: Vec<_> = tc.types().collect();
+ self.add_computed_types(source, &types)
+ }
+ }
+ }
+
+ /// Add the `file` data to the provided `source` if it exists and is writable.
+ fn add_file(&mut self, source: &SourceId, file: &WarpFile) -> ContainerResult<()> {
+ for chunk in &file.chunks {
+ self.add_chunk(source, chunk)?;
+ }
+ Ok(())
+ }
+
/// Fetches WARP information for the associated functions.
///
/// Typically, a container that resides only in memory has nothing to fetch, so the default implementation
diff --git a/plugins/warp/src/lib.rs b/plugins/warp/src/lib.rs
index 6889c79d..e0aaeed3 100644
--- a/plugins/warp/src/lib.rs
+++ b/plugins/warp/src/lib.rs
@@ -35,7 +35,6 @@ pub mod container;
pub mod convert;
pub mod matcher;
pub mod processor;
-pub mod report;
/// Only used when compiled for cdylib target.
mod plugin;
diff --git a/plugins/warp/src/plugin.rs b/plugins/warp/src/plugin.rs
index d163205b..8192102d 100644
--- a/plugins/warp/src/plugin.rs
+++ b/plugins/warp/src/plugin.rs
@@ -9,27 +9,17 @@ use crate::plugin::render_layer::HighlightRenderLayer;
use crate::plugin::settings::PluginSettings;
use crate::{core_signature_dir, user_signature_dir};
use binaryninja::background_task::BackgroundTask;
-use binaryninja::command::{
- register_command, register_command_for_function, register_command_for_project,
- register_global_command,
-};
+use binaryninja::command::{register_command, register_command_for_function};
use binaryninja::is_ui_enabled;
use binaryninja::settings::{QueryOptions, Settings};
-mod commit;
-mod create;
mod ffi;
-mod file;
mod function;
mod load;
-mod project;
mod render_layer;
mod settings;
mod workflow;
-#[cfg(debug_assertions)]
-mod debug;
-
fn load_bundled_signatures() {
let global_bn_settings = Settings::new();
let plugin_settings =
@@ -166,39 +156,12 @@ fn plugin_init() -> bool {
workflow::RunMatcher {},
);
- #[cfg(debug_assertions)]
- register_command(
- "WARP\\Debug\\Cache",
- "Debug cache sizes... because...",
- debug::DebugCache {},
- );
-
- #[cfg(debug_assertions)]
- register_command(
- "WARP\\Debug\\Invalidate Caches",
- "Invalidate all WARP caches",
- debug::DebugInvalidateCache {},
- );
-
- #[cfg(debug_assertions)]
- register_command_for_function(
- "WARP\\Debug\\Function Signature",
- "Print the entire signature for the function",
- debug::DebugFunction {},
- );
-
register_command(
"WARP\\Load File",
- "Load file into the matcher, this does NOT kick off matcher analysis",
+ "Load WARP file",
load::LoadSignatureFile {},
);
- register_global_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",
@@ -217,42 +180,6 @@ fn plugin_init() -> bool {
function::RemoveFunction {},
);
- register_command_for_function(
- "WARP\\Copy GUID",
- "Copy the computed GUID for the function",
- function::CopyFunctionGUID {},
- );
-
- register_command(
- "WARP\\Find GUID",
- "Locate the function in the view using a GUID",
- function::FindFunctionFromGUID {},
- );
-
- register_command(
- "WARP\\Create\\From Current View",
- "Creates a signature file containing all selected functions",
- create::CreateFromCurrentView {},
- );
-
- register_global_command(
- "WARP\\Create\\From File(s)",
- "Creates a signature file containing all selected functions",
- create::CreateFromFiles {},
- );
-
- register_command(
- "WARP\\Show Report",
- "Creates a report for the selected file, displaying info on functions and types",
- file::ShowFileReport {},
- );
-
- register_command_for_project(
- "WARP\\Create\\From Project",
- "Create signature files from select project files",
- project::CreateSignatures {},
- );
-
true
}
diff --git a/plugins/warp/src/plugin/commit.rs b/plugins/warp/src/plugin/commit.rs
deleted file mode 100644
index 4e8a9fee..00000000
--- a/plugins/warp/src/plugin/commit.rs
+++ /dev/null
@@ -1,149 +0,0 @@
-//! Commit file to a source.
-
-use crate::cache::container::cached_containers;
-use crate::container::{SourceId, SourcePath};
-use crate::plugin::create::OpenFileField;
-use binaryninja::command::GlobalCommand;
-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)?;
- tracing::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 {
- tracing::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();
- tracing::info!(
- "Adding {} functions to source: {}",
- functions.len(),
- source_id
- );
- if let Err(e) = container.add_functions(
- &chunk.header.target,
- &source_id,
- &functions,
- ) {
- tracing::error!("Failed to add functions to source: {}", e);
- }
- }
- ChunkKind::Type(sc) => {
- let types: Vec<_> = sc.types().collect();
- tracing::info!("Adding {} types to source: {}", types.len(), source_id);
- if let Err(e) = container.add_computed_types(&source_id, &types) {
- tracing::error!("Failed to add types to source: {}", e);
- }
- }
- }
- }
- if let Err(e) = container.commit_source(&source_id) {
- tracing::error!("Failed to commit source: {}", e);
- }
- tracing::info!("Committed file to source: {}", source_id);
- return Some(());
- }
- }
-
- Some(())
- }
-}
-
-impl GlobalCommand for CommitFile {
- fn action(&self) {
- std::thread::spawn(move || {
- Self::execute();
- });
- }
-
- fn valid(&self) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/create.rs b/plugins/warp/src/plugin/create.rs
deleted file mode 100644
index 5f5527a7..00000000
--- a/plugins/warp/src/plugin/create.rs
+++ /dev/null
@@ -1,265 +0,0 @@
-use crate::processor::{
- new_processing_state_background_thread, CompressionTypeField, FileDataKindField,
- IncludedFunctionsField, SaveReportToDiskField, WarpFileProcessor,
-};
-use crate::report::{ReportGenerator, ReportKindField};
-use crate::{user_signature_dir, INCLUDE_TAG_NAME};
-use binaryninja::background_task::BackgroundTask;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::{Command, GlobalCommand};
-use binaryninja::file_metadata::FileMetadata;
-use binaryninja::interaction::form::{Form, FormInputField};
-use binaryninja::interaction::{MessageBoxButtonResult, MessageBoxButtonSet, MessageBoxIcon};
-use binaryninja::rc::Ref;
-use std::path::PathBuf;
-use std::thread;
-use warp::chunk::Chunk;
-use warp::WarpFile;
-
-pub struct SaveFileField;
-
-impl SaveFileField {
- pub fn field(view: &BinaryView) -> FormInputField {
- let file = view.file();
- let default_name = match file.project_file() {
- None => {
- // Not in a project, use the file name directly.
- file.display_name()
- }
- Some(project_file) => project_file.name(),
- };
- let signature_dir = user_signature_dir();
- let default_file_path = signature_dir.join(&default_name).with_extension("warp");
- FormInputField::SaveFileName {
- prompt: "File Path".to_string(),
- // TODO: This is called extension but is really a filter.
- extension: Some("*.warp".to_string()),
- default: Some(default_file_path.to_string_lossy().to_string()),
- value: None,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<PathBuf> {
- let field = form.get_field_with_name("File Path")?;
- let field_value = field.try_value_string()?;
- Some(PathBuf::from(field_value))
- }
-}
-
-pub struct OpenFileField;
-
-impl OpenFileField {
- pub fn field() -> FormInputField {
- FormInputField::OpenFileName {
- prompt: "Input File Path".to_string(),
- extension: None,
- default: None,
- value: None,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<PathBuf> {
- let field = form.get_field_with_name("Input File Path")?;
- let field_value = field.try_value_string()?;
- Some(PathBuf::from(field_value))
- }
-}
-
-pub struct CreateFromCurrentView;
-
-impl CreateFromCurrentView {
- pub fn execute(view: Ref<BinaryView>, external_file: bool) -> Option<()> {
- // Prompt the user first so that they can go do other things and not worry about a popup.
- let mut form = Form::new("Create From View");
-
- if external_file {
- form.add_field(OpenFileField::field());
- }
-
- form.add_field(SaveFileField::field(&view));
-
- let fd_field = FileDataKindField::default();
- form.add_field(fd_field.to_field());
-
- let compression_field = CompressionTypeField::default();
- form.add_field(compression_field.to_field());
-
- let mut included_field = IncludedFunctionsField::default();
- // If the view has the include tag, we better set the default to the selected functions.
- if view.tag_type_by_name(INCLUDE_TAG_NAME).is_some() {
- included_field = IncludedFunctionsField::Selected;
- }
- form.add_field(included_field.to_field());
-
- let report_field = ReportKindField::default();
- form.add_field(report_field.to_field());
- let report_to_disk_field = SaveReportToDiskField::default();
- form.add_field(report_to_disk_field.to_field());
-
- if !form.prompt() {
- return None;
- }
- let compression_type = CompressionTypeField::from_form(&form).unwrap_or_default();
- let file_path = SaveFileField::from_form(&form)?;
- let file_data_kind = FileDataKindField::from_form(&form).unwrap_or_default();
- let file_included_functions = IncludedFunctionsField::from_form(&form).unwrap_or_default();
- let report_kind = ReportKindField::from_form(&form).unwrap_or_default();
- let save_report_to_disk = SaveReportToDiskField::from_form(&form).unwrap_or_default();
- let open_file_path = OpenFileField::from_form(&form);
-
- // If we already have a file, prompt the user if they want to add the data.
- let mut existing_chunks = Vec::new();
- if file_path.exists() {
- let prompt_result = binaryninja::interaction::show_message_box(
- "Keep existing file data?",
- "The file already exists. Do you want to keep the existing data?",
- MessageBoxButtonSet::YesNoCancelButtonSet,
- MessageBoxIcon::QuestionIcon,
- );
-
- match prompt_result {
- MessageBoxButtonResult::NoButton => {
- // User wants to overwrite the file.
- }
- MessageBoxButtonResult::YesButton | MessageBoxButtonResult::OKButton => {
- // User wants to keep the existing data.
- let data = std::fs::read(&file_path).ok()?;
- let existing_file = WarpFile::from_owned_bytes(data)?;
- existing_chunks.extend(existing_file.chunks);
- }
- MessageBoxButtonResult::CancelButton => {
- tracing::info!(
- "User cancelled signature file creation, no operations were performed."
- );
- return None;
- }
- }
- }
-
- let processor = WarpFileProcessor::new()
- .with_compression_type(compression_type)
- .with_file_data(file_data_kind)
- .with_included_functions(file_included_functions);
-
- let file = match open_file_path {
- None => {
- // We are processing the current view. NOT an external file.
- // Reference path is just used for the state tracking. Does not need to be readable.
- let reference_path = file_path.clone();
- processor.process_view(reference_path, &view)
- }
- Some(open_file_path) => {
- // This thread will show the state in a background task.
- let background_task = BackgroundTask::new("Processing started...", true);
- new_processing_state_background_thread(background_task.clone(), processor.state());
- let file = processor.process(open_file_path);
- background_task.finish();
- file
- }
- };
-
- if let Err(err) = file {
- binaryninja::interaction::show_message_box(
- "Failed to create signature file",
- &err.to_string(),
- MessageBoxButtonSet::OKButtonSet,
- MessageBoxIcon::ErrorIcon,
- );
- tracing::error!("Failed to create signature file: {}", err);
- return None;
- }
-
- let background_task = BackgroundTask::new("Creating WARP File...", false);
- let mut file = file.unwrap();
- // Add back the existing chunks if the user selected to keep them.
- if !existing_chunks.is_empty() {
- file.chunks.extend(existing_chunks);
- // TODO: Make merging optional?
- // TODO: Merging can lose chunk data if it goes above the maximum table count.
- // TODO: We should probably solve that in the warp crate itself?
- file.chunks = Chunk::merge(&file.chunks, compression_type.into());
-
- // After merging, we should have at least one chunk. If not, merging actually removed data.
- if file.chunks.len() < 1 {
- tracing::error!(
- "Failed to merge chunks! Please report this, it should not happen."
- );
- return None;
- }
- }
-
- let file_bytes = file.to_bytes();
- let file_size = file_bytes.len();
- if std::fs::write(&file_path, file_bytes).is_err() {
- tracing::error!("Failed to write data to signature file!");
- }
- tracing::info!("Saved signature file to: '{}'", file_path.display());
- background_task.finish();
-
- // Show a report of the generate signatures, if desired.
- let report_generator = ReportGenerator::new();
- if let Some(report_string) = report_generator.report(&report_kind, &file) {
- if save_report_to_disk == SaveReportToDiskField::Yes {
- let report_ext = report_generator
- .report_extension(&report_kind)
- .unwrap_or_default();
- let report_path = file_path.with_extension(report_ext);
- let _ = std::fs::write(report_path, &report_string);
- }
-
- // The ReportWidget uses a QTextBrowser, which cannot render large files very well.
- if file_size > 10000000 {
- tracing::warn!("WARP report file is too large to show in the UI. Please see the report file on disk.");
- } else {
- match report_kind {
- ReportKindField::None => {}
- ReportKindField::Html => {
- view.show_html_report("Generated WARP File", report_string.as_str(), "");
- }
- ReportKindField::Markdown => {
- view.show_markdown_report(
- "Generated WARP File",
- report_string.as_str(),
- "",
- );
- }
- ReportKindField::Json => {
- view.show_plaintext_report("Generated WARP File", report_string.as_str());
- }
- }
- }
- }
-
- Some(())
- }
-}
-
-impl Command for CreateFromCurrentView {
- fn action(&self, view: &BinaryView) {
- let view = view.to_owned();
- thread::spawn(move || {
- CreateFromCurrentView::execute(view, false);
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
-
-pub struct CreateFromFiles;
-
-impl GlobalCommand for CreateFromFiles {
- fn action(&self) {
- let empty_file_metadata = FileMetadata::new();
- let empty_bv = BinaryView::from_data(&empty_file_metadata, &[]);
- thread::spawn(move || {
- CreateFromCurrentView::execute(empty_bv.to_owned(), true);
- empty_bv.file().close();
- });
- }
-
- fn valid(&self) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/debug.rs b/plugins/warp/src/plugin/debug.rs
deleted file mode 100644
index 4198ea30..00000000
--- a/plugins/warp/src/plugin/debug.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use crate::cache::container::for_cached_containers;
-use crate::{build_function, cache};
-use binaryninja::binary_view::BinaryView;
-use binaryninja::command::{Command, FunctionCommand};
-use binaryninja::function::Function;
-use binaryninja::object_destructor::ObjectDestructor;
-
-pub struct DebugFunction;
-
-impl FunctionCommand for DebugFunction {
- fn action(&self, _view: &BinaryView, func: &Function) {
- tracing::info!(
- "{:#?}",
- build_function(func, || func.lifted_il().ok(), false)
- );
- }
-
- fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
- true
- }
-}
-
-pub struct DebugCache;
-
-impl Command for DebugCache {
- fn action(&self, _view: &BinaryView) {
- for_cached_containers(|c| {
- tracing::info!("Container: {:#?}", c);
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
-
-pub struct DebugInvalidateCache;
-
-impl Command for DebugInvalidateCache {
- fn action(&self, view: &BinaryView) {
- let destructor = cache::CacheDestructor {};
- destructor.destruct_view(view);
- tracing::info!("Invalidated all WARP caches...");
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/ffi.rs b/plugins/warp/src/plugin/ffi.rs
index c1f5acb8..bb6fb483 100644
--- a/plugins/warp/src/plugin/ffi.rs
+++ b/plugins/warp/src/plugin/ffi.rs
@@ -1,6 +1,8 @@
mod container;
mod file;
mod function;
+mod processor;
+mod ty;
use binaryninjacore_sys::{
BNBasicBlock, BNBinaryView, BNFunction, BNLowLevelILFunction, BNPlatform,
@@ -48,6 +50,7 @@ pub type BNWARPTypeGUID = TypeGUID;
pub type BNWARPTarget = warp::target::Target;
pub type BNWARPFunction = warp::signature::function::Function;
pub type BNWARPContainer = RwLock<Box<dyn Container>>;
+pub type BNWARPType = warp::r#type::Type;
// TODO: Some sort of callback for loading functions
// TODO: Be able to run matcher for a specific file
diff --git a/plugins/warp/src/plugin/ffi/container.rs b/plugins/warp/src/plugin/ffi/container.rs
index aa0c37d3..09f15d99 100644
--- a/plugins/warp/src/plugin/ffi/container.rs
+++ b/plugins/warp/src/plugin/ffi/container.rs
@@ -3,17 +3,11 @@ use crate::container::disk::DiskContainer;
use crate::container::{
ContainerSearchItem, ContainerSearchItemKind, ContainerSearchQuery, SourcePath, SourceTag,
};
-use crate::convert::{from_bn_type, to_bn_type};
use crate::plugin::ffi::{
BNWARPConstraintGUID, BNWARPContainer, BNWARPFunction, BNWARPFunctionGUID, BNWARPSource,
- BNWARPTarget, BNWARPTypeGUID,
+ BNWARPTarget, BNWARPType, BNWARPTypeGUID,
};
-use binaryninja::architecture::CoreArchitecture;
-use binaryninja::binary_view::BinaryView;
-use binaryninja::rc::Ref;
use binaryninja::string::BnString;
-use binaryninja::types::Type;
-use binaryninjacore_sys::{BNArchitecture, BNBinaryView, BNType};
use std::collections::HashMap;
use std::ffi::{c_char, CStr};
use std::mem::ManuallyDrop;
@@ -98,32 +92,25 @@ pub unsafe extern "C" fn BNWARPContainerSearchItemGetSource(
#[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,
- };
-
+) -> *mut BNWARPType {
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
+ Some(func_ty) => {
+ let arc_func_ty = Arc::new(func_ty.clone());
+ // NOTE: Freed by BNWARPFreeTypeReference
+ Arc::into_raw(arc_func_ty) as *mut BNWARPType
}
}
}
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
+ let arc_ty = Arc::new(ty.clone());
+ // NOTE: Freed by BNWARPFreeTypeReference
+ Arc::into_raw(arc_ty) as *mut BNWARPType
}
ContainerSearchItemKind::Symbol(_) => std::ptr::null_mut(),
}
@@ -254,7 +241,7 @@ pub unsafe extern "C" fn BNWARPContainerGetSources(
return std::ptr::null_mut();
};
- // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ // NOTE: Leak the sources to be freed by BNWARPFreeUUIDList
let boxed_sources: Box<[_]> = container.sources().unwrap_or_default().into_boxed_slice();
*count = boxed_sources.len();
Box::into_raw(boxed_sources) as *mut BNWARPSource
@@ -389,14 +376,11 @@ pub unsafe extern "C" fn BNWARPContainerAddFunctions(
#[no_mangle]
pub unsafe extern "C" fn BNWARPContainerAddTypes(
- view: *mut BNBinaryView,
container: *mut BNWARPContainer,
source: *const BNWARPSource,
- types: *mut *mut BNType,
+ types: *mut *mut BNWARPType,
count: usize,
) -> bool {
- let view = unsafe { BinaryView::from_raw(view) };
-
let arc_container = ManuallyDrop::new(Arc::from_raw(container));
let Ok(mut container) = arc_container.write() else {
return false;
@@ -405,10 +389,11 @@ pub unsafe extern "C" fn BNWARPContainerAddTypes(
let source = unsafe { *source };
let types_ptr = std::slice::from_raw_parts(types, count);
+ // TODO: We have to clone the objects here to make the type checker happy.
+ // TODO: See about avoiding this later.
let types: Vec<_> = types_ptr
.iter()
- .map(|&t| Type::from_raw(t))
- .map(|ty| from_bn_type(&view, &ty, 255))
+ .map(|&t| unsafe { ManuallyDrop::new(Arc::from_raw(t)).as_ref().clone() })
.collect();
container.add_types(&source, &types).is_ok()
}
@@ -476,7 +461,7 @@ pub unsafe extern "C" fn BNWARPContainerGetSourcesWithFunctionGUID(
let guid = unsafe { *guid };
- // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ // NOTE: Leak the sources to be freed by BNWARPFreeUUIDList
let boxed_sources: Box<[_]> = container
.sources_with_function_guid(&target, &guid)
.unwrap_or_default()
@@ -498,7 +483,7 @@ pub unsafe extern "C" fn BNWARPContainerGetSourcesWithTypeGUID(
let guid = unsafe { *guid };
- // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ // NOTE: Leak the sources to be freed by BNWARPFreeUUIDList
let boxed_sources: Box<[_]> = container
.sources_with_type_guid(&guid)
.unwrap_or_default()
@@ -538,22 +523,17 @@ pub unsafe extern "C" fn BNWARPContainerGetFunctionsWithGUID(
Box::into_raw(raw_boxed_functions) as *mut *mut BNWARPFunction
}
-// TODO: Swap arch to Target?
#[no_mangle]
pub unsafe extern "C" fn BNWARPContainerGetTypeWithGUID(
- arch: *mut BNArchitecture,
container: *mut BNWARPContainer,
source: *const BNWARPSource,
guid: *const BNWARPTypeGUID,
-) -> *mut BNType {
+) -> *mut BNWARPType {
let arc_container = ManuallyDrop::new(Arc::from_raw(container));
let Ok(container) = arc_container.read() else {
return std::ptr::null_mut();
};
- // NOTE: to convert the type, we must have an architecture.
- let arch = CoreArchitecture::from_raw(arch);
-
let source = unsafe { *source };
let guid = unsafe { *guid };
@@ -561,9 +541,10 @@ 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(Some(arch), &ty);
- // NOTE: The type ref has been pre-incremented for the caller.
- unsafe { Ref::into_raw(function_type) }.handle
+
+ let arc_ty = Arc::new(ty);
+ // NOTE: Freed by BNWARPFreeTypeReference
+ Arc::into_raw(arc_ty) as *mut BNWARPType
}
#[no_mangle]
diff --git a/plugins/warp/src/plugin/ffi/file.rs b/plugins/warp/src/plugin/ffi/file.rs
index 951b5eb2..3abfe5aa 100644
--- a/plugins/warp/src/plugin/ffi/file.rs
+++ b/plugins/warp/src/plugin/ffi/file.rs
@@ -1,11 +1,43 @@
+use crate::plugin::ffi::{BNWARPFunction, BNWARPTarget, BNWARPType};
+use binaryninja::data_buffer::DataBuffer;
+use binaryninjacore_sys::BNDataBuffer;
use std::ffi::c_char;
+use std::mem::ManuallyDrop;
use std::sync::Arc;
-use warp::WarpFile;
+use warp::chunk::ChunkKind;
+use warp::{WarpFile, WarpFileHeader};
-pub type BNWARPFile = WarpFile<'static>;
+/// A [`WarpFile`] wrapper that uses reference counting to manage its chunks lifetime.
+///
+/// Used primarily when passing to the C FFI so that chunks do not need to have a complicated lifetime.
+pub struct RcWarpFile {
+ pub header: WarpFileHeader,
+ pub chunks: Vec<Arc<warp::chunk::Chunk<'static>>>,
+}
+
+impl From<WarpFile<'static>> for RcWarpFile {
+ fn from(file: WarpFile<'static>) -> Self {
+ let chunks = file.chunks.into_iter().map(|c| Arc::new(c)).collect();
+ Self {
+ header: file.header,
+ chunks,
+ }
+ }
+}
+
+impl From<&RcWarpFile> for WarpFile<'static> {
+ fn from(rc_file: &RcWarpFile) -> Self {
+ let chunks = rc_file.chunks.iter().map(|c| (**c).clone()).collect();
+ Self {
+ header: rc_file.header.clone(),
+ chunks,
+ }
+ }
+}
+
+pub type BNWARPFile = RcWarpFile;
-// 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>;
+pub type BNWARPChunk = warp::chunk::Chunk<'static>;
// TODO: From bytes as well.
#[no_mangle]
@@ -20,7 +52,78 @@ pub unsafe extern "C" fn BNWARPNewFileFromPath(path: *mut c_char) -> *mut BNWARP
let Some(file) = WarpFile::from_owned_bytes(bytes) else {
return std::ptr::null_mut();
};
- Arc::into_raw(Arc::new(file)) as *mut BNWARPFile
+ let rc_file = RcWarpFile::from(file);
+ Arc::into_raw(Arc::new(rc_file)) as *mut BNWARPFile
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFileGetChunks(
+ file: *mut BNWARPFile,
+ count: *mut usize,
+) -> *mut *mut BNWARPChunk {
+ let arc_file = ManuallyDrop::new(Arc::from_raw(file));
+ *count = arc_file.chunks.len();
+ let boxed_chunks: Box<[_]> = arc_file
+ .chunks
+ .iter()
+ .map(|c| Arc::into_raw(c.clone()))
+ .collect();
+ Box::into_raw(boxed_chunks) as *mut *mut BNWARPChunk
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFileToDataBuffer(file: *mut BNWARPFile) -> *mut BNDataBuffer {
+ let arc_file = ManuallyDrop::new(Arc::from_raw(file));
+ let warp_file = WarpFile::from(arc_file.as_ref());
+ let buffer = DataBuffer::new(&warp_file.to_bytes());
+ buffer.into_raw()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPChunkGetTarget(chunk: *const BNWARPChunk) -> *mut BNWARPTarget {
+ let chunk = unsafe { &*chunk };
+ let chunk_target = chunk.header.target.clone();
+ Arc::into_raw(Arc::new(chunk_target)) as *mut BNWARPTarget
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPChunkGetFunctions(
+ chunk: *const BNWARPChunk,
+ count: *mut usize,
+) -> *mut *mut BNWARPFunction {
+ let chunk = unsafe { &*chunk };
+ match &chunk.kind {
+ ChunkKind::Signature(sc) => {
+ let boxed_funcs: Box<[_]> = sc
+ .functions()
+ .into_iter()
+ .map(|f| Arc::into_raw(Arc::new(f.clone())))
+ .collect();
+ *count = boxed_funcs.len();
+ Box::into_raw(boxed_funcs) as *mut *mut BNWARPFunction
+ }
+ ChunkKind::Type(_) => std::ptr::null_mut(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPChunkGetTypes(
+ chunk: *const BNWARPChunk,
+ count: *mut usize,
+) -> *mut *mut BNWARPType {
+ let chunk = unsafe { &*chunk };
+ match &chunk.kind {
+ ChunkKind::Signature(_) => std::ptr::null_mut(),
+ ChunkKind::Type(tc) => {
+ let boxed_types: Box<[_]> = tc
+ .types()
+ .into_iter()
+ .map(|t| Arc::into_raw(Arc::new(t.ty.clone())))
+ .collect();
+ *count = boxed_types.len();
+ Box::into_raw(boxed_types) as *mut *mut BNWARPType
+ }
+ }
}
#[no_mangle]
@@ -36,3 +139,27 @@ pub unsafe extern "C" fn BNWARPFreeFileReference(file: *mut BNWARPFile) {
}
Arc::decrement_strong_count(file);
}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewChunkReference(chunk: *mut BNWARPChunk) -> *mut BNWARPChunk {
+ Arc::increment_strong_count(chunk);
+ chunk
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeChunkReference(chunk: *mut BNWARPChunk) {
+ if chunk.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(chunk);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeChunkList(chunks: *mut *mut BNWARPChunk, count: usize) {
+ let chunks_ptr = std::ptr::slice_from_raw_parts_mut(chunks, count);
+ let chunks = unsafe { Box::from_raw(chunks_ptr) };
+ for chunk in chunks {
+ // NOTE: The chunks themselves should also be arc.
+ BNWARPFreeChunkReference(chunk);
+ }
+}
diff --git a/plugins/warp/src/plugin/ffi/function.rs b/plugins/warp/src/plugin/ffi/function.rs
index 138c0a7b..2eff9e4a 100644
--- a/plugins/warp/src/plugin/ffi/function.rs
+++ b/plugins/warp/src/plugin/ffi/function.rs
@@ -1,11 +1,11 @@
use crate::build_function;
use crate::cache::{insert_cached_function_match, try_cached_function_match};
-use crate::convert::{to_bn_symbol_at_address, to_bn_type};
-use crate::plugin::ffi::{BNWARPConstraint, BNWARPFunction, BNWARPFunctionGUID};
+use crate::convert::to_bn_symbol_at_address;
+use crate::plugin::ffi::{BNWARPConstraint, BNWARPFunction, BNWARPFunctionGUID, BNWARPType};
use binaryninja::function::Function;
use binaryninja::rc::Ref;
use binaryninja::string::BnString;
-use binaryninjacore_sys::{BNFunction, BNSymbol, BNType};
+use binaryninjacore_sys::{BNFunction, BNSymbol};
use std::ffi::c_char;
use std::mem::ManuallyDrop;
use std::sync::Arc;
@@ -111,19 +111,14 @@ pub unsafe extern "C" fn BNWARPFunctionGetSymbolName(function: *mut BNWARPFuncti
}
#[no_mangle]
-pub unsafe extern "C" fn BNWARPFunctionGetType(
- function: *mut BNWARPFunction,
- analysis_function: *mut BNFunction,
-) -> *mut BNType {
- let analysis_function = Function::from_raw(analysis_function);
+pub unsafe extern "C" fn BNWARPFunctionGetType(function: *mut BNWARPFunction) -> *mut BNWARPType {
// We do not own function so we should not drop.
let function = ManuallyDrop::new(Arc::from_raw(function));
match &function.ty {
Some(func_ty) => {
- let arch = analysis_function.arch();
- 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
+ let arc_func_ty = Arc::new(func_ty.clone());
+ // NOTE: Freed by BNWARPFreeTypeReference
+ Arc::into_raw(arc_func_ty) as *mut BNWARPType
}
None => std::ptr::null_mut(),
}
diff --git a/plugins/warp/src/plugin/ffi/processor.rs b/plugins/warp/src/plugin/ffi/processor.rs
new file mode 100644
index 00000000..83b47f5a
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi/processor.rs
@@ -0,0 +1,187 @@
+use crate::plugin::ffi::file::{BNWARPFile, RcWarpFile};
+use crate::processor::{
+ IncludedDataField, IncludedFunctionsField, ProcessingFileState, WarpFileProcessor,
+ WarpFileProcessorEntry,
+};
+use binaryninja::binary_view::BinaryView;
+use binaryninja::project::file::ProjectFile;
+use binaryninja::project::Project;
+use binaryninjacore_sys::{BNBinaryView, BNProject, BNProjectFile};
+use std::ffi::{c_char, CStr, CString};
+use std::mem::ManuallyDrop;
+use std::path::PathBuf;
+use std::ptr::NonNull;
+use std::sync::Arc;
+use warp::chunk::CompressionType;
+
+pub type BNWARPProcessor = WarpFileProcessor;
+
+#[repr(C)]
+pub struct BNWARPProcessorState {
+ cancelled: bool,
+ unprocessed_files_count: usize,
+ processed_files_count: usize,
+ analyzing_files: *mut *mut c_char,
+ analyzing_files_count: usize,
+ processing_files: *mut *mut c_char,
+ processing_files_count: usize,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewProcessor(
+ included_data: IncludedDataField,
+ included_functions: IncludedFunctionsField,
+ worker_count: usize,
+) -> *mut BNWARPProcessor {
+ let processor = WarpFileProcessor::new()
+ .with_file_data(included_data)
+ .with_included_functions(included_functions)
+ .with_compression_type(CompressionType::Zstd)
+ .with_entry_worker_count(worker_count);
+ Box::into_raw(Box::new(processor))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorAddPath(
+ processor: *mut BNWARPProcessor,
+ path: *const c_char,
+) {
+ let mut processor = ManuallyDrop::new(Box::from_raw(processor));
+ let path_cstr = unsafe { CStr::from_ptr(path) };
+ let path = PathBuf::from(path_cstr.to_str().unwrap());
+ // TODO: Not thread safe.
+ processor.add_entry(WarpFileProcessorEntry::Path(path));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorAddProject(
+ processor: *mut BNWARPProcessor,
+ project: *mut BNProject,
+) {
+ let mut processor = ManuallyDrop::new(Box::from_raw(processor));
+ let project = Project::from_raw(NonNull::new(project).unwrap());
+ // TODO: Not thread safe.
+ processor.add_entry(WarpFileProcessorEntry::Project(project.to_owned()));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorAddProjectFile(
+ processor: *mut BNWARPProcessor,
+ project_file: *mut BNProjectFile,
+) {
+ let mut processor = ManuallyDrop::new(Box::from_raw(processor));
+ let project_file = ProjectFile::from_raw(NonNull::new(project_file).unwrap());
+ // TODO: Not thread safe.
+ processor.add_entry(WarpFileProcessorEntry::ProjectFile(project_file.to_owned()));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorAddBinaryView(
+ processor: *mut BNWARPProcessor,
+ view: *mut BNBinaryView,
+) {
+ let mut processor = ManuallyDrop::new(Box::from_raw(processor));
+ let view = BinaryView::from_raw(view);
+ // TODO: Not thread safe.
+ processor.add_entry(WarpFileProcessorEntry::BinaryView(view.to_owned()));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorStart(processor: *mut BNWARPProcessor) -> *mut BNWARPFile {
+ let mut processor = ManuallyDrop::new(Box::from_raw(processor));
+ // TODO: Not thread safe.
+ match processor.process_entries() {
+ Ok(file) => {
+ let rc_file = RcWarpFile::from(file);
+ Arc::into_raw(Arc::new(rc_file)) as *mut BNWARPFile
+ }
+ Err(_) => std::ptr::null_mut(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorIsCancelled(processor: *mut BNWARPProcessor) -> bool {
+ let processor = ManuallyDrop::new(Box::from_raw(processor));
+ processor
+ .state()
+ .cancelled
+ .load(std::sync::atomic::Ordering::Relaxed)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorCancel(processor: *mut BNWARPProcessor) {
+ let processor = ManuallyDrop::new(Box::from_raw(processor));
+ processor
+ .state()
+ .cancelled
+ .store(true, std::sync::atomic::Ordering::Relaxed)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorGetState(
+ processor: *mut BNWARPProcessor,
+) -> BNWARPProcessorState {
+ let processor = ManuallyDrop::new(Box::from_raw(processor));
+ let processor_state = processor.state();
+
+ let mut unprocessed_files_count = 0;
+ let mut processed_files_count = 0;
+ let mut analyzing_files = Vec::new();
+ let mut processing_files = Vec::new();
+
+ for file_state in &processor_state.files {
+ match file_state.value() {
+ ProcessingFileState::Unprocessed => unprocessed_files_count += 1,
+ ProcessingFileState::Analyzing => analyzing_files.push(file_state.key().clone()),
+ ProcessingFileState::Processing => processing_files.push(file_state.key().clone()),
+ ProcessingFileState::Processed => processed_files_count += 1,
+ }
+ }
+
+ let raw_analyzing_files: Box<[_]> = analyzing_files
+ .into_iter()
+ .map(|p| CString::new(p.to_str().unwrap()).unwrap().into_raw())
+ .collect();
+ let raw_analyzing_files_count = raw_analyzing_files.len();
+ let raw_analyzing_files_ptr = Box::into_raw(raw_analyzing_files);
+
+ let raw_processing_files: Box<[_]> = processing_files
+ .into_iter()
+ .map(|p| CString::new(p.to_str().unwrap()).unwrap().into_raw())
+ .collect();
+ let raw_processing_files_count = raw_processing_files.len();
+ let raw_processing_files_ptr = Box::into_raw(raw_processing_files);
+
+ BNWARPProcessorState {
+ cancelled: processor_state
+ .cancelled
+ .load(std::sync::atomic::Ordering::Relaxed),
+ unprocessed_files_count,
+ processed_files_count,
+ analyzing_files: raw_analyzing_files_ptr as *mut *mut c_char,
+ analyzing_files_count: raw_analyzing_files_count,
+ processing_files: raw_processing_files_ptr as *mut *mut c_char,
+ processing_files_count: raw_processing_files_count,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeProcessor(processor: *mut BNWARPProcessor) {
+ let _ = Box::from_raw(processor);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeProcessorState(state: BNWARPProcessorState) {
+ let a_files_ptr =
+ std::ptr::slice_from_raw_parts_mut(state.analyzing_files, state.analyzing_files_count);
+ let a_files_boxed = unsafe { Box::from_raw(a_files_ptr) };
+ for path in a_files_boxed.iter() {
+ let _ = CString::from_raw(*path);
+ }
+ let p_files_ptr =
+ std::ptr::slice_from_raw_parts_mut(state.processing_files, state.processing_files_count);
+ let p_files_boxed = unsafe { Box::from_raw(p_files_ptr) };
+ for path in p_files_boxed.iter() {
+ let _ = CString::from_raw(*path);
+ }
+}
diff --git a/plugins/warp/src/plugin/ffi/ty.rs b/plugins/warp/src/plugin/ffi/ty.rs
new file mode 100644
index 00000000..6a16fdab
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi/ty.rs
@@ -0,0 +1,75 @@
+use crate::convert::{from_bn_type, to_bn_type};
+use crate::plugin::ffi::BNWARPType;
+use binaryninja::architecture::CoreArchitecture;
+use binaryninja::binary_view::BinaryView;
+use binaryninja::file_metadata::FileMetadata;
+use binaryninja::rc::Ref as BnRef;
+use binaryninja::string::BnString;
+use binaryninja::types::Type;
+use binaryninjacore_sys::{BNArchitecture, BNType};
+use std::ffi::c_char;
+use std::mem::ManuallyDrop;
+use std::sync::Arc;
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetType(
+ analysis_type: *mut BNType,
+ confidence: u8,
+) -> *mut BNWARPType {
+ let analysis_type = Type::from_raw(analysis_type);
+ // TODO: This will leak a bunch of memory, but we need to remove the view requirement anyways.
+ let binary_view = BinaryView::from_data(&FileMetadata::new(), &[]);
+ let ty = from_bn_type(&binary_view, &analysis_type, confidence);
+ Arc::into_raw(Arc::new(ty)) as *mut BNWARPType
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPTypeGetName(ty: *mut BNWARPType) -> *mut c_char {
+ let ty = ManuallyDrop::new(Arc::from_raw(ty));
+ match ty.name.as_deref() {
+ Some(name) => BnString::into_raw(BnString::new(name)),
+ None => std::ptr::null_mut(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPTypeGetConfidence(ty: *mut BNWARPType) -> u8 {
+ let ty = ManuallyDrop::new(Arc::from_raw(ty));
+ ty.confidence
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPTypeGetAnalysisType(
+ arch: *mut BNArchitecture,
+ ty: *mut BNWARPType,
+) -> *mut BNType {
+ let ty = ManuallyDrop::new(Arc::from_raw(ty));
+ let analysis_ty = match arch.is_null() {
+ true => to_bn_type::<CoreArchitecture>(None, &ty),
+ false => to_bn_type(Some(CoreArchitecture::from_raw(arch)), &ty),
+ };
+ BnRef::into_raw(analysis_ty).handle
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewTypeReference(ty: *mut BNWARPType) -> *mut BNWARPType {
+ Arc::increment_strong_count(ty);
+ ty
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeTypeReference(ty: *mut BNWARPType) {
+ if ty.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(ty);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeTypeList(types: *mut *mut BNWARPType, count: usize) {
+ let types_ptr = std::ptr::slice_from_raw_parts_mut(types, count);
+ let types = unsafe { Box::from_raw(types_ptr) };
+ for ty in types {
+ unsafe { BNWARPFreeTypeReference(ty) };
+ }
+}
diff --git a/plugins/warp/src/plugin/file.rs b/plugins/warp/src/plugin/file.rs
deleted file mode 100644
index cd5fb626..00000000
--- a/plugins/warp/src/plugin/file.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use crate::report::ReportGenerator;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::Command;
-
-pub struct ShowFileReport;
-
-impl Command for ShowFileReport {
- fn action(&self, view: &BinaryView) {
- let view = view.to_owned();
- std::thread::spawn(move || {
- let Some(path) =
- binaryninja::interaction::get_open_filename_input("Select file to show", "*.warp")
- else {
- return;
- };
-
- let Ok(bytes) = std::fs::read(&path) else {
- tracing::error!("Failed to read file: {:?}", path);
- return;
- };
-
- let Some(file) = warp::WarpFile::from_bytes(&bytes) else {
- tracing::error!("Failed to parse file: {:?}", path);
- return;
- };
-
- let report_generator = ReportGenerator::new();
- if let Some(html_string) = report_generator.html_report(&file) {
- view.show_html_report(
- &format!("WARP File: {}", path.to_string_lossy()),
- html_string.as_str(),
- "",
- );
- }
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/function.rs b/plugins/warp/src/plugin/function.rs
index 36db7d55..908bd0db 100644
--- a/plugins/warp/src/plugin/function.rs
+++ b/plugins/warp/src/plugin/function.rs
@@ -1,18 +1,10 @@
-use crate::cache::{
- cached_function_guid, insert_cached_function_match, try_cached_function_guid,
- try_cached_function_match,
-};
+use crate::cache::{insert_cached_function_match, try_cached_function_match};
use crate::{
get_warp_ignore_tag_type, get_warp_include_tag_type, IGNORE_TAG_NAME, INCLUDE_TAG_NAME,
};
-use binaryninja::background_task::BackgroundTask;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::{Command, FunctionCommand};
+use binaryninja::binary_view::BinaryView;
+use binaryninja::command::FunctionCommand;
use binaryninja::function::{Function, FunctionUpdateType};
-use binaryninja::rc::Guard;
-use rayon::iter::ParallelIterator;
-use std::thread;
-use warp::signature::function::FunctionGUID;
pub struct IncludeFunction;
@@ -101,91 +93,3 @@ impl FunctionCommand for RemoveFunction {
try_cached_function_match(func).is_some()
}
}
-
-pub struct CopyFunctionGUID;
-
-impl FunctionCommand for CopyFunctionGUID {
- fn action(&self, _view: &BinaryView, func: &Function) {
- let Some(guid) = cached_function_guid(func, || func.lifted_il().ok()) else {
- tracing::error!("Could not get guid for copied function");
- return;
- };
- tracing::info!(
- "Function GUID for {:?}... {}",
- func.symbol().short_name(),
- guid
- );
- if let Ok(mut clipboard) = arboard::Clipboard::new() {
- let _ = clipboard.set_text(guid.to_string());
- }
- }
-
- fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
- true
- }
-}
-
-pub struct FindFunctionFromGUID;
-
-impl Command for FindFunctionFromGUID {
- fn action(&self, view: &BinaryView) {
- let Some(guid_str) = binaryninja::interaction::get_text_line_input(
- "Function GUID",
- "Find Function from GUID",
- ) else {
- return;
- };
-
- let Ok(searched_guid) = guid_str.parse::<FunctionGUID>() else {
- tracing::error!("Failed to parse function guid... {}", guid_str);
- return;
- };
-
- tracing::info!("Searching functions for GUID... {}", searched_guid);
- let funcs = view.functions();
- let view = view.to_owned();
- thread::spawn(move || {
- let background_task = BackgroundTask::new(
- &format!("Searching functions for GUID... {}", searched_guid),
- false,
- );
-
- // Only run this for functions which have already generated a GUID.
- let matched: Vec<Guard<Function>> = funcs
- .par_iter()
- .filter(|func| {
- try_cached_function_guid(func).is_some_and(|guid| guid == searched_guid)
- })
- .collect();
-
- if matched.is_empty() {
- tracing::info!("No matches found for GUID... {}", searched_guid);
- } else {
- for func in &matched {
- // Also navigate the user, as that is probably what they want.
- if matched.len() == 1 {
- let current_view = view.file().current_view();
- if view
- .file()
- .navigate_to(&current_view, func.start())
- .is_err()
- {
- tracing::error!(
- "Failed to navigate to found function 0x{:0x} in view {}",
- func.start(),
- current_view
- );
- }
- }
- tracing::info!("Match found at function... 0x{:0x}", func.start());
- }
- }
-
- background_task.finish();
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/project.rs b/plugins/warp/src/plugin/project.rs
deleted file mode 100644
index b6081c1f..00000000
--- a/plugins/warp/src/plugin/project.rs
+++ /dev/null
@@ -1,286 +0,0 @@
-use crate::processor::{
- new_processing_state_background_thread, CompressionTypeField, FileDataKindField,
- FileFilterField, ProcessingFileState, RequestAnalysisField, WarpFileProcessor,
-};
-use crate::report::{ReportGenerator, ReportKindField};
-use binaryninja::background_task::BackgroundTask;
-use binaryninja::command::ProjectCommand;
-use binaryninja::interaction::{Form, FormInputField};
-use binaryninja::project::folder::ProjectFolder;
-use binaryninja::project::Project;
-use binaryninja::rc::Ref;
-use binaryninja::worker_thread::{set_worker_thread_count, worker_thread_count};
-use rayon::ThreadPoolBuilder;
-use regex::Regex;
-use std::path::Path;
-use std::thread;
-use std::time::Instant;
-use warp::WarpFile;
-
-pub struct CreateSignaturesForm {
- form: Form,
-}
-
-impl CreateSignaturesForm {
- pub fn new(_project: &Project) -> CreateSignaturesForm {
- let mut form = Form::new("Create Signature File");
- form.add_field(Self::file_data_field());
- form.add_field(Self::file_filter_field());
- form.add_field(Self::generated_report_field());
- form.add_field(Self::compression_type_field());
- form.add_field(Self::save_individual_files_field());
- form.add_field(Self::skip_existing_warp_files_field());
- form.add_field(Self::request_analysis_field());
- form.add_field(Self::processing_thread_count_field());
- Self { form }
- }
-
- pub fn file_data_field() -> FormInputField {
- FileDataKindField::default().to_field()
- }
-
- pub fn file_data_kind(&self) -> FileDataKindField {
- FileDataKindField::from_form(&self.form).unwrap_or_default()
- }
-
- pub fn file_filter_field() -> FormInputField {
- FileFilterField::to_field()
- }
-
- pub fn file_filter(&self) -> Option<Result<Regex, regex::Error>> {
- FileFilterField::from_form(&self.form)
- }
-
- pub fn generated_report_field() -> FormInputField {
- ReportKindField::default().to_field()
- }
-
- pub fn generated_report_kind(&self) -> ReportKindField {
- ReportKindField::from_form(&self.form).unwrap_or_default()
- }
-
- pub fn compression_type_field() -> FormInputField {
- CompressionTypeField::default().to_field()
- }
-
- pub fn compression_type(&self) -> CompressionTypeField {
- CompressionTypeField::from_form(&self.form).unwrap_or_default()
- }
-
- pub fn save_individual_files_field() -> FormInputField {
- FormInputField::Checkbox {
- prompt: "Save individual files".to_string(),
- default: None,
- value: false,
- }
- }
-
- pub fn save_individual_files(&self) -> bool {
- let field = self.form.get_field_with_name("Save individual files");
- let field_value = field.and_then(|f| f.try_value_int()).unwrap_or(0);
- match field_value {
- 1 => true,
- _ => false,
- }
- }
-
- pub fn skip_existing_warp_files_field() -> FormInputField {
- FormInputField::Checkbox {
- prompt: "Skip existing WARP files".to_string(),
- default: Some(true),
- value: false,
- }
- }
-
- pub fn skip_existing_warp_files(&self) -> bool {
- let field = self.form.get_field_with_name("Skip existing WARP files");
- let field_value = field.and_then(|f| f.try_value_int()).unwrap_or(0);
- match field_value {
- 1 => true,
- _ => false,
- }
- }
-
- pub fn request_analysis_field() -> FormInputField {
- RequestAnalysisField::default().to_field()
- }
-
- pub fn request_analysis(&self) -> RequestAnalysisField {
- RequestAnalysisField::from_form(&self.form).unwrap_or_default()
- }
-
- pub fn processing_thread_count_field() -> FormInputField {
- let default = rayon::current_num_threads();
- FormInputField::Integer {
- prompt: "Processing threads".to_string(),
- default: Some(default as i64),
- value: 0,
- }
- }
-
- pub fn processing_thread_count(&self) -> usize {
- let field = self.form.get_field_with_name("Processing threads");
- let worker_thread_count = worker_thread_count();
- field
- .and_then(|f| f.try_value_int())
- .unwrap_or(worker_thread_count as i64)
- .abs() as usize
- }
-
- pub fn prompt(&mut self) -> bool {
- self.form.prompt()
- }
-}
-
-pub struct CreateSignatures;
-
-impl CreateSignatures {
- pub fn execute(project: Ref<Project>) {
- let mut form = CreateSignaturesForm::new(&project);
- if !form.prompt() {
- return;
- }
- let file_data_kind = form.file_data_kind();
- let report_kind = form.generated_report_kind();
- let compression_type = form.compression_type();
- let save_individual_files = form.save_individual_files();
- let skip_existing_warp_files = form.skip_existing_warp_files();
- let request_analysis = form.request_analysis();
- let processing_thread_count = form.processing_thread_count();
-
- // Save the warp file to the project.
- let save_warp_file = move |project: &Project,
- folder: Option<&ProjectFolder>,
- name: &str,
- warp_file: &WarpFile| {
- if project
- .create_file(&warp_file.to_bytes(), folder, name, "")
- .is_err()
- {
- tracing::error!("Failed to create project file!");
- }
-
- let report = ReportGenerator::new();
- if let Some(generated) = report.report(&report_kind, &warp_file) {
- let ext = report.report_extension(&report_kind).unwrap_or_default();
- let file_name = format!("{}_report.{}", name, ext);
- if project
- .create_file(&generated.into_bytes(), folder, &file_name, "Warp file")
- .is_err()
- {
- tracing::error!("Failed to create project file!");
- }
- }
- };
-
- // Optional callback for saving off the individual project files.
- let callback_project = project.clone();
- let save_individual_files_cb = move |path: &Path, file: &WarpFile| {
- if file.chunks.is_empty() {
- tracing::debug!("Skipping empty file: {}", path.display());
- return;
- }
- // The path returned will be the one on disk, so we will go and grab the project for it.
- let Some(project_file) = callback_project.file_by_path(path) else {
- tracing::error!("Failed to find project file for path: {}", path.display());
- return;
- };
- let project_file = project_file.to_owned();
- let file_name = format!("{}.warp", project_file.name());
- let project_folder = project_file.folder();
- save_warp_file(
- &callback_project,
- project_folder.as_deref(),
- &file_name,
- file,
- );
- };
-
- let mut processor = WarpFileProcessor::new()
- .with_file_data(file_data_kind)
- .with_compression_type(compression_type)
- .with_skip_warp_files(skip_existing_warp_files)
- .with_request_analysis(request_analysis == RequestAnalysisField::Yes);
-
- if save_individual_files {
- processor = processor.with_processed_file_callback(save_individual_files_cb);
- }
-
- // Construct the user-supplied file filter. This will filter files in the project only, files
- // in an archive will be considered a part of the archive file.
- if let Some(filter) = form.file_filter() {
- match filter {
- Ok(f) => {
- processor = processor.with_file_filter(f);
- }
- Err(err) => {
- tracing::error!("Failed to parse file filter: {}", err);
- tracing::error!(
- "Consider using a substring instead of a glob pattern, e.g. *.exe => exe"
- );
- return;
- }
- }
- }
-
- // This thread will show the state in a background task.
- let background_task = BackgroundTask::new("Processing started...", true);
- new_processing_state_background_thread(background_task.clone(), processor.state());
-
- let Ok(thread) = ThreadPoolBuilder::new()
- .num_threads(processing_thread_count)
- .build()
- else {
- tracing::error!("Failed to create processing thread pool!");
- return;
- };
-
- // We have to bump the number of worker threads up so that view destruction's and analysis
- // does not halt, using a multiple of three seems good. This is only temporary.
- let previous_worker_thread_count = worker_thread_count();
- let upgraded_thread_count = previous_worker_thread_count * 3;
- if upgraded_thread_count > previous_worker_thread_count {
- tracing::info!(
- "Setting worker thread count to {} for the duration of processing...",
- upgraded_thread_count
- );
- set_worker_thread_count(upgraded_thread_count);
- }
-
- let start = Instant::now();
- thread.scope(|_| match processor.process_project(&project) {
- Ok(warp_file) => {
- save_warp_file(&project, None, "generated.warp", &warp_file);
- }
- Err(e) => {
- tracing::error!("Failed to process project: {}", e);
- }
- });
-
- let processed_file_count = processor
- .state()
- .files_with_state(ProcessingFileState::Processed);
- tracing::info!(
- "Processing {} project files took: {:?}",
- processed_file_count,
- start.elapsed()
- );
- // Reset the worker thread count to the user specified; either way it will not persist.
- set_worker_thread_count(previous_worker_thread_count);
- // Tells the processing state thread to finish.
- background_task.finish();
- }
-}
-
-impl ProjectCommand for CreateSignatures {
- fn action(&self, project: &Project) {
- let project = project.to_owned();
- thread::spawn(move || {
- CreateSignatures::execute(project);
- });
- }
-
- fn valid(&self, _view: &Project) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/processor.rs b/plugins/warp/src/processor.rs
index 12403ed2..32310d3c 100644
--- a/plugins/warp/src/processor.rs
+++ b/plugins/warp/src/processor.rs
@@ -12,7 +12,7 @@ use dashmap::DashMap;
use rayon::iter::IntoParallelIterator;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rayon::prelude::ParallelSlice;
-use regex::Regex;
+use rayon::{ThreadPoolBuildError, ThreadPoolBuilder};
use serde_json::{json, Value};
use tempdir::TempDir;
use thiserror::Error;
@@ -21,14 +21,13 @@ use walkdir::WalkDir;
use binaryninja::background_task::BackgroundTask;
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
use binaryninja::function::Function as BNFunction;
-use binaryninja::interaction::{Form, FormInputField};
use binaryninja::project::file::ProjectFile;
use binaryninja::project::Project;
use binaryninja::rc::{Guard, Ref};
use crate::cache::cached_type_references;
use crate::convert::platform_to_target;
-use crate::{build_function, INCLUDE_TAG_ICON, INCLUDE_TAG_NAME};
+use crate::{build_function, INCLUDE_TAG_NAME};
use binaryninja::file_metadata::{SaveOption, SaveSettings};
use warp::chunk::{Chunk, ChunkKind, CompressionType};
use warp::r#type::chunk::TypeChunk;
@@ -76,142 +75,28 @@ pub enum ProcessingError {
#[error("Skipping file: {0}")]
SkippedFile(PathBuf),
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct FileFilterField;
-
-impl FileFilterField {
- pub fn to_field() -> FormInputField {
- FormInputField::TextLine {
- prompt: "File Filter".to_string(),
- default: None,
- value: None,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Result<Regex, regex::Error>> {
- let field = form.get_field_with_name("File Filter")?;
- let field_value = field.try_value_string()?;
- // TODO: This is pretty absurd but whatever.
- let pattern = if field_value.contains(['*', '.', '[', '(']) {
- // Assume it's a regex if it contains meta-characters.
- field_value
- } else {
- // Treat it as a substring
- format!(".*{}.*", regex::escape(&field_value))
- };
-
- Some(Regex::new(&pattern))
- }
+ #[error("Failed to create thread pool: {0}")]
+ ThreadPoolCreation(ThreadPoolBuildError),
}
+#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub enum FileDataKindField {
- Symbols,
- Signatures,
- Types,
+pub enum IncludedDataField {
+ Symbols = 0,
+ Signatures = 1,
+ Types = 2,
#[default]
- All,
-}
-
-impl FileDataKindField {
- pub fn to_field(&self) -> FormInputField {
- FormInputField::Choice {
- prompt: "File Data".to_string(),
- choices: vec![
- "Symbols".to_string(),
- "Signatures".to_string(),
- "Types".to_string(),
- "All".to_string(),
- ],
- default: Some(match self {
- Self::Symbols => 0,
- Self::Signatures => 1,
- Self::Types => 2,
- Self::All => 3,
- }),
- value: 0,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("File Data")?;
- let field_value = field.try_value_index()?;
- match field_value {
- 3 => Some(Self::All),
- 2 => Some(Self::Types),
- 1 => Some(Self::Signatures),
- 0 => Some(Self::Symbols),
- _ => None,
- }
- }
+ All = 3,
}
+#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum IncludedFunctionsField {
- Selected,
+ Selected = 0,
#[default]
- Annotated,
- All,
-}
-
-impl IncludedFunctionsField {
- pub fn to_field(&self) -> FormInputField {
- // If the user has selected any functions, change the default value of the included functions field.
- FormInputField::Choice {
- prompt: "Included Functions".to_string(),
- choices: vec![
- format!("Selected {}", INCLUDE_TAG_ICON),
- "Annotated".to_string(),
- "All".to_string(),
- ],
- default: Some(match self {
- Self::Selected => 0,
- Self::Annotated => 1,
- Self::All => 2,
- }),
- value: 0,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("Included Functions")?;
- let field_value = field.try_value_index()?;
- match field_value {
- 2 => Some(Self::All),
- 1 => Some(Self::Annotated),
- 0 => Some(Self::Selected),
- _ => None,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub enum SaveReportToDiskField {
- No,
- #[default]
- Yes,
-}
-
-impl SaveReportToDiskField {
- pub fn to_field(&self) -> FormInputField {
- FormInputField::Checkbox {
- prompt: "Save Report to Disk".to_string(),
- default: Some(true),
- value: false,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("Save Report to Disk")?;
- let field_value = field.try_value_int()?;
- match field_value {
- 1 => Some(Self::Yes),
- _ => Some(Self::No),
- }
- }
+ Annotated = 1,
+ All = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
@@ -221,64 +106,6 @@ pub enum RequestAnalysisField {
Yes,
}
-impl RequestAnalysisField {
- pub fn to_field(&self) -> FormInputField {
- FormInputField::Checkbox {
- prompt: "Request Analysis for BNDB's".to_string(),
- default: Some(true),
- value: false,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("Request Analysis for BNDB's")?;
- let field_value = field.try_value_int()?;
- match field_value {
- 1 => Some(Self::Yes),
- _ => Some(Self::No),
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub enum CompressionTypeField {
- None,
- #[default]
- Zstd,
-}
-
-impl CompressionTypeField {
- pub fn to_field(&self) -> FormInputField {
- FormInputField::Choice {
- prompt: "Compression Type".to_string(),
- choices: vec!["None".to_string(), "Zstd".to_string()],
- default: Some(match self {
- Self::None => 0,
- Self::Zstd => 1,
- }),
- value: 0,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("Compression Type")?;
- let field_value = field.try_value_index()?;
- match field_value {
- 1 => Some(Self::Zstd),
- _ => Some(Self::None),
- }
- }
-}
-
-impl From<CompressionTypeField> for CompressionType {
- fn from(field: CompressionTypeField) -> Self {
- match field {
- CompressionTypeField::None => CompressionType::None,
- CompressionTypeField::Zstd => CompressionType::Zstd,
- }
- }
-}
-
pub fn new_processing_state_background_thread(
task: Ref<BackgroundTask>,
state: Arc<ProcessingState>,
@@ -358,6 +185,15 @@ impl ProcessingState {
}
}
+/// An entry stored in the [`WarpFileProcessor`] to be processed.
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub enum WarpFileProcessorEntry {
+ Path(PathBuf),
+ Project(Ref<Project>),
+ ProjectFile(Ref<ProjectFile>),
+ BinaryView(Ref<BinaryView>),
+}
+
/// Create a new [`WarpFile`] from files, projects, and directories.
#[derive(Clone)]
pub struct WarpFileProcessor {
@@ -370,18 +206,20 @@ pub struct WarpFileProcessor {
// TODO: Databases will require regenerating LLIL in some cases, so we must support generating the LLIL.
/// The path to a folder to intake and output analysis artifacts.
cache_path: Option<PathBuf>,
- file_data: FileDataKindField,
+ file_data: IncludedDataField,
included_functions: IncludedFunctionsField,
- compression_type: CompressionTypeField,
+ compression_type: CompressionType,
processed_file_callback: Option<ProcessedFileCallback>,
- /// Regex pattern used to filter out files.
- file_filter: Option<Regex>,
- // TODO: Merge with file filter.
/// Whether to skip processing warp files.
skip_warp_files: bool,
/// Processor state, this is shareable between threads, so the processor and the consumer can
/// read / write to the state, use this if you want to show a progress indicator.
state: Arc<ProcessingState>,
+ /// The list of entries to process.
+ entries: HashSet<WarpFileProcessorEntry>,
+ /// When processing entries with [`WarpFileProcessor::process_entries`], this will
+ /// be used to specify the number of worker threads to use for processing entries.
+ entry_worker_count: Option<usize>,
}
impl WarpFileProcessor {
@@ -390,21 +228,23 @@ impl WarpFileProcessor {
analysis_settings: json!({
"analysis.linearSweep.autorun": false,
"analysis.signatureMatcher.autorun": false,
- "analysis.mode": "full",
+ "analysis.mode": "intermediate",
// Disable warp when opening views.
- "analysis.warp.guid": false,
+ "analysis.warp.guid": true,
"analysis.warp.matcher": false,
"analysis.warp.apply": false,
}),
- request_analysis: true,
+ // We expect the `build_function` call to be run, so this should be a fine default.
+ request_analysis: false,
cache_path: None,
file_data: Default::default(),
included_functions: Default::default(),
compression_type: Default::default(),
processed_file_callback: None,
- file_filter: None,
skip_warp_files: false,
state: Arc::new(ProcessingState::default()),
+ entries: HashSet::new(),
+ entry_worker_count: None,
}
}
@@ -428,7 +268,7 @@ impl WarpFileProcessor {
self
}
- pub fn with_file_data(mut self, file_data: FileDataKindField) -> Self {
+ pub fn with_file_data(mut self, file_data: IncludedDataField) -> Self {
self.file_data = file_data;
self
}
@@ -438,7 +278,7 @@ impl WarpFileProcessor {
self
}
- pub fn with_compression_type(mut self, compression_type: CompressionTypeField) -> Self {
+ pub fn with_compression_type(mut self, compression_type: CompressionType) -> Self {
self.compression_type = compression_type;
self
}
@@ -451,20 +291,13 @@ impl WarpFileProcessor {
self
}
- pub fn with_file_filter(mut self, file_filter: Regex) -> Self {
- self.file_filter = Some(file_filter);
+ pub fn with_skip_warp_files(mut self, skip: bool) -> Self {
+ self.skip_warp_files = skip;
self
}
- pub fn file_filter(&self, path: &Path) -> bool {
- match (&self.file_filter, path.to_str()) {
- (Some(filter), Some(path)) => filter.is_match(path),
- _ => true,
- }
- }
-
- pub fn with_skip_warp_files(mut self, skip: bool) -> Self {
- self.skip_warp_files = skip;
+ pub fn with_entry_worker_count(mut self, count: usize) -> Self {
+ self.entry_worker_count = Some(count);
self
}
@@ -485,7 +318,55 @@ impl WarpFileProcessor {
Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks))
}
- pub fn process(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> {
+ /// Add an entry to be processed later by [`WarpFileProcessor::process_entries`].
+ pub fn add_entry(&mut self, entry: WarpFileProcessorEntry) {
+ self.entries.insert(entry);
+ }
+
+ /// Process all entries in the processor, merging them into a single [`WarpFile`].
+ ///
+ /// The entries list will be cleared after processing to allow the processor to be reused.
+ ///
+ /// Because entries are processed in parallel, it is advised to set the worker count to a reasonable
+ /// amount to avoid excessive resource usage and to ensure optimal performance.
+ pub fn process_entries(&mut self) -> Result<WarpFile<'static>, ProcessingError> {
+ let thread_pool = match self.entry_worker_count {
+ Some(count) => ThreadPoolBuilder::new()
+ .num_threads(count)
+ .build()
+ .map_err(ProcessingError::ThreadPoolCreation)?,
+ None => ThreadPoolBuilder::new()
+ .build()
+ .map_err(ProcessingError::ThreadPoolCreation)?,
+ };
+
+ let unmerged_files: Result<Vec<_>, _> = thread_pool.install(|| {
+ self.entries
+ .par_iter()
+ .map(|e| self.process_entry(e))
+ .collect()
+ });
+ self.entries.clear();
+ self.merge_files(unmerged_files?)
+ }
+
+ pub fn process_entry(
+ &self,
+ entry: &WarpFileProcessorEntry,
+ ) -> Result<WarpFile<'static>, ProcessingError> {
+ match entry {
+ WarpFileProcessorEntry::Path(path) => self.process_path(path.clone()),
+ WarpFileProcessorEntry::Project(project) => self.process_project(&project),
+ WarpFileProcessorEntry::ProjectFile(project_file) => {
+ self.process_project_file(&project_file)
+ }
+ WarpFileProcessorEntry::BinaryView(view) => {
+ self.process_view(view.file().file_path(), &view)
+ }
+ }
+ }
+
+ pub fn process_path(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> {
let file = match path.extension() {
Some(ext) if ext == "a" || ext == "lib" || ext == "rlib" => {
self.process_archive(path.clone())
@@ -507,18 +388,7 @@ impl WarpFileProcessor {
}
pub fn process_project(&self, project: &Project) -> Result<WarpFile<'static>, ProcessingError> {
- let filter_project_file = |file: &Guard<ProjectFile>| {
- let path = project_file_path(file);
- self.file_filter(&path)
- };
-
- let files: Vec<_> = project
- .files()
- .iter()
- .filter(filter_project_file)
- .map(|f| f.to_owned())
- .collect();
-
+ let files = project.files();
// Inform the state of the new unprocessed project files.
for project_file in &files {
// NOTE: We use the on disk path here because the downstream file state uses that.
@@ -532,7 +402,7 @@ impl WarpFileProcessor {
.par_iter()
.map(|file| {
self.check_cancelled()?;
- self.process_project_file(file)
+ self.process_project_file(&file)
})
.filter_map(|res| match res {
Ok(result) => Some(Ok(result)),
@@ -686,7 +556,7 @@ impl WarpFileProcessor {
.into_iter()
.filter_map(|e| {
let path = e.ok()?.into_path();
- if path.is_file() && self.file_filter(&path) {
+ if path.is_file() {
Some(path)
} else {
None
@@ -706,7 +576,7 @@ impl WarpFileProcessor {
.inspect(|path| tracing::debug!("Processing file: {:?}", path))
.map(|path| {
self.check_cancelled()?;
- self.process(path)
+ self.process_path(path)
})
.filter_map(|res| match res {
Ok(result) => Some(Ok(result)),
@@ -800,7 +670,7 @@ impl WarpFileProcessor {
.set_file_state(path.clone(), ProcessingFileState::Processing);
let mut chunks = Vec::new();
- if self.file_data != FileDataKindField::Types {
+ if self.file_data != IncludedDataField::Types {
let mut signature_chunks = self.create_signature_chunks(view)?;
for (target, mut target_chunks) in signature_chunks.drain() {
for signature_chunk in target_chunks.drain(..) {
@@ -816,7 +686,7 @@ impl WarpFileProcessor {
}
}
- if self.file_data != FileDataKindField::Signatures {
+ if self.file_data != IncludedDataField::Signatures {
let type_chunk = self.create_type_chunk(view)?;
if type_chunk.raw_types().next().is_some() {
chunks.push(Chunk::new(
@@ -857,7 +727,8 @@ impl WarpFileProcessor {
let background_task = BackgroundTask::new(
&format!("Generating signatures... ({}/{})", 0, total_functions),
true,
- );
+ )
+ .enter();
// Create all of the "built" functions, for the chunk.
// NOTE: This does a bit of filtering to remove undesired functions, look at this if
@@ -883,7 +754,7 @@ impl WarpFileProcessor {
let built_function = build_function(
&func,
|| func.lifted_il().ok(),
- self.file_data == FileDataKindField::Symbols,
+ self.file_data == IncludedDataField::Symbols,
)?;
Some((target, built_function))
})
@@ -917,7 +788,6 @@ impl WarpFileProcessor {
})
.collect();
- background_task.finish();
chunks
}
@@ -944,7 +814,6 @@ impl Debug for WarpFileProcessor {
.field("file_data", &self.file_data)
.field("compression_type", &self.compression_type)
.field("included_functions", &self.included_functions)
- .field("file_filter", &self.file_filter)
.field("state", &self.state)
.field("cache_path", &self.cache_path)
.field("analysis_settings", &self.analysis_settings)
@@ -952,17 +821,3 @@ impl Debug for WarpFileProcessor {
.finish()
}
}
-
-fn project_file_path(file: &ProjectFile) -> PathBuf {
- // Recurse up the folders to build a string like /foldera/folderb/myfile
- let mut path = PathBuf::new();
- // Add file name
- path.push(file.name());
- // Recursively add parent folder names
- let mut current = file.folder();
- while let Some(folder) = current {
- path = PathBuf::from(folder.name()).join(path);
- current = folder.parent();
- }
- path
-}
diff --git a/plugins/warp/src/report.rs b/plugins/warp/src/report.rs
deleted file mode 100644
index 8ba9cc8b..00000000
--- a/plugins/warp/src/report.rs
+++ /dev/null
@@ -1,185 +0,0 @@
-use binaryninja::interaction::{Form, FormInputField};
-use minijinja::Environment;
-use serde::Serialize;
-use warp::chunk::{Chunk, ChunkKind};
-use warp::r#type::guid::TypeGUID;
-use warp::WarpFile;
-
-#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub enum ReportKindField {
- None,
- #[default]
- Html,
- Markdown,
- Json,
-}
-
-impl ReportKindField {
- pub fn to_field(&self) -> FormInputField {
- FormInputField::Choice {
- prompt: "Generated Report".to_string(),
- choices: vec![
- "None".to_string(),
- "HTML".to_string(),
- "Markdown".to_string(),
- "JSON".to_string(),
- ],
- default: Some(match self {
- Self::None => 0,
- Self::Html => 1,
- Self::Markdown => 2,
- Self::Json => 3,
- }),
- value: 0,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("Generated Report")?;
- let field_value = field.try_value_index()?;
- match field_value {
- 3 => Some(Self::Json),
- 2 => Some(Self::Markdown),
- 1 => Some(Self::Html),
- _ => Some(Self::None),
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct ReportGenerator {
- environment: Environment<'static>,
-}
-
-impl ReportGenerator {
- pub fn new() -> Self {
- let mut environment = Environment::new();
- // Remove trailing lines for blocks, this is required for Markdown tables.
- environment.set_trim_blocks(true);
- minijinja_embed::load_templates!(&mut environment);
- Self { environment }
- }
-
- pub fn report(&self, kind: &ReportKindField, file: &WarpFile) -> Option<String> {
- match kind {
- ReportKindField::None => None,
- ReportKindField::Html => self.html_report(file),
- ReportKindField::Markdown => self.markdown_report(file),
- ReportKindField::Json => self.json_report(file),
- }
- }
-
- pub fn report_extension(&self, kind: &ReportKindField) -> Option<&'static str> {
- match kind {
- ReportKindField::None => None,
- ReportKindField::Html => Some("html"),
- ReportKindField::Markdown => Some("md"),
- ReportKindField::Json => Some("json"),
- }
- }
-
- pub fn html_report(&self, file: &WarpFile) -> Option<String> {
- let data = FileReportData::new(file);
- let tmpl = self.environment.get_template("file.html").ok()?;
- tmpl.render(data).ok()
- }
-
- pub fn markdown_report(&self, file: &WarpFile) -> Option<String> {
- let data = FileReportData::new(file);
- let tmpl = self.environment.get_template("file.md").ok()?;
- tmpl.render(data).ok()
- }
-
- pub fn json_report(&self, file: &WarpFile) -> Option<String> {
- let data = FileReportData::new(file);
- let tmpl = self.environment.get_template("file.json").ok()?;
- tmpl.render(data).ok()
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FileReportData {
- pub title: String,
- // pub header: WarpFileHeader,
- pub chunks: Vec<ChunkReportData>,
-}
-
-impl FileReportData {
- pub fn new(file: &WarpFile) -> Self {
- Self {
- title: "Warp File Report".to_string(),
- // header: file.header.clone(),
- chunks: file
- .chunks
- .iter()
- .map(|chunk| ChunkReportData::new(chunk))
- .collect(),
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ChunkReportData {
- pub title: String,
- // pub header: ChunkHeader,
- pub target: String,
- pub total_item_count: usize,
- /// View into a (possible subset) of chunk items.
- pub item_view: Vec<ItemReportData>,
-}
-
-impl ChunkReportData {
- pub fn new(chunk: &Chunk) -> Self {
- // TODO: Set a limit for the number of items so we dont construct 10000000 items in the report.
- let items: Vec<_> = match &chunk.kind {
- ChunkKind::Signature(sc) => sc
- .raw_functions()
- .map(|f| ItemReportData {
- name: f.symbol().and_then(|s| s.name().map(|n| n.to_string())),
- guid: f.guid().to_string(),
- note: None,
- })
- .collect(),
- ChunkKind::Type(tc) => tc
- .raw_types()
- .map(|t| ItemReportData {
- name: t.type_().and_then(|s| s.name().map(|n| n.to_string())),
- guid: TypeGUID::from(t.guid()).to_string(),
- note: None,
- })
- .collect(),
- };
-
- let chunk_type = match &chunk.kind {
- ChunkKind::Signature(_) => "Signature".to_string(),
- ChunkKind::Type(_) => "Type".to_string(),
- };
-
- let size_in_kb = chunk.header.size as f64 / 1024.0;
- let formatted_size = format!("{:.1}kb", size_in_kb);
-
- // For the target show the platform, or the architecture if available.
- let target = chunk
- .header
- .target
- .platform
- .clone()
- .or_else(|| chunk.header.target.architecture.clone())
- .unwrap_or_else(|| "None".to_string());
-
- Self {
- title: format!("{} Chunk ({})", chunk_type, formatted_size),
- target,
- // header: chunk.header.clone(),
- total_item_count: items.len(),
- item_view: items,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ItemReportData {
- pub guid: String,
- pub name: Option<String>,
- pub note: Option<String>,
-}
diff --git a/plugins/warp/src/templates/file.html b/plugins/warp/src/templates/file.html
deleted file mode 100644
index 7ea465ef..00000000
--- a/plugins/warp/src/templates/file.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>{{ title }}</title>
-</head>
-<body>
-<h1>{{ title }}</h1>
-
-{% for chunk in chunks %}
-<section>
- <h2>{{ chunk.title }}</h2>
- <p>Target: {{ chunk.target }}</p>
- <p>Total items: {{ chunk.total_item_count }}</p>
-
- <table>
- <thead>
- <tr>
- <th>GUID</th>
- <th>Name</th>
- <th>Note</th>
- </tr>
- </thead>
- <tbody>
- {% for item in chunk.item_view %}
- <tr>
- <td>{{ item.guid }}</td>
- <td>{{ item.name or 'N/A' }}</td>
- <td>{{ item.note or 'N/A' }}</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
-</section>
-{% endfor %}
-</body>
-</html> \ No newline at end of file
diff --git a/plugins/warp/src/templates/file.json b/plugins/warp/src/templates/file.json
deleted file mode 100644
index 764d35cd..00000000
--- a/plugins/warp/src/templates/file.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "title": "{{ title }}",
- "chunks": [
- {% for chunk in chunks %}
- {
- "title": "{{ chunk.title }}",
- "target": "{{ chunk.target }}",
- "total_item_count": {{ chunk.total_item_count }},
- "item_view": [
- {% for item in chunk.item_view %}
- {
- "guid": "{{ item.guid }}",
- "name": "{{ item.name or 'N/A' }}",
- "note": "{{ item.note or 'N/A' }}"
- }{% if not loop.last %},{% endif %}
- {% endfor %}
- ]
- }{% if not loop.last %},{% endif %}
- {% endfor %}
- ]
-} \ No newline at end of file
diff --git a/plugins/warp/src/templates/file.md b/plugins/warp/src/templates/file.md
deleted file mode 100644
index 433cb8e9..00000000
--- a/plugins/warp/src/templates/file.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# {{ title }}
-
-{% for chunk in chunks %}
-## {{ chunk.title }}
-
-Target: {{ chunk.target }}
-
-Total items: {{ chunk.total_item_count }}
-
-| GUID | Name | Note |
-|--------------|--------------|--------------|
-{% for item in chunk.item_view -%}
-| {{ item.guid }} | {{ item.name or 'N/A' }} | {{ item.note or 'N/A' }} |
-{% endfor %}
-{% endfor %}
diff --git a/plugins/warp/tests/processor.rs b/plugins/warp/tests/processor.rs
index 12749779..835b33ec 100644
--- a/plugins/warp/tests/processor.rs
+++ b/plugins/warp/tests/processor.rs
@@ -27,12 +27,12 @@ fn test_processor() {
// All files should process and not error.
for file_name in BIN_TARGET_FILES {
let path = out_dir.join(file_name);
- processor.process(path).unwrap();
+ processor.process_path(path).unwrap();
}
// We should be able to process a warp file.
let warp_path = out_dir.join("random.warp");
- processor.process(warp_path).unwrap();
+ processor.process_path(warp_path).unwrap();
}
#[test]
@@ -47,7 +47,7 @@ fn test_caching() {
// Go through files, this should cache the databases.
for file_name in BIN_TARGET_FILES {
let path = out_dir.join(file_name);
- processor.process(path).unwrap();
+ processor.process_path(path).unwrap();
}
// Verify the databases were saved to the cache.
diff --git a/plugins/warp/ui/CMakeLists.txt b/plugins/warp/ui/CMakeLists.txt
index 328289ac..d95ea6a7 100644
--- a/plugins/warp/ui/CMakeLists.txt
+++ b/plugins/warp/ui/CMakeLists.txt
@@ -12,7 +12,13 @@ file(GLOB SOURCES CONFIGURE_DEPENDS
containers.cpp containers.h
shared/search.cpp shared/search.h
shared/fetcher.cpp shared/fetcher.h
- shared/fetchdialog.cpp shared/fetchdialog.h)
+ shared/fetchdialog.cpp shared/fetchdialog.h
+ shared/processordialog.cpp shared/processordialog.h
+ shared/commitdialog.cpp shared/commitdialog.h
+ shared/file.cpp shared/file.h
+ shared/chunk.cpp shared/chunk.h
+ shared/source.cpp shared/source.h
+ shared/selectprojectfilesdialog.cpp shared/selectprojectfilesdialog.h)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
diff --git a/plugins/warp/ui/containers.cpp b/plugins/warp/ui/containers.cpp
index f800f977..49415a52 100644
--- a/plugins/warp/ui/containers.cpp
+++ b/plugins/warp/ui/containers.cpp
@@ -1,94 +1,5 @@
#include "containers.h"
-QVariant WarpSourcesModel::data(const QModelIndex& index, int role) const
-{
- if (!index.isValid())
- return {};
- if (index.row() < 0 || index.row() >= rowCount())
- return {};
-
- const auto& r = m_rows[static_cast<size_t>(index.row())];
-
- // Build a small two-dot status icon (left: writable, right: uncommitted)
- auto statusIcon = [](bool writable, bool uncommitted) -> QIcon {
- static QIcon cache[2][2]; // [writable][uncommitted]
- QIcon& cached = cache[writable ? 1 : 0][uncommitted ? 1 : 0];
- if (!cached.isNull())
- return cached;
-
- const int w = 16, h = 12, radius = 4;
- QPixmap pm(w, h);
- pm.fill(Qt::transparent);
- QPainter p(&pm);
- p.setRenderHint(QPainter::Antialiasing, true);
-
- // Colors
- QColor writableOn(76, 175, 80); // green
- QColor writableOff(158, 158, 158); // grey
- QColor uncommittedOn(255, 193, 7); // amber
- QColor uncommittedOff(158, 158, 158); // grey
-
- // Left dot: writable
- p.setBrush(writable ? writableOn : writableOff);
- p.setPen(Qt::NoPen);
- p.drawEllipse(QPoint(4, h / 2), radius, radius);
-
- // Right dot: uncommitted
- p.setBrush(uncommitted ? uncommittedOn : uncommittedOff);
- p.drawEllipse(QPoint(w - 6, h / 2), radius, radius);
-
- p.end();
- cached = QIcon(pm);
- return cached;
- };
-
- if (role == Qt::DecorationRole && index.column() == PathCol)
- {
- return statusIcon(r.writable, r.uncommitted);
- }
-
- if (role == Qt::ToolTipRole && index.column() == PathCol)
- {
- QStringList parts;
- parts << (r.writable ? "Writable" : "Read-only");
- parts << (r.uncommitted ? "Uncommitted changes" : "No uncommitted changes");
- return parts.join(" • ");
- }
-
- if (role == Qt::DisplayRole)
- {
- switch (index.column())
- {
- case GuidCol:
- return r.guid;
- case PathCol:
- return r.path;
- case WritableCol:
- return r.writable ? "Yes" : "No";
- case UncommittedCol:
- return r.uncommitted ? "Yes" : "No";
- default:
- return {};
- }
- }
-
- if (role == Qt::CheckStateRole)
- {
- // Optional: expose as checkboxes if someone ever shows these columns
- switch (index.column())
- {
- case WritableCol:
- return r.writable ? Qt::Checked : Qt::Unchecked;
- case UncommittedCol:
- return r.uncommitted ? Qt::Checked : Qt::Unchecked;
- default:
- break;
- }
- }
-
- return {};
-}
-
WarpContainerWidget::WarpContainerWidget(Warp::Ref<Warp::Container> container, QWidget* parent) : QWidget(parent)
{
m_container = std::move(container);
@@ -100,95 +11,14 @@ WarpContainerWidget::WarpContainerWidget(Warp::Ref<Warp::Container> container, Q
// Sources tab
m_sourcesPage = new QWidget(this);
auto* sourcesLayout = new QVBoxLayout(m_sourcesPage);
- m_sourcesView = new QTableView(m_sourcesPage);
- m_sourcesModel = new WarpSourcesModel(m_sourcesPage);
- m_sourcesModel->setContainer(m_container);
- m_sourcesView->setModel(m_sourcesModel);
- m_sourcesView->horizontalHeader()->setStretchLastSection(true);
- m_sourcesView->setSelectionBehavior(QAbstractItemView::SelectRows);
- m_sourcesView->setSelectionMode(QAbstractItemView::SingleSelection);
-
- // Make the table look like a simple list that shows only the source path
- m_sourcesView->setShowGrid(false);
- m_sourcesView->verticalHeader()->setVisible(false);
- m_sourcesView->horizontalHeader()->setVisible(false);
- m_sourcesView->setAlternatingRowColors(false);
- m_sourcesView->setEditTriggers(QAbstractItemView::NoEditTriggers);
- m_sourcesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
- m_sourcesView->setWordWrap(false);
- m_sourcesView->setIconSize(QSize(16, 12));
- // Ensure long paths truncate from the left: "...tail/of/the/path"
- m_sourcesView->setTextElideMode(Qt::ElideLeft);
- // Hide GUID column, keep only the Path column visible
- m_sourcesView->setColumnHidden(WarpSourcesModel::GuidCol, true);
- // Also hide boolean columns; their state is shown as an icon next to the path
- m_sourcesView->setColumnHidden(WarpSourcesModel::WritableCol, true);
- m_sourcesView->setColumnHidden(WarpSourcesModel::UncommittedCol, true);
- // Ensure the remaining (Path) column fills the width
- m_sourcesView->horizontalHeader()->setSectionResizeMode(WarpSourcesModel::PathCol, QHeaderView::Stretch);
-
- // Per-item context menu
- m_sourcesView->setContextMenuPolicy(Qt::CustomContextMenu);
- connect(m_sourcesView, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
- QMenu menu(m_sourcesView);
- const QModelIndex index = m_sourcesView->indexAt(pos);
-
- if (!index.isValid())
- {
- QAction* actAdd = menu.addAction(tr("Add Source"));
- QAction* chosen = menu.exec(m_sourcesView->viewport()->mapToGlobal(pos));
- if (!chosen)
- return;
- if (chosen == actAdd)
- {
- std::string sourceName;
- if (!BinaryNinja::GetTextLineInput(sourceName, "Source name:", "Add Source"))
- return;
- if (const auto sourceId = m_container->AddSource(sourceName); !sourceId.has_value())
- {
- BinaryNinja::LogAlertF("Failed to add source: {}", sourceName);
- return;
- }
- m_sourcesModel->reload();
- }
- }
- else
- {
- m_sourcesView->setCurrentIndex(index.sibling(index.row(), WarpSourcesModel::PathCol));
-
- const int row = index.row();
- const QModelIndex pathIdx = m_sourcesModel->index(row, WarpSourcesModel::PathCol);
- const QModelIndex guidIdx = m_sourcesModel->index(row, WarpSourcesModel::GuidCol);
- const QString path = m_sourcesModel->data(pathIdx, Qt::DisplayRole).toString();
- const QFileInfo fi(path);
-
- const QString guid = m_sourcesModel->data(guidIdx, Qt::DisplayRole).toString();
-
- QAction* actReveal = menu.addAction(tr("Reveal in File Browser"));
- actReveal->setEnabled(fi.exists());
- QAction* actCopyPath = menu.addAction(tr("Copy Path"));
- QAction* actCopyGuid = menu.addAction(tr("Copy GUID"));
-
- QAction* chosen = menu.exec(m_sourcesView->viewport()->mapToGlobal(pos));
- if (!chosen)
- return;
- if (chosen == actCopyPath)
- QGuiApplication::clipboard()->setText(path);
- else if (chosen == actCopyGuid)
- QGuiApplication::clipboard()->setText(guid);
- else if (chosen == actReveal)
- QDesktopServices::openUrl(QUrl::fromLocalFile(fi.absoluteFilePath()));
- }
- });
+ m_sourcesView = new WarpSourcesView(m_sourcesPage);
+ m_sourcesView->setContainer(m_container);
sourcesLayout->addWidget(m_sourcesView);
m_tabs->addTab(m_sourcesPage, tr("Sources"));
- // Search tab
- m_searchTab = new WarpSearchWidget(m_container, this);
- m_tabs->addTab(m_searchTab, tr("Search"));
-
+ // TODO: Maybe introduce some callbacks or something, but i feel like this is fine for now.
// Periodic refresh timer for the Sources view
m_refreshTimer = new QTimer(this);
m_refreshTimer->setInterval(5000);
@@ -197,25 +27,26 @@ WarpContainerWidget::WarpContainerWidget(Warp::Ref<Warp::Container> container, Q
if (!this->isVisible() || !m_sourcesPage || !m_sourcesPage->isVisible())
return;
+ WarpSourcesModel* sourcesModel = m_sourcesView->sourceModel();
// Preserve selection by GUID across reloads
QString currentGuid;
if (const QModelIndex currentIdx = m_sourcesView->currentIndex(); currentIdx.isValid())
{
const int row = currentIdx.row();
- const QModelIndex guidIdx = m_sourcesModel->index(row, WarpSourcesModel::GuidCol);
- currentGuid = m_sourcesModel->data(guidIdx, Qt::DisplayRole).toString();
+ const QModelIndex guidIdx = sourcesModel->index(row, WarpSourcesModel::GuidCol);
+ currentGuid = sourcesModel->data(guidIdx, Qt::DisplayRole).toString();
}
- m_sourcesModel->reload();
+ sourcesModel->reload();
if (!currentGuid.isEmpty())
{
- for (int r = 0; r < m_sourcesModel->rowCount(); ++r)
+ for (int r = 0; r < sourcesModel->rowCount(); ++r)
{
- const QModelIndex gIdx = m_sourcesModel->index(r, WarpSourcesModel::GuidCol);
- if (m_sourcesModel->data(gIdx, Qt::DisplayRole).toString() == currentGuid)
+ const QModelIndex gIdx = sourcesModel->index(r, WarpSourcesModel::GuidCol);
+ if (sourcesModel->data(gIdx, Qt::DisplayRole).toString() == currentGuid)
{
- m_sourcesView->setCurrentIndex(m_sourcesModel->index(r, WarpSourcesModel::PathCol));
+ m_sourcesView->setCurrentIndex(sourcesModel->index(r, WarpSourcesModel::PathCol));
break;
}
}
@@ -223,11 +54,11 @@ WarpContainerWidget::WarpContainerWidget(Warp::Ref<Warp::Container> container, Q
});
m_refreshTimer->start();
- // Optional: force a refresh when switching back to the Sources tab
+ // TODO: Do we want to reload this on tab changed???
connect(m_tabs, &QTabWidget::currentChanged, this, [this](const int idx) {
QWidget* w = m_tabs->widget(idx);
if (w == m_sourcesPage)
- m_sourcesModel->reload();
+ m_sourcesView->sourceModel()->reload();
});
}
diff --git a/plugins/warp/ui/containers.h b/plugins/warp/ui/containers.h
index b95c32db..935b552e 100644
--- a/plugins/warp/ui/containers.h
+++ b/plugins/warp/ui/containers.h
@@ -1,8 +1,5 @@
#pragma once
-#include <QWidget>
-#include <optional>
-#include <QClipboard>
#include <QDesktopServices>
#include <QInputDialog>
#include <QListWidget>
@@ -11,93 +8,7 @@
#include "theme.h"
#include "warp.h"
#include "../../../../ui/mainwindow.h"
-
-class WarpSourcesModel final : public QAbstractTableModel
-{
- Q_OBJECT
-
-public:
- enum Columns : int
- {
- GuidCol = 0,
- PathCol,
- WritableCol,
- UncommittedCol,
- ColumnCount
- };
-
- explicit WarpSourcesModel(QObject* parent = nullptr) : QAbstractTableModel(parent) {}
-
- void setContainer(Warp::Ref<Warp::Container> container)
- {
- m_container = std::move(container);
- reload();
- }
-
- void reload()
- {
- // Fetch synchronously (can be adapted to async if needed)
- beginResetModel();
- m_rows.clear();
- for (const auto& src : m_container->GetSources())
- {
- QString guid = QString::fromStdString(src.ToString());
- QString path = QString::fromStdString(m_container->SourcePath(src).value_or(std::string {}));
- bool writable = m_container->IsSourceWritable(src);
- bool uncommitted = m_container->IsSourceUncommitted(src);
- m_rows.push_back({guid, path, writable, uncommitted});
- }
- endResetModel();
- }
-
- int rowCount(const QModelIndex& parent = QModelIndex()) const override
- {
- if (parent.isValid())
- return 0;
- return static_cast<int>(m_rows.size());
- }
-
- int columnCount(const QModelIndex& parent = QModelIndex()) const override
- {
- Q_UNUSED(parent);
- return ColumnCount;
- }
-
- QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
-
- QVariant headerData(int section, Qt::Orientation orientation, int role) const override
- {
- if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
- {
- switch (section)
- {
- case GuidCol:
- return "Source GUID";
- case PathCol:
- return "Path";
- case WritableCol:
- return "Writable";
- case UncommittedCol:
- return "Uncommitted";
- default:
- return {};
- }
- }
- return {};
- }
-
-private:
- struct Row
- {
- QString guid;
- QString path;
- bool writable;
- bool uncommitted;
- };
-
- std::vector<Row> m_rows;
- Warp::Ref<Warp::Container> m_container;
-};
+#include "shared/source.h"
class WarpContainerWidget : public QWidget
{
@@ -112,8 +23,7 @@ private:
QTabWidget* m_tabs = nullptr;
// Sources
- QTableView* m_sourcesView = nullptr;
- WarpSourcesModel* m_sourcesModel = nullptr;
+ WarpSourcesView* m_sourcesView = nullptr;
QWidget* m_sourcesPage = nullptr;
QTimer* m_refreshTimer = nullptr;
diff --git a/plugins/warp/ui/matched.cpp b/plugins/warp/ui/matched.cpp
index 786b9d26..f3132938 100644
--- a/plugins/warp/ui/matched.cpp
+++ b/plugins/warp/ui/matched.cpp
@@ -1,10 +1,7 @@
#include "matched.h"
-
-#include <QGridLayout>
-
#include "theme.h"
-const char* WARP_APPLY_ACTIVITY = "analysis.warp.apply";
+#include <QGridLayout>
WarpMatchedWidget::WarpMatchedWidget(BinaryViewRef current)
{
@@ -28,7 +25,8 @@ WarpMatchedWidget::WarpMatchedWidget(BinaryViewRef current)
m_splitter->addWidget(m_tableWidget);
// Removes the match for the function, this is irreversible currently, and the user must run the matcher again.
- // TODO: We previously were trying to instead toggle the application of the match, but because the symbols are applied
+ // TODO: We previously were trying to instead toggle the application of the match, but because the symbols are
+ // applied
// TODO: when applying the match metadata we would persist that regardless.
m_tableWidget->RegisterContextMenuAction(
"Remove Match", [this](WarpFunctionItem*, std::optional<uint64_t> address) {
@@ -76,7 +74,8 @@ void WarpMatchedWidget::Update()
for (const auto& analysisFunction : m_current->GetAnalysisFunctionList())
{
if (const auto& matchedFunction = Warp::Function::GetMatched(*analysisFunction))
- m_tableWidget->InsertFunction(analysisFunction->GetStart(), new WarpFunctionItem(matchedFunction, analysisFunction));
+ m_tableWidget->InsertFunction(
+ analysisFunction->GetStart(), new WarpFunctionItem(matchedFunction, analysisFunction));
else
m_tableWidget->RemoveFunction(analysisFunction->GetStart());
}
diff --git a/plugins/warp/ui/matches.cpp b/plugins/warp/ui/matches.cpp
index cede06ab..918312fd 100644
--- a/plugins/warp/ui/matches.cpp
+++ b/plugins/warp/ui/matches.cpp
@@ -1,15 +1,13 @@
-#include <QGridLayout>
-#include <QHeaderView>
-
#include "matches.h"
+#include "theme.h"
+#include "warp.h"
+#include "shared/misc.h"
#include <QClipboard>
#include <QFormLayout>
#include <thread>
-
-#include "theme.h"
-#include "warp.h"
-#include "shared/misc.h"
+#include <QGridLayout>
+#include <QHeaderView>
WarpCurrentFunctionWidget::WarpCurrentFunctionWidget(QWidget* parent) : QWidget(parent)
{
@@ -74,23 +72,24 @@ WarpCurrentFunctionWidget::WarpCurrentFunctionWidget(QWidget* parent) : QWidget(
m_tableWidget->GetModel()->SetMatchedFunction(selectedFunction);
});
// If the selected function is the current match, let the user remove the match.
- m_tableWidget->RegisterContextMenuAction("Remove Match",
+ m_tableWidget->RegisterContextMenuAction(
+ "Remove Match",
[this](WarpFunctionItem*, std::optional<uint64_t>) {
WarpRemoveMatchDialog dlg(this, m_current);
if (dlg.execute())
m_tableWidget->GetModel()->SetMatchedFunction(nullptr);
},
[this](WarpFunctionItem* item, std::optional<uint64_t>) {
- if (item == nullptr)
- return false;
- Warp::Ref<Warp::Function> selectedFunction = item->GetFunction();
- if (!selectedFunction)
- return false;
- Warp::Ref<Warp::Function> matchedFunction = m_tableWidget->GetModel()->GetMatchedFunction();
- if (!matchedFunction)
- return false;
- return BNWARPFunctionsEqual(selectedFunction->m_object, matchedFunction->m_object);
- });
+ if (item == nullptr)
+ return false;
+ Warp::Ref<Warp::Function> selectedFunction = item->GetFunction();
+ if (!selectedFunction)
+ return false;
+ Warp::Ref<Warp::Function> matchedFunction = m_tableWidget->GetModel()->GetMatchedFunction();
+ if (!matchedFunction)
+ return false;
+ return BNWARPFunctionsEqual(selectedFunction->m_object, matchedFunction->m_object);
+ });
m_tableWidget->RegisterContextMenuAction(
"Search for Source", [this](WarpFunctionItem* item, std::optional<uint64_t>) {
// Apply the source as the filter.
diff --git a/plugins/warp/ui/plugin.cpp b/plugins/warp/ui/plugin.cpp
index e99059b9..0cf8deea 100644
--- a/plugins/warp/ui/plugin.cpp
+++ b/plugins/warp/ui/plugin.cpp
@@ -1,12 +1,15 @@
#include "plugin.h"
-
-#include <QToolBar>
-
#include "matched.h"
#include "matches.h"
#include "symbollist.h"
#include "viewframe.h"
+#include "shared/processordialog.h"
#include "shared/fetchdialog.h"
+#include "shared/file.h"
+
+#include <QToolBar>
+#include <QVBoxLayout>
+#include <utility>
using namespace BinaryNinja;
@@ -61,7 +64,7 @@ void ShowNetworkNotice()
}
}
-WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP"), m_data(data)
+WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP"), m_data(std::move(data))
{
m_logger = LogRegistry::CreateLogger("WARP UI");
m_currentFrame = nullptr;
@@ -85,30 +88,25 @@ WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP")
});
fetchAction->setToolTip("Fetch data from WARP containers");
- auto commitIcon = GetColoredIcon(":/icons/images/arrow-push.png", getThemeColor(BlueStandardHighlightColor));
- auto commitAction = headerToolbar->addAction(commitIcon, "Commit a WARP file to a source", [this]() {
- UIActionHandler* handler = m_currentFrame->getCurrentViewInterface()->actionHandler();
- handler->executeAction("WARP\\Commit File");
+ auto processIcon = GetColoredIcon(":/icons/images/plus.png", getThemeColor(BlueStandardHighlightColor));
+ auto processAction = headerToolbar->addAction(processIcon, "Process files or views for WARP", [this]() {
+ auto* dialog = new ProcessorDialog(this);
+ dialog->setAttribute(Qt::WA_DeleteOnClose);
+ dialog->onAddBinaryView(m_data);
+ dialog->show();
});
- commitAction->setToolTip("Commit a WARP file to a source");
+ processAction->setToolTip("Process files or views for WARP");
// We want to make it clear that the container actions for fetching and pushing are seperate.
headerToolbar->addSeparator();
- auto loadIcon = GetColoredIcon(":/icons/images/file-add.png", getThemeColor(BlueStandardHighlightColor));
+ auto loadIcon = GetColoredIcon(":/icons/images/archive.png", getThemeColor(BlueStandardHighlightColor));
auto loadAction = headerToolbar->addAction(loadIcon, "Load Signature File", [this]() {
UIActionHandler* handler = m_currentFrame->getCurrentViewInterface()->actionHandler();
handler->executeAction("WARP\\Load File");
});
loadAction->setToolTip("Load a signature file to match against");
- auto saveIcon = GetColoredIcon(":/icons/images/edit.png", getThemeColor(BlueStandardHighlightColor));
- auto saveAction = headerToolbar->addAction(saveIcon, "Create Signature File", [this]() {
- UIActionHandler* handler = m_currentFrame->getCurrentViewInterface()->actionHandler();
- handler->executeAction("WARP\\Create\\From Current View");
- });
- saveAction->setToolTip("Save data to a signature file");
-
headerToolbar->addSeparator();
static auto matcherStopIcon = GetColoredIcon(":/icons/images/stop.png", getThemeColor(RedStandardHighlightColor));
@@ -234,10 +232,8 @@ void WarpSidebarWidget::notifyViewLocationChanged(View* view, const ViewLocation
// Warp sidebar really should only update if it is visible, otherwise its a waste of cycles.
if (!this->isVisible())
return;
- auto function = location.getFunction();
- // TODO: Only update if the function exists?
// NOTE: The function called will exit early if it is the same function.
- m_currentFunctionWidget->SetCurrentFunction(function);
+ m_currentFunctionWidget->SetCurrentFunction(location.getFunction());
}
void WarpSidebarWidget::focus()
@@ -249,6 +245,45 @@ void WarpSidebarWidget::focus()
WarpSidebarWidgetType::WarpSidebarWidgetType() : SidebarWidgetType(QImage(":/icons/images/warp.png"), "WARP") {}
+void RegisterCommands()
+{
+ RegisterPluginAction(
+ "Fetch",
+ [](const UIActionContext& context) {
+ WarpFetchDialog dlg(context.binaryView, WarpFetcher::Global(), nullptr);
+ dlg.exec();
+ },
+ [](const UIActionContext& context) { return context.binaryView != nullptr; });
+ RegisterPluginAction("Process", [](const UIActionContext& context) {
+ auto* dlg = new ProcessorDialog(context.widget);
+ dlg->setAttribute(Qt::WA_DeleteOnClose);
+ if (context.binaryView)
+ dlg->onAddBinaryView(context.binaryView);
+ dlg->show();
+ });
+ RegisterPluginAction("View File", [](const UIActionContext& context) {
+ std::string path;
+ if (!GetOpenFileNameInput(path, "Open WARP File", "*.warp"))
+ return;
+ auto file = Warp::File::FromPath(path);
+ if (!file)
+ return;
+
+ auto* dlg = new QDialog(context.widget);
+ dlg->setWindowTitle(QString::fromStdString("WARP File: " + path));
+ dlg->setAttribute(Qt::WA_DeleteOnClose);
+
+ auto* layout = new QVBoxLayout(dlg);
+ layout->setContentsMargins(10, 10, 10, 10);
+ auto* fileWidget = new FileWidget(dlg);
+ fileWidget->setFile(file);
+ layout->addWidget(fileWidget);
+
+ dlg->resize(1000, 700);
+ dlg->show();
+ });
+}
+
extern "C"
{
BN_DECLARE_UI_ABI_VERSION
@@ -267,7 +302,7 @@ extern "C"
BINARYNINJAPLUGIN bool UIPluginInit()
#endif
{
- RegisterWarpFetchFunctionsCommand();
+ RegisterCommands();
Sidebar::addSidebarWidgetType(new WarpSidebarWidgetType());
return true;
}
diff --git a/plugins/warp/ui/plugin.h b/plugins/warp/ui/plugin.h
index 0a4c630c..4ad43864 100644
--- a/plugins/warp/ui/plugin.h
+++ b/plugins/warp/ui/plugin.h
@@ -48,7 +48,7 @@ class WarpSidebarWidgetType : public SidebarWidgetType
public:
WarpSidebarWidgetType();
- SidebarWidgetLocation defaultLocation() const override { return SidebarWidgetLocation::RightContent; }
+ SidebarWidgetLocation defaultLocation() const override { return RightContent; }
SidebarContextSensitivity contextSensitivity() const override { return PerViewTypeSidebarContext; }
WarpSidebarWidget* createWidget(ViewFrame* viewFrame, BinaryViewRef data) override
diff --git a/plugins/warp/ui/shared/chunk.cpp b/plugins/warp/ui/shared/chunk.cpp
new file mode 100644
index 00000000..911628eb
--- /dev/null
+++ b/plugins/warp/ui/shared/chunk.cpp
@@ -0,0 +1,159 @@
+#include "chunk.h"
+
+#include <QAction>
+#include <QClipboard>
+#include <QGuiApplication>
+#include <QHeaderView>
+#include <QMenu>
+#include <QVBoxLayout>
+
+#include "misc.h"
+
+ChunkWidget::ChunkWidget(QWidget* parent) : QWidget(parent)
+{
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(4);
+
+ // Search Box
+ m_searchBox = new QLineEdit(this);
+ m_searchBox->setPlaceholderText("Search chunk contents...");
+ m_searchBox->setClearButtonEnabled(true);
+ connect(m_searchBox, &QLineEdit::textChanged, this, &ChunkWidget::onSearchTextChanged);
+ layout->addWidget(m_searchBox);
+
+ m_countLabel = new QLabel(this);
+ m_countLabel->setContentsMargins(4, 0, 4, 0);
+ layout->addWidget(m_countLabel);
+
+ // Table Widget (Styled as a list)
+ m_table = new QTableWidget(this);
+ m_table->setColumnCount(3);
+ m_table->setHorizontalHeaderLabels({"Type", "Name", "ID"});
+ m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
+ m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
+ m_table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
+ m_table->setItemDelegateForColumn(1, new TokenDataDelegate(this));
+ m_table->setColumnHidden(2, true);
+
+ // Visual tweaks to make it look like a nice list
+ m_table->verticalHeader()->setVisible(false);
+ m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_table->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_table->setShowGrid(false);
+ m_table->setAlternatingRowColors(false);
+ m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_table->setStyleSheet("QTableWidget::item { padding: 10px; }");
+
+ layout->addWidget(m_table);
+}
+
+void ChunkWidget::setChunk(Warp::Ref<Warp::Chunk> chunk)
+{
+ m_chunk = chunk;
+ m_searchBox->clear();
+ populateTable();
+}
+
+void ChunkWidget::populateTable()
+{
+ m_table->setRowCount(0);
+ if (!m_chunk)
+ {
+ updateCountLabel();
+ return;
+ }
+
+ auto functions = m_chunk->GetFunctions();
+ auto types = m_chunk->GetTypes();
+
+ m_table->setRowCount(functions.size() + types.size());
+ int row = 0;
+
+ for (const auto& func : functions)
+ {
+ m_table->setItem(row, 0, new QTableWidgetItem("Function"));
+
+ auto* nameItem = new QTableWidgetItem();
+ std::string symbolName = func->GetSymbolName();
+ TokenData tokenData(symbolName);
+
+ if (auto warpType = func->GetType())
+ {
+ if (auto analysisType = warpType->GetAnalysisType())
+ tokenData = TokenData(*analysisType, symbolName);
+ }
+
+ nameItem->setText(QString::fromStdString(symbolName)); // Fallback text for search
+ nameItem->setData(Qt::UserRole, QVariant::fromValue(tokenData));
+ m_table->setItem(row, 1, nameItem);
+
+ auto* idItem = new QTableWidgetItem(QString::fromStdString(func->GetGUID().ToString()));
+ m_table->setItem(row, 2, idItem);
+
+ row++;
+ }
+
+ for (const auto& type : types)
+ {
+ m_table->setItem(row, 0, new QTableWidgetItem("Type"));
+
+ auto* nameItem = new QTableWidgetItem();
+ std::string typeName = type->GetName().value_or("");
+ TokenData tokenData(typeName);
+
+ if (auto analysisType = type->GetAnalysisType())
+ {
+ tokenData = TokenData(*analysisType, typeName);
+ }
+
+ nameItem->setText(QString::fromStdString(typeName)); // Fallback text for search
+ nameItem->setData(Qt::UserRole, QVariant::fromValue(tokenData));
+ m_table->setItem(row, 1, nameItem);
+
+ m_table->setItem(row, 2, new QTableWidgetItem(""));
+
+ row++;
+ }
+
+ updateCountLabel();
+}
+
+void ChunkWidget::onSearchTextChanged(const QString& text)
+{
+ for (int i = 0; i < m_table->rowCount(); ++i)
+ {
+ bool match = false;
+ for (int j = 0; j < m_table->columnCount(); ++j)
+ {
+ auto* item = m_table->item(i, j);
+ if (item && item->text().contains(text, Qt::CaseInsensitive))
+ {
+ match = true;
+ break;
+ }
+ }
+ m_table->setRowHidden(i, !match);
+ }
+ updateCountLabel();
+}
+
+void ChunkWidget::updateCountLabel()
+{
+ int totalCount = m_table->rowCount();
+ int visibleCount = 0;
+ for (int i = 0; i < totalCount; ++i)
+ {
+ if (!m_table->isRowHidden(i))
+ visibleCount++;
+ }
+
+ if (m_searchBox->text().isEmpty())
+ {
+ m_countLabel->setText(QString::number(totalCount) + " items");
+ }
+ else
+ {
+ m_countLabel->setText(QString::number(visibleCount) + " of " + QString::number(totalCount) + " items");
+ }
+} \ No newline at end of file
diff --git a/plugins/warp/ui/shared/chunk.h b/plugins/warp/ui/shared/chunk.h
new file mode 100644
index 00000000..a03a8eae
--- /dev/null
+++ b/plugins/warp/ui/shared/chunk.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <QWidget>
+#include <QTableWidget>
+#include <QLineEdit>
+#include <QLabel>
+#include "warp.h"
+
+class ChunkWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit ChunkWidget(QWidget* parent = nullptr);
+ void setChunk(Warp::Ref<Warp::Chunk> chunk);
+
+private slots:
+ void onSearchTextChanged(const QString& text);
+
+private:
+ void populateTable();
+ void updateCountLabel();
+
+ Warp::Ref<Warp::Chunk> m_chunk;
+ QLineEdit* m_searchBox;
+ QLabel* m_countLabel;
+ QTableWidget* m_table;
+}; \ No newline at end of file
diff --git a/plugins/warp/ui/shared/commitdialog.cpp b/plugins/warp/ui/shared/commitdialog.cpp
new file mode 100644
index 00000000..1ea6f4ba
--- /dev/null
+++ b/plugins/warp/ui/shared/commitdialog.cpp
@@ -0,0 +1,141 @@
+#include "commitdialog.h"
+#include "file.h"
+#include "misc.h"
+
+#include <QHBoxLayout>
+#include <QFormLayout>
+#include <QMessageBox>
+#include <QPushButton>
+#include <QTimer>
+
+CommitDialog::CommitDialog(Warp::Ref<Warp::File> file, QWidget* parent) : QDialog(parent), m_file(file)
+{
+ setWindowModality(Qt::NonModal);
+ setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
+ setWindowTitle("Commit to Source");
+ setMinimumSize(300, 200);
+
+ auto* mainLayout = new QVBoxLayout(this);
+
+ auto* commitFormLayout = new QFormLayout();
+
+ m_containerCombo = new QComboBox(this);
+ connect(
+ m_containerCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CommitDialog::onContainerChanged);
+
+ m_sourcesView = new WarpSourcesView(this);
+ m_proxyModel = new QSortFilterProxyModel(this);
+ m_proxyModel->setSourceModel(m_sourcesView->sourceModel());
+ m_proxyModel->setFilterKeyColumn(WarpSourcesModel::PathCol);
+ m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ m_sourcesView->setModel(m_proxyModel);
+
+ auto* sourceLayout = new QVBoxLayout();
+
+ auto* filterLayout = new QHBoxLayout();
+ m_sourceFilter = new QLineEdit(this);
+ m_sourceFilter->setPlaceholderText("Filter sources...");
+ connect(m_sourceFilter, &QLineEdit::textChanged, m_proxyModel, &QSortFilterProxyModel::setFilterFixedString);
+ filterLayout->addWidget(m_sourceFilter);
+
+ m_addSourceButton = new QPushButton("+", this);
+ m_addSourceButton->setFixedWidth(30);
+ m_addSourceButton->setToolTip("Add source");
+ connect(m_addSourceButton, &QPushButton::clicked, this, &CommitDialog::onCreateNewSource);
+ filterLayout->addWidget(m_addSourceButton);
+
+ sourceLayout->addLayout(filterLayout);
+ sourceLayout->addWidget(m_sourcesView);
+
+ commitFormLayout->addRow("Container:", m_containerCombo);
+ commitFormLayout->addRow("Source:", sourceLayout);
+
+ auto commitBtnLabel = QString("Commit %1 chunks").arg(m_file->GetChunks().size());
+ m_commitButton = new QPushButton(commitBtnLabel, this);
+ connect(m_commitButton, &QPushButton::clicked, this, &CommitDialog::onCommit);
+
+ mainLayout->addLayout(commitFormLayout);
+ mainLayout->addWidget(m_commitButton, 0, Qt::AlignRight);
+
+ populateContainers();
+
+ if (!m_containers.empty())
+ m_sourcesView->setContainer(m_containers[m_containerCombo->currentIndex()]);
+}
+
+void CommitDialog::populateContainers()
+{
+ m_containers = Warp::Container::All();
+ m_containerCombo->clear();
+ for (const auto& container : m_containers)
+ m_containerCombo->addItem(QString::fromStdString(container->GetName()));
+}
+
+void CommitDialog::onContainerChanged(int index)
+{
+ if (index >= 0 && index < m_containers.size())
+ m_sourcesView->setContainer(m_containers[index]);
+}
+
+void CommitDialog::onCreateNewSource()
+{
+ if (m_sourcesView->addSource())
+ {
+ // Select the newly added source
+ int rowCount = m_sourcesView->sourceModel()->rowCount();
+ if (rowCount > 0)
+ {
+ QModelIndex sourceIdx = m_sourcesView->sourceModel()->index(rowCount - 1, WarpSourcesModel::PathCol);
+ m_sourcesView->setCurrentIndex(m_proxyModel->mapFromSource(sourceIdx));
+ }
+ }
+}
+
+void CommitDialog::onCommit()
+{
+ if (!m_file)
+ return;
+ int containerIdx = m_containerCombo->currentIndex();
+ QModelIndex proxyIdx = m_sourcesView->currentIndex();
+
+ if (m_file->GetChunks().empty())
+ {
+ QMessageBox::critical(this, "Error", "No chunks to commit.");
+ return;
+ }
+
+ if (containerIdx < 0 || !proxyIdx.isValid())
+ {
+ QMessageBox::critical(this, "Error", "No source selected, please select a source.");
+ return;
+ }
+
+ auto container = m_containers[containerIdx];
+ QModelIndex sourceIdx = m_proxyModel->mapToSource(proxyIdx);
+ auto optSource = m_sourcesView->sourceFromRow(sourceIdx.row());
+ if (!optSource.has_value())
+ {
+ QMessageBox::critical(this, "Error", "Failed to retrieve the selected source.");
+ return;
+ }
+ auto source = optSource.value();
+
+ m_commitButton->setEnabled(false);
+ m_commitButton->setText("Committing...");
+
+ auto* worker = new WarpCommitWorker(container, source, m_file);
+ connect(worker, &WarpCommitWorker::finishedCommitting, this, &CommitDialog::onCommitFinished);
+ connect(worker, &WarpCommitWorker::finished, worker, &QObject::deleteLater);
+ worker->start();
+}
+
+void CommitDialog::onCommitFinished(bool success)
+{
+ m_commitButton->setEnabled(true);
+ m_commitButton->setText("Commit");
+
+ if (success)
+ QMessageBox::information(this, "Success", "Successfully committed to the source.");
+ else
+ QMessageBox::critical(this, "Error", "Failed to commit to the source.");
+}
diff --git a/plugins/warp/ui/shared/commitdialog.h b/plugins/warp/ui/shared/commitdialog.h
new file mode 100644
index 00000000..ccbe8da1
--- /dev/null
+++ b/plugins/warp/ui/shared/commitdialog.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include <QDialog>
+#include <QStackedWidget>
+#include <QComboBox>
+#include <QProgressBar>
+#include <QThread>
+#include <QVBoxLayout>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QSortFilterProxyModel>
+
+#include "binaryninjaapi.h"
+#include "source.h"
+#include "warp.h"
+#include "source.h"
+
+// Worker to commit a file to a container
+class WarpCommitWorker : public QThread
+{
+ Q_OBJECT
+
+ Warp::Ref<Warp::Container> m_container;
+ Warp::Source m_source;
+ Warp::Ref<Warp::File> m_file;
+
+public:
+ WarpCommitWorker(Warp::Ref<Warp::Container> container, Warp::Source source, Warp::Ref<Warp::File> file,
+ QObject* parent = nullptr) : QThread(parent), m_container(container), m_source(source), m_file(file)
+ {}
+
+ void run() override
+ {
+ for (const auto& chunk : m_file->GetChunks())
+ {
+ if (auto target = chunk->GetTarget())
+ m_container->AddFunctions(*target, m_source, chunk->GetFunctions());
+ m_container->AddTypes(m_source, chunk->GetTypes());
+ }
+
+ const bool result = m_container->CommitSource(m_source);
+ emit finishedCommitting(result);
+ }
+
+signals:
+ void finishedCommitting(bool success);
+};
+
+class CommitDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit CommitDialog(Warp::Ref<Warp::File> file, QWidget* parent = nullptr);
+
+private slots:
+ void onContainerChanged(int index);
+ void onCreateNewSource();
+ void onCommit();
+ void onCommitFinished(bool success);
+
+private:
+ void populateContainers();
+
+ Warp::Ref<Warp::File> m_file;
+ std::vector<Warp::Ref<Warp::Container>> m_containers;
+
+ QComboBox* m_containerCombo;
+ QLineEdit* m_sourceFilter;
+ QPushButton* m_addSourceButton;
+ QSortFilterProxyModel* m_proxyModel;
+ WarpSourcesView* m_sourcesView;
+ QPushButton* m_commitButton;
+}; \ No newline at end of file
diff --git a/plugins/warp/ui/shared/fetchdialog.cpp b/plugins/warp/ui/shared/fetchdialog.cpp
index 44d7c237..41dea3e0 100644
--- a/plugins/warp/ui/shared/fetchdialog.cpp
+++ b/plugins/warp/ui/shared/fetchdialog.cpp
@@ -25,7 +25,7 @@ static void AddListItem(QListWidget* list, const QString& value)
WarpFetchDialog::WarpFetchDialog(BinaryViewRef bv, std::shared_ptr<WarpFetcher> fetcher, QWidget* parent) :
QDialog(parent), m_fetchProcessor(std::move(fetcher)), m_bv(std::move(bv))
{
- setWindowTitle("Fetch WARP Functions");
+ setWindowTitle("WARP Fetcher");
auto form = new QFormLayout();
m_containerCombo = new QComboBox(this);
@@ -160,8 +160,8 @@ void WarpFetchDialog::onReject()
reject();
}
-void WarpFetchDialog::runBatchedFetch(const std::optional<size_t>& containerIndex,
- const std::vector<Warp::SourceTag>& allowedTags, bool rerunMatcher)
+void WarpFetchDialog::runBatchedFetch(
+ const std::optional<size_t>& containerIndex, const std::vector<Warp::SourceTag>& allowedTags, bool rerunMatcher)
{
if (!m_bv)
return;
@@ -178,48 +178,27 @@ void WarpFetchDialog::runBatchedFetch(const std::optional<size_t>& containerInde
auto bv = m_bv;
// TODO: Too many captures in this thing lol.
- WorkerInteractiveEnqueue(
- [fetcher, bv, funcs = std::move(funcs), rerunMatcher, task, allowedTags]() mutable {
- const auto batchSize = GetBatchSizeFromView(bv);
- size_t processed = 0;
- while (processed < funcs.size())
- {
- if (task->IsCancelled())
- break;
- const size_t remaining = funcs.size() - processed;
- const size_t thisBatchCount = std::min(batchSize, remaining);
- for (size_t i = 0; i < thisBatchCount; ++i)
- fetcher->AddPendingFunction(funcs[processed + i]);
- fetcher->FetchPendingFunctions(allowedTags);
- processed += thisBatchCount;
- task->SetProgressText("Fetching WARP functions (" + std::to_string(processed) + " / " + std::to_string(funcs.size()) + ")");
- }
+ WorkerInteractiveEnqueue([fetcher, bv, funcs = std::move(funcs), rerunMatcher, task, allowedTags]() mutable {
+ const auto batchSize = GetBatchSizeFromView(bv);
+ size_t processed = 0;
+ while (processed < funcs.size())
+ {
+ if (task->IsCancelled())
+ break;
+ const size_t remaining = funcs.size() - processed;
+ const size_t thisBatchCount = std::min(batchSize, remaining);
+ for (size_t i = 0; i < thisBatchCount; ++i)
+ fetcher->AddPendingFunction(funcs[processed + i]);
+ fetcher->FetchPendingFunctions(allowedTags);
+ processed += thisBatchCount;
+ task->SetProgressText(
+ "Fetching WARP functions (" + std::to_string(processed) + " / " + std::to_string(funcs.size()) + ")");
+ }
- task->Finish();
- Logger("WARP Fetcher").LogInfo("Finished fetching WARP functions in %d seconds...", task->GetRuntimeSeconds());
+ task->Finish();
+ Logger("WARP Fetcher").LogInfo("Finished fetching WARP functions in %d seconds...", task->GetRuntimeSeconds());
- if (rerunMatcher && bv)
- Warp::RunMatcher(*bv);
- });
-}
-
-void RegisterWarpFetchFunctionsCommand()
-{
- // Register a UI action and bind it globally. Add it to the Tools menu.
- const QString actionName = "WARP\\Fetch";
- if (!UIAction::isActionRegistered(actionName))
- UIAction::registerAction(actionName);
-
- UIActionHandler::globalActions()->bindAction(actionName,
- UIAction(
- [](const UIActionContext& context) {
- if (const BinaryViewRef bv = context.binaryView; bv)
- {
- WarpFetchDialog dlg(bv, WarpFetcher::Global(), nullptr);
- dlg.exec();
- }
- },
- [](const UIActionContext& context) { return context.binaryView != nullptr; }));
-
- Menu::mainMenu("Plugins")->addAction(actionName, "Plugins");
+ if (rerunMatcher && bv)
+ Warp::RunMatcher(*bv);
+ });
}
diff --git a/plugins/warp/ui/shared/fetchdialog.h b/plugins/warp/ui/shared/fetchdialog.h
index 72b8c57e..68fca6c1 100644
--- a/plugins/warp/ui/shared/fetchdialog.h
+++ b/plugins/warp/ui/shared/fetchdialog.h
@@ -52,5 +52,3 @@ private:
void runBatchedFetch(const std::optional<size_t>& containerIndex, const std::vector<Warp::SourceTag>& allowedTags,
bool rerunMatcher);
};
-
-void RegisterWarpFetchFunctionsCommand();
diff --git a/plugins/warp/ui/shared/fetcher.cpp b/plugins/warp/ui/shared/fetcher.cpp
index 2eab910a..f5aa626a 100644
--- a/plugins/warp/ui/shared/fetcher.cpp
+++ b/plugins/warp/ui/shared/fetcher.cpp
@@ -73,7 +73,8 @@ void WarpFetcher::FetchPendingFunctions(const std::vector<Warp::SourceTag>& allo
auto platform = func->GetPlatform();
platformMappedGuidSet[platform].insert(warpFunc->GetGUID());
- // We want to keep track of the guids so we can constrain the server response to only return functions with any of them.
+ // We want to keep track of the guids so we can constrain the server response to only return functions with any
+ // of them.
const auto constraints = warpFunc->GetConstraints();
std::vector<Warp::ConstraintGUID> constraintGuids;
constraintGuids.reserve(constraints.size());
diff --git a/plugins/warp/ui/shared/fetcher.h b/plugins/warp/ui/shared/fetcher.h
index 3464ecd4..ad38ed60 100644
--- a/plugins/warp/ui/shared/fetcher.h
+++ b/plugins/warp/ui/shared/fetcher.h
@@ -24,6 +24,7 @@ class WarpFetcher
std::mutex m_requestMutex;
std::vector<FunctionRef> m_pendingRequests;
std::unordered_set<Warp::FunctionGUID> m_processedGuids;
+
public:
using CallbackId = uint64_t;
using CompletionCallback = std::function<WarpFetchCompletionStatus()>;
diff --git a/plugins/warp/ui/shared/file.cpp b/plugins/warp/ui/shared/file.cpp
new file mode 100644
index 00000000..70a169fe
--- /dev/null
+++ b/plugins/warp/ui/shared/file.cpp
@@ -0,0 +1,71 @@
+#include "file.h"
+
+#include <QVBoxLayout>
+
+FileWidget::FileWidget(QWidget* parent) : QWidget(parent)
+{
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ auto* splitter = new QSplitter(Qt::Horizontal, this);
+
+ // Left side: List of chunks
+ m_list = new QListWidget(this);
+ m_list->setSelectionMode(QAbstractItemView::SingleSelection);
+ m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_list->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+ m_list->setUniformItemSizes(true);
+ connect(m_list, &QListWidget::itemSelectionChanged, this, &FileWidget::onListSelectionChanged);
+ splitter->addWidget(m_list);
+
+ // Right side: Chunk Widget
+ m_chunkWidget = new ChunkWidget(this);
+ splitter->addWidget(m_chunkWidget);
+
+ splitter->setSizes({100, 900});
+ layout->addWidget(splitter);
+}
+
+void FileWidget::setFile(Warp::Ref<Warp::File> file)
+{
+ m_file = file;
+ m_list->clear();
+ m_chunkWidget->setChunk(nullptr);
+ m_currentChunks.clear();
+
+ if (!m_file)
+ return;
+
+ m_currentChunks = m_file->GetChunks();
+ for (size_t i = 0; i < m_currentChunks.size(); ++i)
+ {
+ auto* listItem = new QListWidgetItem(m_list);
+ listItem->setText(QString("Chunk #%1").arg(i + 1));
+ // Store the chunk index in the item's data for easy retrieval
+ listItem->setData(Qt::UserRole, QVariant::fromValue(static_cast<qulonglong>(i)));
+ }
+
+ if (m_list->count() > 0)
+ m_list->setCurrentRow(0);
+}
+
+void FileWidget::onListSelectionChanged()
+{
+ auto selectedItems = m_list->selectedItems();
+ if (selectedItems.isEmpty())
+ {
+ m_chunkWidget->setChunk(nullptr);
+ return;
+ }
+
+ auto* item = selectedItems.first();
+ QVariant data = item->data(Qt::UserRole);
+ if (data.isValid())
+ {
+ size_t index = data.value<qulonglong>();
+ if (index < m_currentChunks.size())
+ {
+ m_chunkWidget->setChunk(m_currentChunks[index]);
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/warp/ui/shared/file.h b/plugins/warp/ui/shared/file.h
new file mode 100644
index 00000000..057fd2cd
--- /dev/null
+++ b/plugins/warp/ui/shared/file.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <QWidget>
+#include <QListWidget>
+#include <QSplitter>
+#include "warp.h"
+#include "chunk.h"
+
+class FileWidget : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit FileWidget(QWidget* parent = nullptr);
+ void setFile(Warp::Ref<Warp::File> file);
+
+private slots:
+ void onListSelectionChanged();
+
+private:
+ Warp::Ref<Warp::File> m_file;
+ QListWidget* m_list;
+ ChunkWidget* m_chunkWidget;
+ std::vector<Warp::Ref<Warp::Chunk>> m_currentChunks;
+}; \ No newline at end of file
diff --git a/plugins/warp/ui/shared/function.cpp b/plugins/warp/ui/shared/function.cpp
index 3b2451ab..fd403d77 100644
--- a/plugins/warp/ui/shared/function.cpp
+++ b/plugins/warp/ui/shared/function.cpp
@@ -21,8 +21,8 @@ WarpFunctionItem::WarpFunctionItem(
// Serialize the tokens to make it accessible via QModelIndex.
// We will take these tokens and then user them in our custom item delegate.
TokenData tokenData = TokenData(symbolName);
- if (BinaryNinja::Ref<BinaryNinja::Type> type = m_function->GetType(*analysisFunction))
- tokenData = TokenData(*type, symbolName);
+ if (Warp::Ref<Warp::Type> warpType = m_function->GetType())
+ tokenData = TokenData(*warpType->GetAnalysisType(analysisFunction->GetArchitecture()), symbolName);
setData(QVariant::fromValue(tokenData), Qt::UserRole);
}
@@ -256,8 +256,7 @@ void WarpFunctionTableWidget::RegisterContextMenuAction(
m_contextMenuActions[name] = callback;
}
-void WarpFunctionTableWidget::RegisterContextMenuAction(
- const QString& name,
+void WarpFunctionTableWidget::RegisterContextMenuAction(const QString& name,
const std::function<void(WarpFunctionItem*, std::optional<uint64_t>)>& callback,
const std::function<bool(WarpFunctionItem*, std::optional<uint64_t>)>& isValid)
{
diff --git a/plugins/warp/ui/shared/function.h b/plugins/warp/ui/shared/function.h
index c6e5ba63..4cf226c7 100644
--- a/plugins/warp/ui/shared/function.h
+++ b/plugins/warp/ui/shared/function.h
@@ -112,10 +112,9 @@ public:
void RegisterContextMenuAction(
const QString& name, const std::function<void(WarpFunctionItem*, std::optional<uint64_t>)>& callback);
- void RegisterContextMenuAction(
- const QString &name,
- const std::function<void(WarpFunctionItem *, std::optional<uint64_t>)> &callback,
- const std::function<bool(WarpFunctionItem *, std::optional<uint64_t>)> &isValid);
+ void RegisterContextMenuAction(const QString& name,
+ const std::function<void(WarpFunctionItem*, std::optional<uint64_t>)>& callback,
+ const std::function<bool(WarpFunctionItem*, std::optional<uint64_t>)>& isValid);
void SetFunctions(QVector<WarpFunctionItem*> functions);
diff --git a/plugins/warp/ui/shared/misc.cpp b/plugins/warp/ui/shared/misc.cpp
index 6de314b1..c88cee10 100644
--- a/plugins/warp/ui/shared/misc.cpp
+++ b/plugins/warp/ui/shared/misc.cpp
@@ -72,6 +72,75 @@ void AddressColorDelegate::paint(QPainter* painter, const QStyleOptionViewItem&
QStyledItemDelegate::paint(painter, opt, index);
}
+void SourcePathDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
+{
+ QStyleOptionViewItem opt = option;
+ initStyleOption(&opt, index);
+
+ // Draw background and selection highlights
+ opt.widget->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
+
+ QString text = index.data(Qt::DisplayRole).toString();
+ int sepIdx = qMax(text.lastIndexOf('/'), text.lastIndexOf('\\'));
+ QString dirPart = sepIdx != -1 ? text.left(sepIdx + 1) : "";
+ QString filePart = sepIdx != -1 ? text.mid(sepIdx + 1) : text;
+
+ QFont regularFont = opt.font;
+ QFont boldFont = regularFont;
+ boldFont.setBold(true);
+
+ QFontMetrics fmReg(regularFont);
+ QFontMetrics fmBold(boldFont);
+
+ // Basic padding inside the list item
+ QRect textRect = opt.rect.adjusted(3, 0, -3, 0);
+
+ int fileWidth = fmBold.horizontalAdvance(filePart);
+ int dirWidth = fmReg.horizontalAdvance(dirPart);
+
+ QString textToDrawDir;
+ QString textToDrawFile = filePart;
+
+ if (dirWidth + fileWidth > textRect.width())
+ {
+ if (fileWidth > textRect.width())
+ {
+ // The file name itself is too long, elide it
+ textToDrawDir = "";
+ textToDrawFile = fmBold.elidedText(filePart, Qt::ElideLeft, textRect.width());
+ }
+ else
+ {
+ // Elide the directory part so the bold file name fits
+ textToDrawDir = fmReg.elidedText(dirPart, Qt::ElideLeft, textRect.width() - fileWidth);
+ }
+ }
+ else
+ {
+ textToDrawDir = dirPart;
+ }
+
+ painter->save();
+
+ // Set the proper text color based on selection state
+ if (opt.state & QStyle::State_Selected)
+ painter->setPen(opt.palette.highlightedText().color());
+ else
+ painter->setPen(opt.palette.text().color());
+
+ // Draw the directory part
+ painter->setFont(regularFont);
+ painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, textToDrawDir);
+
+ // Draw the file part
+ painter->setFont(boldFont);
+ int dirAdvance = fmReg.horizontalAdvance(textToDrawDir);
+ QRect fileRect = textRect.adjusted(dirAdvance, 0, 0, 0);
+ painter->drawText(fileRect, Qt::AlignLeft | Qt::AlignVCenter, textToDrawFile);
+
+ painter->restore();
+}
+
bool GenericTextFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
auto filterString = filterRegularExpression().pattern();
@@ -142,13 +211,14 @@ ParsedQuery::ParsedQuery(const QString& rawQuery)
query = query.simplified();
}
-WarpRemoveMatchDialog::WarpRemoveMatchDialog(QWidget *parent, FunctionRef func) : QDialog(parent), m_func(func)
+WarpRemoveMatchDialog::WarpRemoveMatchDialog(QWidget* parent, FunctionRef func) : QDialog(parent), m_func(func)
{
setWindowTitle("Remove Matching Function");
setModal(true);
auto* vbox = new QVBoxLayout(this);
- auto* text = new QLabel("Remove the match for this function? You can also mark it as ignored to prevent future automatic matches.");
+ auto* text = new QLabel(
+ "Remove the match for this function? You can also mark it as ignored to prevent future automatic matches.");
text->setWordWrap(true);
vbox->addWidget(text);
diff --git a/plugins/warp/ui/shared/misc.h b/plugins/warp/ui/shared/misc.h
index 92e27fd8..e9d6fd43 100644
--- a/plugins/warp/ui/shared/misc.h
+++ b/plugins/warp/ui/shared/misc.h
@@ -67,6 +67,14 @@ public:
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
};
+class SourcePathDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit SourcePathDelegate(QObject* parent = nullptr) : QStyledItemDelegate(parent) {}
+ void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
+};
class GenericTextFilterModel : public QSortFilterProxyModel
{
@@ -109,6 +117,7 @@ struct ParsedQuery
class WarpRemoveMatchDialog : public QDialog
{
Q_OBJECT
+
public:
explicit WarpRemoveMatchDialog(QWidget* parent, FunctionRef func);
@@ -116,7 +125,7 @@ public:
private:
FunctionRef m_func;
- QCheckBox* m_ignoreCheck{nullptr};
+ QCheckBox* m_ignoreCheck {nullptr};
};
constexpr const char* ALLOWED_TAGS_SETTING = "warp.fetcher.allowedSourceTags";
@@ -144,4 +153,15 @@ inline size_t GetBatchSizeFromView(const BinaryViewRef& view)
if (!settings->Contains(BATCH_SIZE_SETTING))
return 10000;
return settings->Get<uint64_t>(BATCH_SIZE_SETTING, view);
+}
+
+inline void RegisterPluginAction(
+ std::string name, std::function<void(const UIActionContext&)> action,
+ std::function<bool(const UIActionContext&)> isValid = [](const UIActionContext&) { return true; })
+{
+ const QString actionName = QString("WARP\\%1").arg(QString::fromStdString(name));
+ if (!UIAction::isActionRegistered(actionName))
+ UIAction::registerAction(actionName);
+ UIActionHandler::globalActions()->bindAction(actionName, UIAction(action, isValid));
+ Menu::mainMenu("Plugins")->addAction(actionName, "Plugins");
} \ No newline at end of file
diff --git a/plugins/warp/ui/shared/processordialog.cpp b/plugins/warp/ui/shared/processordialog.cpp
new file mode 100644
index 00000000..9e602b79
--- /dev/null
+++ b/plugins/warp/ui/shared/processordialog.cpp
@@ -0,0 +1,451 @@
+#include "processordialog.h"
+#include "commitdialog.h"
+#include "misc.h"
+#include "selectprojectfilesdialog.h"
+
+#include <QHBoxLayout>
+#include <QFormLayout>
+#include <QFileDialog>
+#include <QFile>
+#include <QMessageBox>
+#include <QMenu>
+#include <QContextMenuEvent>
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QDir>
+
+using namespace BinaryNinja;
+
+ProcessorDialog::ProcessorDialog(QWidget* parent) : QDialog(parent)
+{
+ setWindowModality(Qt::NonModal);
+ setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
+ setWindowTitle("WARP Processor");
+ setMinimumSize(400, 300);
+
+ auto* mainLayout = new QVBoxLayout(this);
+ m_stack = new QStackedWidget(this);
+ mainLayout->addWidget(m_stack);
+
+ // Page 1: Configuration
+ auto* configPage = new QWidget(this);
+ auto* configLayout = new QVBoxLayout(configPage);
+
+ auto* entrySearchLayout = new QHBoxLayout();
+ entrySearchLayout->setContentsMargins(0, 0, 0, 0);
+ m_entrySearch = new QLineEdit(this);
+ m_entrySearch->setPlaceholderText("Search entries...");
+ connect(m_entrySearch, &QLineEdit::textChanged, this, &ProcessorDialog::onSearchItems);
+ entrySearchLayout->addWidget(m_entrySearch);
+
+ m_addButton = new QPushButton("+", this);
+ m_addButton->setFixedWidth(30);
+ m_addButton->setToolTip("Add entries");
+ connect(m_addButton, &QPushButton::clicked, this, &ProcessorDialog::onAddEntryMenu);
+ entrySearchLayout->addWidget(m_addButton);
+ configLayout->addLayout(entrySearchLayout);
+
+ m_entryList = new QListWidget(this);
+ m_entryList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ m_entryList->setContextMenuPolicy(Qt::CustomContextMenu);
+ m_entryList->setTextElideMode(Qt::ElideLeft);
+ m_entryList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_entryList->setStyleSheet("QListWidget::item { padding: 2px; }");
+ connect(m_entryList, &QListWidget::customContextMenuRequested, this, &ProcessorDialog::showContextMenu);
+ configLayout->addWidget(m_entryList);
+
+ auto* formLayout = new QFormLayout();
+ m_includedDataCombo = new QComboBox(this);
+ m_includedDataCombo->addItem("Symbols", WARPProcessorIncludedDataSymbols);
+ m_includedDataCombo->addItem("Signatures", WARPProcessorIncludedDataSignatures);
+ m_includedDataCombo->addItem("Types", WARPProcessorIncludedDataTypes);
+ m_includedDataCombo->addItem("All", WARPProcessorIncludedDataAll);
+ m_includedDataCombo->setCurrentIndex(3);
+
+ m_includedFunctionsCombo = new QComboBox(this);
+ m_includedFunctionsCombo->addItem("Selected", WARPProcessorIncludedFunctionsSelected);
+ m_includedFunctionsCombo->addItem("Annotated", WARPProcessorIncludedFunctionsAnnotated);
+ m_includedFunctionsCombo->addItem("All", WARPProcessorIncludedFunctionsAll);
+ m_includedFunctionsCombo->setCurrentIndex(1);
+
+ m_workerCountSpinBox = new QSpinBox(this);
+ m_workerCountSpinBox->setMinimum(2);
+ m_workerCountSpinBox->setValue(GetWorkerThreadCount());
+
+ formLayout->addRow("Included Data:", m_includedDataCombo);
+ formLayout->addRow("Included Functions:", m_includedFunctionsCombo);
+ formLayout->addRow("Worker Count:", m_workerCountSpinBox);
+ configLayout->addLayout(formLayout);
+
+ m_processButton = new QPushButton("Process", this);
+ m_processButton->setEnabled(false);
+ connect(m_processButton, &QPushButton::clicked, this, &ProcessorDialog::onStartProcessing);
+ configLayout->addWidget(m_processButton, 0, Qt::AlignRight);
+ m_stack->addWidget(configPage);
+
+ // Page 2: Processing
+ auto* processPage = new QWidget(this);
+ auto* processLayout = new QVBoxLayout(processPage);
+ m_processingLabel = new QLabel("Processing...", this);
+ m_processingLabel->setAlignment(Qt::AlignCenter);
+ m_progressBar = new QProgressBar(this);
+ m_progressBar->setRange(0, 0);
+
+ m_stateList = new QListWidget(this);
+ m_stateList->setSelectionMode(QAbstractItemView::NoSelection);
+ m_stateList->setFocusPolicy(Qt::NoFocus);
+ m_stateList->setTextElideMode(Qt::ElideLeft);
+ m_stateList->setWordWrap(false);
+ m_stateList->setStyleSheet(
+ "QListWidget { background: transparent; border: none; } QListWidget::item { padding: 1px; }");
+ m_stateList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ m_stateList->setMinimumHeight(100);
+
+ m_cancelButton = new QPushButton("Cancel", this);
+ connect(m_cancelButton, &QPushButton::clicked, this, &ProcessorDialog::onCancelProcessing);
+
+ m_updateTimer = new QTimer(this);
+ connect(m_updateTimer, &QTimer::timeout, this, &ProcessorDialog::onUpdateState);
+
+ processLayout->addStretch();
+ processLayout->addWidget(m_processingLabel);
+ processLayout->addWidget(m_progressBar);
+ processLayout->addWidget(m_stateList);
+ processLayout->addStretch();
+ processLayout->addWidget(m_cancelButton, 0, Qt::AlignRight);
+ m_stack->addWidget(processPage);
+
+ // Page 3: Results
+ auto* resultsPage = new QWidget(this);
+ auto* resultsLayout = new QVBoxLayout(resultsPage);
+ m_fileWidget = new FileWidget(this);
+ resultsLayout->addWidget(m_fileWidget);
+
+ auto* buttonLayout = new QHBoxLayout();
+ m_saveButton = new QPushButton("Save to File", this);
+ connect(m_saveButton, &QPushButton::clicked, this, &ProcessorDialog::onSaveToFile);
+ m_elapsedLabel = new QLabel(this);
+ m_commitButton = new QPushButton("Commit", this);
+ connect(m_commitButton, &QPushButton::clicked, this, &ProcessorDialog::onCommit);
+ buttonLayout->addWidget(m_elapsedLabel);
+ buttonLayout->addStretch();
+ buttonLayout->addWidget(m_saveButton);
+ buttonLayout->addWidget(m_commitButton);
+ resultsLayout->addLayout(buttonLayout);
+ m_stack->addWidget(resultsPage);
+
+ m_stack->setCurrentIndex(ConfigurationPage);
+}
+
+ProcessorDialog::~ProcessorDialog()
+{
+ m_updateTimer->stop();
+ if (m_processor)
+ m_processor->Cancel();
+}
+
+void ProcessorDialog::onStartProcessing()
+{
+ if (m_toProcess.empty())
+ return;
+
+ auto includedData = static_cast<BNWARPProcessorIncludedData>(m_includedDataCombo->currentData().toInt());
+ auto includedFunctions =
+ static_cast<BNWARPProcessorIncludedFunctions>(m_includedFunctionsCombo->currentData().toInt());
+ auto workerCount = static_cast<size_t>(m_workerCountSpinBox->value());
+
+ m_processor = std::make_shared<Warp::Processor>(includedData, includedFunctions, workerCount);
+
+ for (const auto& item : m_toProcess)
+ {
+ switch (item.type)
+ {
+ case ToProcessEntry::ViewMode:
+ m_processor->AddBinaryView(*item.view);
+ break;
+ case ToProcessEntry::PathMode:
+ m_processor->AddPath(item.path);
+ break;
+ case ToProcessEntry::ProjectMode:
+ m_processor->AddProject(*item.project);
+ break;
+ case ToProcessEntry::ProjectFileMode:
+ m_processor->AddProjectFile(*item.projectFile);
+ break;
+ }
+ }
+
+ m_stack->setCurrentIndex(ProcessingPage);
+ m_processTimer.start();
+ auto* worker = new WarpProcessorWorker(m_processor);
+ connect(worker, &WarpProcessorWorker::finishedProcessing, this, &ProcessorDialog::onProcessingFinished);
+ connect(worker, &WarpProcessorWorker::finished, worker, &QObject::deleteLater);
+ worker->start();
+
+ m_cancelButton->setEnabled(true);
+ m_updateTimer->start(100);
+}
+
+void ProcessorDialog::onAddEntryMenu()
+{
+ QMenu menu(nullptr);
+ addAddActionsToMenu(&menu);
+ menu.exec(m_addButton->mapToGlobal(QPoint(0, m_addButton->height())));
+}
+
+void ProcessorDialog::addAddActionsToMenu(QMenu* menu)
+{
+ menu->addAction("Add Files...", this, &ProcessorDialog::onAddPath);
+ menu->addAction("Add Directory...", this, &ProcessorDialog::onAddDirectory);
+ menu->addAction("Add Project Files", this, &ProcessorDialog::onAddProjectFiles);
+}
+
+void ProcessorDialog::showContextMenu(const QPoint& pos)
+{
+ QMenu menu(nullptr);
+ addAddActionsToMenu(&menu);
+
+ QListWidgetItem* item = m_entryList->itemAt(pos);
+ if (item)
+ {
+ menu.addSeparator();
+ menu.addAction("Remove", this, &ProcessorDialog::onRemoveItem);
+ }
+
+ menu.exec(m_entryList->mapToGlobal(pos));
+}
+
+void ProcessorDialog::onAddBinaryView(Ref<BinaryView> view)
+{
+ ToProcessEntry item;
+ item.type = ToProcessEntry::ViewMode;
+ item.view = view;
+ item.displayName = QString("View: %1").arg(QString::fromStdString(view->GetFile()->GetFilename()));
+
+ m_toProcess.push_back(item);
+ m_entryList->addItem(item.displayName);
+ onSearchItems();
+ m_processButton->setEnabled(true);
+}
+
+void ProcessorDialog::onAddPath()
+{
+ QStringList paths = QFileDialog::getOpenFileNames(this, "Select Files", "", "All Files (*)");
+ if (paths.isEmpty())
+ return;
+
+ for (const auto& path : paths)
+ addPathRecursive(path);
+
+ onSearchItems();
+ m_processButton->setEnabled(true);
+}
+
+void ProcessorDialog::onAddDirectory()
+{
+ QString path = QFileDialog::getExistingDirectory(this, "Select Directory", "");
+ if (path.isEmpty())
+ return;
+
+ addPathRecursive(path);
+
+ onSearchItems();
+ m_processButton->setEnabled(true);
+}
+
+void ProcessorDialog::addPathRecursive(const QString& path)
+{
+ QFileInfo info(path);
+ if (info.isDir())
+ {
+ QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories);
+ while (it.hasNext())
+ {
+ addSinglePath(it.next());
+ }
+ }
+ else
+ {
+ addSinglePath(path);
+ }
+}
+
+void ProcessorDialog::addSinglePath(const QString& path)
+{
+ ToProcessEntry item;
+ item.type = ToProcessEntry::PathMode;
+ item.path = path.toStdString();
+ item.displayName = QString("Path: %1").arg(path);
+
+ m_toProcess.push_back(item);
+ m_entryList->addItem(item.displayName);
+}
+
+void ProcessorDialog::onAddProjectFiles()
+{
+ auto projects = Project::GetOpenProjects();
+ if (projects.empty())
+ {
+ QMessageBox::information(this, "Add Project Files", "No projects are currently open.");
+ return;
+ }
+
+ SelectProjectFilesDialog dlg(this);
+ if (dlg.exec() == Accepted)
+ {
+ auto selectedFiles = dlg.getSelectedFiles();
+ if (selectedFiles.empty())
+ {
+ // If no files selected, add the entire project
+ auto project = dlg.getSelectedProject();
+ ToProcessEntry item;
+ item.type = ToProcessEntry::ProjectMode;
+ item.project = project;
+ item.displayName = QString("Project: %1").arg(QString::fromStdString(project->GetName()));
+ m_toProcess.push_back(item);
+ m_entryList->addItem(item.displayName);
+ }
+ else
+ {
+ for (auto& file : selectedFiles)
+ {
+ ToProcessEntry item;
+ item.type = ToProcessEntry::ProjectFileMode;
+ item.projectFile = file;
+ item.displayName = QString("File: %1").arg(QString::fromStdString(file->GetName()));
+ m_toProcess.push_back(item);
+ m_entryList->addItem(item.displayName);
+ }
+ }
+ onSearchItems();
+ m_processButton->setEnabled(true);
+ }
+}
+
+void ProcessorDialog::onRemoveItem()
+{
+ auto selectedItems = m_entryList->selectedItems();
+ if (selectedItems.isEmpty())
+ {
+ int row = m_entryList->currentRow();
+ if (row >= 0 && row < (int)m_toProcess.size())
+ {
+ m_toProcess.erase(m_toProcess.begin() + row);
+ delete m_entryList->takeItem(row);
+ }
+ }
+ else
+ {
+ for (auto* item : selectedItems)
+ {
+ int row = m_entryList->row(item);
+ if (row >= 0 && row < (int)m_toProcess.size())
+ {
+ m_toProcess.erase(m_toProcess.begin() + row);
+ delete m_entryList->takeItem(row);
+ }
+ }
+ }
+ m_processButton->setEnabled(!m_toProcess.empty());
+}
+
+void ProcessorDialog::onSearchItems()
+{
+ QString filter = m_entrySearch->text().toLower();
+ for (int i = 0; i < m_entryList->count(); ++i)
+ {
+ auto* item = m_entryList->item(i);
+ item->setHidden(!item->text().toLower().contains(filter));
+ }
+}
+
+void ProcessorDialog::onProcessingFinished(Warp::Ref<Warp::File> file)
+{
+ m_updateTimer->stop();
+
+ if (!file)
+ {
+ QMessageBox::critical(this, "Error", "Failed to process the selected input.");
+ m_stack->setCurrentIndex(ConfigurationPage);
+ return;
+ }
+
+ auto elapsed = m_processTimer.elapsed();
+ if (elapsed < 1000)
+ m_elapsedLabel->setText(QString("Processing took: %1ms").arg(elapsed));
+ else
+ m_elapsedLabel->setText(QString("Processing took: %1s").arg(elapsed / 1000.0, 0, 'f', 2));
+
+ m_file = file;
+ m_fileWidget->setFile(m_file);
+ m_stack->setCurrentIndex(ResultsPage);
+}
+
+void ProcessorDialog::onCancelProcessing()
+{
+ if (m_processor)
+ m_processor->Cancel();
+ m_cancelButton->setEnabled(false);
+ m_processingLabel->setText("Cancelling...");
+}
+
+void ProcessorDialog::onUpdateState()
+{
+ if (!m_processor)
+ return;
+
+ auto state = m_processor->GetState();
+ size_t total = state.processedFilesCount + state.unprocessedFilesCount;
+ if (total > 0)
+ {
+ m_progressBar->setMaximum(total);
+ m_progressBar->setValue(state.processedFilesCount);
+ }
+
+ m_stateList->clear();
+
+ auto addToList = [&](const std::vector<std::string>& files, const QString& prefix) {
+ for (auto it = files.rbegin(); it != files.rend(); ++it)
+ {
+ auto* item = new QListWidgetItem(prefix + QString::fromStdString(*it));
+ item->setTextAlignment(Qt::AlignCenter);
+ m_stateList->addItem(item);
+ }
+ };
+
+ addToList(state.processingFiles, "Processing: ");
+ addToList(state.analyzingFiles, "Analyzing: ");
+}
+
+void ProcessorDialog::onSaveToFile()
+{
+ if (!m_file)
+ return;
+
+ QString fileName = QFileDialog::getSaveFileName(this, "Save WARP File", "", "WARP Files (*.warp)");
+ if (!fileName.isEmpty())
+ {
+ DataBuffer buffer = m_file->ToDataBuffer();
+ QFile file(fileName);
+ if (file.open(QIODevice::WriteOnly))
+ {
+ file.write(static_cast<const char*>(buffer.GetData()), buffer.GetLength());
+ file.close();
+ QMessageBox::information(this, "Success", "File saved successfully.");
+ }
+ else
+ {
+ QMessageBox::critical(this, "Error", "Failed to open file for writing.");
+ }
+ }
+}
+
+void ProcessorDialog::onCommit()
+{
+ if (!m_file)
+ return;
+
+ auto* dialog = new CommitDialog(m_file, this);
+ dialog->setAttribute(Qt::WA_DeleteOnClose);
+ dialog->show();
+}
diff --git a/plugins/warp/ui/shared/processordialog.h b/plugins/warp/ui/shared/processordialog.h
new file mode 100644
index 00000000..3f69601d
--- /dev/null
+++ b/plugins/warp/ui/shared/processordialog.h
@@ -0,0 +1,127 @@
+#pragma once
+
+#include <QDialog>
+#include <QStackedWidget>
+#include <QComboBox>
+#include <QProgressBar>
+#include <QThread>
+#include <QTimer>
+#include <QMenu>
+#include <QSpinBox>
+#include <QElapsedTimer>
+#include <utility>
+
+#include "binaryninjaapi.h"
+#include "warp.h"
+#include "file.h"
+
+// TODO: Both of these are bothersome but I don't really want to do a ID lookup.
+Q_DECLARE_METATYPE(BinaryNinja::Ref<BinaryNinja::Project>)
+Q_DECLARE_METATYPE(BinaryNinja::Ref<BinaryNinja::ProjectFile>)
+
+// Worker to run the processor
+class WarpProcessorWorker : public QThread
+{
+ Q_OBJECT
+
+ std::shared_ptr<Warp::Processor> m_processor;
+
+public:
+ WarpProcessorWorker(std::shared_ptr<Warp::Processor> processor, QObject* parent = nullptr) :
+ QThread(parent), m_processor(std::move(processor))
+ {}
+
+ void run() override
+ {
+ Warp::Ref<Warp::File> file = m_processor->Start();
+ emit finishedProcessing(file);
+ }
+
+signals:
+ void finishedProcessing(Warp::Ref<Warp::File> file);
+};
+
+class ProcessorDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ProcessorDialog(QWidget* parent = nullptr);
+ ~ProcessorDialog() override;
+
+ void onAddBinaryView(BinaryNinja::Ref<BinaryNinja::BinaryView> view);
+ void onAddProjectFiles();
+
+private slots:
+ void onStartProcessing();
+ void onProcessingFinished(Warp::Ref<Warp::File> file);
+ void onCancelProcessing();
+ void onUpdateState();
+ void onSaveToFile();
+ void onCommit();
+ void onAddPath();
+ void onAddDirectory();
+ void onRemoveItem();
+ void onSearchItems();
+ void onAddEntryMenu();
+ void showContextMenu(const QPoint& pos);
+
+private:
+ void addAddActionsToMenu(QMenu* menu);
+ void addPathRecursive(const QString& path);
+ void addSinglePath(const QString& path);
+ struct ToProcessEntry
+ {
+ enum Type
+ {
+ ViewMode,
+ PathMode,
+ ProjectMode,
+ ProjectFileMode
+ } type;
+ BinaryNinja::Ref<BinaryNinja::BinaryView> view;
+ std::string path;
+ BinaryNinja::Ref<BinaryNinja::Project> project;
+ BinaryNinja::Ref<BinaryNinja::ProjectFile> projectFile;
+ QString displayName;
+ };
+
+ Warp::Ref<Warp::File> m_file;
+ std::shared_ptr<Warp::Processor> m_processor;
+ std::vector<ToProcessEntry> m_toProcess;
+
+ enum Page
+ {
+ ConfigurationPage = 0,
+ ProcessingPage = 1,
+ ResultsPage = 2
+ };
+
+ QStackedWidget* m_stack;
+
+ // Page 1: Configuration
+ QLineEdit* m_entrySearch;
+ QPushButton* m_addButton;
+ QListWidget* m_entryList;
+
+ // Global Config
+ QComboBox* m_includedDataCombo;
+ QComboBox* m_includedFunctionsCombo;
+ QSpinBox* m_workerCountSpinBox;
+ QPushButton* m_processButton;
+
+ // Page 2: Processing
+ QLabel* m_processingLabel;
+ QProgressBar* m_progressBar;
+ QListWidget* m_stateList;
+ QPushButton* m_cancelButton;
+ QTimer* m_updateTimer;
+
+ // Page 3: Results
+ FileWidget* m_fileWidget;
+ QPushButton* m_saveButton;
+ QLabel* m_elapsedLabel;
+ QPushButton* m_commitButton;
+
+ QElapsedTimer m_processTimer;
+};
diff --git a/plugins/warp/ui/shared/search.cpp b/plugins/warp/ui/shared/search.cpp
index a852605e..801aa23d 100644
--- a/plugins/warp/ui/shared/search.cpp
+++ b/plugins/warp/ui/shared/search.cpp
@@ -1,6 +1,6 @@
#include "search.h"
#include "misc.h"
-#include "../../../../../ui/mainwindow.h"
+#include "viewframe.h"
QVariant WarpSearchModel::data(const QModelIndex& index, int role) const
{
@@ -12,8 +12,8 @@ QVariant WarpSearchModel::data(const QModelIndex& index, int role) const
if (role == Qt::UserRole && index.column() == DisplayCol)
{
if (it && it->GetKind() == WARPContainerSearchItemKindFunction)
- if (auto itemType = it->GetType(nullptr))
- return QVariant::fromValue(TokenData(*itemType, it->GetName()));
+ if (auto itemType = it->GetType())
+ return QVariant::fromValue(TokenData(*itemType->GetAnalysisType(), it->GetName()));
return {};
}
@@ -246,21 +246,16 @@ WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget
return;
// TODO: Getting the current view here is really awful, but i dont care right now.
- auto ctx = MainWindow::activeContext();
+ auto ctx = UIContext::activeContext();
auto view = ctx->getCurrentView();
auto binaryView = view->getData();
auto viewFrame = ctx->getCurrentViewFrame();
auto viewLocation = viewFrame->getViewLocation();
auto func = viewLocation.getFunction();
- // Retrieve the current architecture from the current function or try the current view.
- auto arch = binaryView->GetDefaultArchitecture();
- if (func)
- arch = func->GetArchitecture();
-
const int row = idx.row();
const auto item = m_model->itemAt(row);
- const auto itemType = item->GetType(arch);
+ const auto itemType = item->GetType();
const auto itemFunc = item->GetFunction();
QMenu menu(this);
@@ -272,7 +267,7 @@ WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget
// We let users apply the type for types and functions (assuming the function has one)
// if the user applies a function, we actually will set the user type for the current view location function.
// For types, we will just throw it in the user types.
- applyType->setEnabled(itemType != nullptr);
+ applyType->setEnabled(itemType->m_object != nullptr);
applyType->setVisible(applyType->isEnabled());
applyFunction->setEnabled(func != nullptr && itemFunc);
@@ -294,11 +289,12 @@ WarpSearchWidget::WarpSearchWidget(Warp::Ref<Warp::Container> container, QWidget
{
if (func && item->GetKind() == WARPContainerSearchItemKindFunction)
{
- func->SetUserType(itemType);
+ func->SetUserType(itemType->GetAnalysisType(func->GetArchitecture()));
binaryView->UpdateAnalysis();
}
else
- binaryView->DefineUserType(item->GetName(), itemType);
+ binaryView->DefineUserType(
+ item->GetName(), itemType->GetAnalysisType(binaryView->GetDefaultArchitecture()));
}
else if (chosen == applyFunction)
{
diff --git a/plugins/warp/ui/shared/selectprojectfilesdialog.cpp b/plugins/warp/ui/shared/selectprojectfilesdialog.cpp
new file mode 100644
index 00000000..e8454cff
--- /dev/null
+++ b/plugins/warp/ui/shared/selectprojectfilesdialog.cpp
@@ -0,0 +1,141 @@
+#include "selectprojectfilesdialog.h"
+
+#include <QFormLayout>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+using namespace BinaryNinja;
+
+SelectProjectFilesDialog::SelectProjectFilesDialog(QWidget* parent) : QDialog(parent)
+{
+ setWindowTitle("Select Project Files");
+ setMinimumSize(700, 400);
+ auto* layout = new QVBoxLayout(this);
+
+ auto* topLayout = new QFormLayout();
+ m_projectCombo = new QComboBox(this);
+ auto projects = Project::GetOpenProjects();
+ for (auto& project : projects)
+ {
+ m_projectCombo->addItem(QString::fromStdString(project->GetName()), QVariant::fromValue(project));
+ }
+ topLayout->addRow("Project:", m_projectCombo);
+ layout->addLayout(topLayout);
+
+ m_searchBar = new QLineEdit(this);
+ m_searchBar->setPlaceholderText("Search files...");
+ connect(m_searchBar, &QLineEdit::textChanged, this, &SelectProjectFilesDialog::filterLists);
+ layout->addWidget(m_searchBar);
+
+ auto* listsLayout = new QHBoxLayout();
+
+ auto* notAddingBox = new QVBoxLayout();
+ notAddingBox->addWidget(new QLabel("Available:"));
+ m_notAddingList = new QListWidget(this);
+ m_notAddingList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ m_notAddingList->setTextElideMode(Qt::ElideLeft);
+ m_notAddingList->setStyleSheet("QListWidget::item { padding: 2px; }");
+ notAddingBox->addWidget(m_notAddingList);
+ listsLayout->addLayout(notAddingBox);
+
+ auto* middleButtons = new QVBoxLayout();
+ middleButtons->addStretch();
+ auto* addButton = new QPushButton(">>", this);
+ connect(addButton, &QPushButton::clicked, [this]() { moveSelected(m_notAddingList, m_addingList); });
+ middleButtons->addWidget(addButton);
+ auto* removeButton = new QPushButton("<<", this);
+ connect(removeButton, &QPushButton::clicked, [this]() { moveSelected(m_addingList, m_notAddingList); });
+ middleButtons->addWidget(removeButton);
+ middleButtons->addStretch();
+ listsLayout->addLayout(middleButtons);
+
+ auto* addingBox = new QVBoxLayout();
+ addingBox->addWidget(new QLabel("Selected:"));
+ m_addingList = new QListWidget(this);
+ m_addingList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ m_addingList->setTextElideMode(Qt::ElideLeft);
+ m_addingList->setStyleSheet("QListWidget::item { padding: 2px; }");
+ addingBox->addWidget(m_addingList);
+ listsLayout->addLayout(addingBox);
+
+ layout->addLayout(listsLayout);
+
+ auto* buttons = new QHBoxLayout(this);
+ auto* ok = new QPushButton("Add", this);
+ connect(ok, &QPushButton::clicked, this, &QDialog::accept);
+ auto* cancel = new QPushButton("Cancel", this);
+ connect(cancel, &QPushButton::clicked, this, &QDialog::reject);
+ buttons->addStretch();
+ buttons->addWidget(ok);
+ buttons->addWidget(cancel);
+ layout->addLayout(buttons);
+
+ connect(m_projectCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+ &SelectProjectFilesDialog::updateFileList);
+
+ connect(m_notAddingList, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) {
+ m_addingList->addItem(m_notAddingList->takeItem(m_notAddingList->row(item)));
+ filterLists();
+ });
+ connect(m_addingList, &QListWidget::itemDoubleClicked, [this](QListWidgetItem* item) {
+ m_notAddingList->addItem(m_addingList->takeItem(m_addingList->row(item)));
+ filterLists();
+ });
+
+ updateFileList();
+}
+
+void SelectProjectFilesDialog::updateFileList()
+{
+ m_notAddingList->clear();
+ m_addingList->clear();
+ m_currentProject = m_projectCombo->currentData().value<Ref<Project>>();
+ if (m_currentProject)
+ {
+ for (auto& file : m_currentProject->GetFiles())
+ {
+ auto* item = new QListWidgetItem(QString::fromStdString(file->GetPathInProject()));
+ item->setData(Qt::UserRole, QVariant::fromValue(file));
+ m_notAddingList->addItem(item);
+ }
+ }
+ filterLists();
+}
+
+void SelectProjectFilesDialog::filterLists()
+{
+ QString filter = m_searchBar->text().toLower();
+ for (int i = 0; i < m_notAddingList->count(); ++i)
+ {
+ auto* item = m_notAddingList->item(i);
+ item->setHidden(!item->text().toLower().contains(filter));
+ }
+ for (int i = 0; i < m_addingList->count(); ++i)
+ {
+ auto* item = m_addingList->item(i);
+ item->setHidden(!item->text().toLower().contains(filter));
+ }
+}
+
+void SelectProjectFilesDialog::moveSelected(QListWidget* from, QListWidget* to)
+{
+ QList<QListWidgetItem*> items = from->selectedItems();
+ for (auto* item : items)
+ to->addItem(from->takeItem(from->row(item)));
+ filterLists();
+}
+
+std::vector<Ref<ProjectFile>> SelectProjectFilesDialog::getSelectedFiles() const
+{
+ std::vector<Ref<ProjectFile>> files;
+ for (int i = 0; i < m_addingList->count(); ++i)
+ files.push_back(m_addingList->item(i)->data(Qt::UserRole).value<Ref<ProjectFile>>());
+ return files;
+}
+
+Ref<Project> SelectProjectFilesDialog::getSelectedProject() const
+{
+ return m_currentProject;
+} \ No newline at end of file
diff --git a/plugins/warp/ui/shared/selectprojectfilesdialog.h b/plugins/warp/ui/shared/selectprojectfilesdialog.h
new file mode 100644
index 00000000..ce5e8093
--- /dev/null
+++ b/plugins/warp/ui/shared/selectprojectfilesdialog.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <QComboBox>
+#include <QDialog>
+#include <QListWidget>
+
+#include "binaryninjaapi.h"
+
+class SelectProjectFilesDialog : public QDialog
+{
+ Q_OBJECT
+ BinaryNinja::Ref<BinaryNinja::Project> m_currentProject;
+ QComboBox* m_projectCombo;
+ QLineEdit* m_searchBar;
+ QListWidget* m_notAddingList;
+ QListWidget* m_addingList;
+
+public:
+ SelectProjectFilesDialog(QWidget* parent = nullptr);
+
+ void updateFileList();
+ void filterLists();
+ void moveSelected(QListWidget* from, QListWidget* to);
+ [[nodiscard]] std::vector<BinaryNinja::Ref<BinaryNinja::ProjectFile>> getSelectedFiles() const;
+ [[nodiscard]] BinaryNinja::Ref<BinaryNinja::Project> getSelectedProject() const;
+};
diff --git a/plugins/warp/ui/shared/source.cpp b/plugins/warp/ui/shared/source.cpp
new file mode 100644
index 00000000..c7736151
--- /dev/null
+++ b/plugins/warp/ui/shared/source.cpp
@@ -0,0 +1,204 @@
+#include "source.h"
+
+#include <QClipboard>
+#include <QFileInfo>
+#include <QHeaderView>
+#include <QPainter>
+
+QVariant WarpSourcesModel::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid())
+ return {};
+ if (index.row() < 0 || index.row() >= rowCount())
+ return {};
+
+ const auto& r = m_rows[static_cast<size_t>(index.row())];
+
+ // Build a small two-dot status icon (left: writable, right: uncommitted)
+ auto statusIcon = [](bool writable, bool uncommitted) -> QIcon {
+ static QIcon cache[2][2]; // [writable][uncommitted]
+ QIcon& cached = cache[writable ? 1 : 0][uncommitted ? 1 : 0];
+ if (!cached.isNull())
+ return cached;
+
+ const int w = 16, h = 12, radius = 4;
+ QPixmap pm(w, h);
+ pm.fill(Qt::transparent);
+ QPainter p(&pm);
+ p.setRenderHint(QPainter::Antialiasing, true);
+
+ // Colors
+ QColor writableOn(76, 175, 80); // green
+ QColor writableOff(158, 158, 158); // grey
+ QColor uncommittedOn(255, 193, 7); // amber
+ QColor uncommittedOff(158, 158, 158); // grey
+
+ // Left dot: writable
+ p.setBrush(writable ? writableOn : writableOff);
+ p.setPen(Qt::NoPen);
+ p.drawEllipse(QPoint(4, h / 2), radius, radius);
+
+ // Right dot: uncommitted
+ p.setBrush(uncommitted ? uncommittedOn : uncommittedOff);
+ p.drawEllipse(QPoint(w - 6, h / 2), radius, radius);
+
+ p.end();
+ cached = QIcon(pm);
+ return cached;
+ };
+
+ if (role == Qt::DecorationRole && index.column() == PathCol)
+ {
+ return statusIcon(r.writable, r.uncommitted);
+ }
+
+ if (role == Qt::ToolTipRole && index.column() == PathCol)
+ {
+ QStringList parts;
+ parts << (r.writable ? "Writable" : "Read-only");
+ parts << (r.uncommitted ? "Uncommitted changes" : "No uncommitted changes");
+ return parts.join(" • ");
+ }
+
+ if (role == Qt::DisplayRole)
+ {
+ switch (index.column())
+ {
+ case GuidCol:
+ return r.guid;
+ case PathCol:
+ return r.path;
+ case WritableCol:
+ return r.writable ? "Yes" : "No";
+ case UncommittedCol:
+ return r.uncommitted ? "Yes" : "No";
+ default:
+ return {};
+ }
+ }
+
+ if (role == Qt::CheckStateRole)
+ {
+ // Optional: expose as checkboxes if someone ever shows these columns
+ switch (index.column())
+ {
+ case WritableCol:
+ return r.writable ? Qt::Checked : Qt::Unchecked;
+ case UncommittedCol:
+ return r.uncommitted ? Qt::Checked : Qt::Unchecked;
+ default:
+ break;
+ }
+ }
+
+ return {};
+}
+
+WarpSourcesView::WarpSourcesView(QWidget* parent) : QTableView(parent)
+{
+ m_model = new WarpSourcesModel(this);
+ QTableView::setModel(m_model);
+
+ horizontalHeader()->setStretchLastSection(true);
+ setSelectionBehavior(SelectRows);
+ setSelectionMode(SingleSelection);
+
+ // Make the table look like a simple list that shows only the source path
+ setShowGrid(false);
+ verticalHeader()->setVisible(false);
+ horizontalHeader()->setVisible(false);
+ setAlternatingRowColors(false);
+ setEditTriggers(NoEditTriggers);
+ setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ setWordWrap(false);
+ setIconSize(QSize(16, 12));
+ // Ensure long paths truncate from the left: "...tail/of/the/path"
+ setTextElideMode(Qt::ElideLeft);
+
+ // Hide GUID column, keep only the Path column visible
+ setColumnHidden(WarpSourcesModel::GuidCol, true);
+ // Also hide boolean columns; their state is shown as an icon next to the path
+ setColumnHidden(WarpSourcesModel::WritableCol, true);
+ setColumnHidden(WarpSourcesModel::UncommittedCol, true);
+ // Ensure the remaining (Path) column fills the width
+ horizontalHeader()->setSectionResizeMode(WarpSourcesModel::PathCol, QHeaderView::Stretch);
+
+ // Per-item context menu
+ setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(this, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
+ if (!m_model || !m_container)
+ return;
+
+ QMenu menu(this);
+ const QModelIndex index = indexAt(pos);
+
+ if (!index.isValid())
+ {
+ QAction* actAdd = menu.addAction(tr("Add Source"));
+ QAction* chosen = menu.exec(viewport()->mapToGlobal(pos));
+ if (!chosen)
+ return;
+ if (chosen == actAdd)
+ addSource();
+ }
+ else
+ {
+ setCurrentIndex(index.sibling(index.row(), WarpSourcesModel::PathCol));
+
+ const int row = index.row();
+ const QModelIndex pathIdx = m_model->index(row, WarpSourcesModel::PathCol);
+ const QModelIndex guidIdx = m_model->index(row, WarpSourcesModel::GuidCol);
+ const QString path = m_model->data(pathIdx, Qt::DisplayRole).toString();
+ const QFileInfo fi(path);
+
+ const QString guid = m_model->data(guidIdx, Qt::DisplayRole).toString();
+
+ QAction* actReveal = menu.addAction(tr("Reveal in File Browser"));
+ actReveal->setEnabled(fi.exists());
+ QAction* actCopyPath = menu.addAction(tr("Copy Path"));
+ QAction* actCopyGuid = menu.addAction(tr("Copy GUID"));
+
+ QAction* chosen = menu.exec(viewport()->mapToGlobal(pos));
+ if (!chosen)
+ return;
+ if (chosen == actCopyPath)
+ QGuiApplication::clipboard()->setText(path);
+ else if (chosen == actCopyGuid)
+ QGuiApplication::clipboard()->setText(guid);
+ else if (chosen == actReveal)
+ QDesktopServices::openUrl(QUrl::fromLocalFile(fi.absoluteFilePath()));
+ }
+ });
+}
+
+void WarpSourcesView::setContainer(Warp::Ref<Warp::Container> container)
+{
+ m_container = std::move(container);
+ m_model->setContainer(m_container);
+}
+
+bool WarpSourcesView::addSource()
+{
+ if (!m_model || !m_container)
+ return false;
+
+ std::string sourceName;
+ if (!BinaryNinja::GetTextLineInput(sourceName, "Source name:", "Add Source"))
+ return false;
+ if (const auto sourceId = m_container->AddSource(sourceName); !sourceId.has_value())
+ {
+ BinaryNinja::LogAlertF("Failed to add source: {}", sourceName);
+ return false;
+ }
+ m_model->reload();
+ return true;
+}
+
+std::optional<Warp::Source> WarpSourcesView::sourceFromRow(int row) const
+{
+ if (!m_model || row < 0 || row >= m_model->rowCount())
+ return std::nullopt;
+ const QModelIndex guidIdx = m_model->index(row, WarpSourcesModel::GuidCol);
+ std::string guidStr = m_model->data(guidIdx, Qt::DisplayRole).toString().toStdString();
+ return Warp::WarpUUID::FromString(guidStr);
+} \ No newline at end of file
diff --git a/plugins/warp/ui/shared/source.h b/plugins/warp/ui/shared/source.h
new file mode 100644
index 00000000..7dad9333
--- /dev/null
+++ b/plugins/warp/ui/shared/source.h
@@ -0,0 +1,116 @@
+#pragma once
+
+#include <QWidget>
+#include <QDesktopServices>
+#include <QInputDialog>
+#include <QListWidget>
+#include <QTableView>
+
+#include "theme.h"
+#include "warp.h"
+
+class WarpSourcesModel final : public QAbstractTableModel
+{
+ Q_OBJECT
+
+public:
+ enum Columns : int
+ {
+ GuidCol = 0,
+ PathCol,
+ WritableCol,
+ UncommittedCol,
+ ColumnCount
+ };
+
+ explicit WarpSourcesModel(QObject* parent = nullptr) : QAbstractTableModel(parent) {}
+
+ void setContainer(Warp::Ref<Warp::Container> container)
+ {
+ m_container = std::move(container);
+ reload();
+ }
+
+ void reload()
+ {
+ // Fetch synchronously (can be adapted to async if needed)
+ beginResetModel();
+ m_rows.clear();
+ for (const auto& src : m_container->GetSources())
+ {
+ QString guid = QString::fromStdString(src.ToString());
+ QString path = QString::fromStdString(m_container->SourcePath(src).value_or(std::string {}));
+ bool writable = m_container->IsSourceWritable(src);
+ bool uncommitted = m_container->IsSourceUncommitted(src);
+ m_rows.push_back({guid, path, writable, uncommitted});
+ }
+ endResetModel();
+ }
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override
+ {
+ if (parent.isValid())
+ return 0;
+ return static_cast<int>(m_rows.size());
+ }
+
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override
+ {
+ Q_UNUSED(parent);
+ return ColumnCount;
+ }
+
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override
+ {
+ if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
+ {
+ switch (section)
+ {
+ case GuidCol:
+ return "Source GUID";
+ case PathCol:
+ return "Path";
+ case WritableCol:
+ return "Writable";
+ case UncommittedCol:
+ return "Uncommitted";
+ default:
+ return {};
+ }
+ }
+ return {};
+ }
+
+private:
+ struct Row
+ {
+ QString guid;
+ QString path;
+ bool writable;
+ bool uncommitted;
+ };
+
+ std::vector<Row> m_rows;
+ Warp::Ref<Warp::Container> m_container;
+};
+
+
+class WarpSourcesView : public QTableView
+{
+ Q_OBJECT
+
+public:
+ explicit WarpSourcesView(QWidget* parent = nullptr);
+
+ void setContainer(Warp::Ref<Warp::Container> container);
+ bool addSource();
+
+ [[nodiscard]] WarpSourcesModel* sourceModel() const { return m_model; }
+ [[nodiscard]] std::optional<Warp::Source> sourceFromRow(int row) const;
+
+private:
+ WarpSourcesModel* m_model = nullptr;
+ Warp::Ref<Warp::Container> m_container;
+}; \ No newline at end of file