diff options
| author | Mason Reed <mason@vector35.com> | 2026-03-13 12:24:18 -0700 |
|---|---|---|
| committer | Mason Reed <35282038+emesare@users.noreply.github.com> | 2026-03-24 18:46:48 -0700 |
| commit | 3c88b11e5df33116580ac008e36092775df66135 (patch) | |
| tree | cd64fbe149f05683ddabcccd0db7b87356f1f8cf | |
| parent | f325aa7b6026a1daef84931baeb1f50d7da10c10 (diff) | |
[WARP] Improved UX and API
- Exposes WARP type objects directly
- Adds processor API (for generating warp files directly)
- Adds file and chunk API
- Misc cleanup
- Simplified the amount of commands
- Replaced the "Create" commands with a purpose built processor dialog
- Added a native QT viewer for WARP files
- Simplified committing to a remote with a purpose built commit dialog
63 files changed, 2894 insertions, 2011 deletions
@@ -3065,10 +3065,7 @@ dependencies = [ "directories", "insta", "itertools 0.14.0", - "minijinja", - "minijinja-embed", "rayon", - "regex", "serde", "serde_json", "serde_qs", @@ -3095,10 +3092,7 @@ dependencies = [ "directories", "insta", "itertools 0.14.0", - "minijinja", - "minijinja-embed", "rayon", - "regex", "serde", "serde_json", "serde_qs", 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(¤t_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 diff --git a/rust/src/data_buffer.rs b/rust/src/data_buffer.rs index 79b3b20b..f8d71088 100644 --- a/rust/src/data_buffer.rs +++ b/rust/src/data_buffer.rs @@ -32,6 +32,13 @@ impl DataBuffer { self.0 } + /// Return the raw pointer to the underlying data buffer, to be freed later with `BNFreeDataBuffer`. + pub fn into_raw(self) -> *mut BNDataBuffer { + let ptr = self.0; + std::mem::forget(self); + ptr + } + pub fn new(data: &[u8]) -> Self { let buffer = unsafe { BNCreateDataBuffer(data.as_ptr() as *const c_void, data.len()) }; assert!(!buffer.is_null()); diff --git a/rust/src/project/file.rs b/rust/src/project/file.rs index 22bf9b4b..d016d377 100644 --- a/rust/src/project/file.rs +++ b/rust/src/project/file.rs @@ -22,7 +22,7 @@ pub struct ProjectFile { } impl ProjectFile { - pub(crate) unsafe fn from_raw(handle: NonNull<BNProjectFile>) -> Self { + pub unsafe fn from_raw(handle: NonNull<BNProjectFile>) -> Self { Self { handle } } |
