From 3bb4a2df1d6800ba19ca5e68eacdbe83588e2d8d Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 27 Feb 2026 14:10:03 -0800 Subject: [PATCH 01/19] [WARP] Demote locking surrounding container function fetching By demoting the containers lock to read only for fetching we can prevent blocking the main ui thread while waiting for the network requests to finish --- plugins/warp/src/container.rs | 5 +- plugins/warp/src/container/network.rs | 147 ++++++++++++++++------- plugins/warp/src/plugin/ffi/container.rs | 4 +- 3 files changed, 108 insertions(+), 48 deletions(-) diff --git a/plugins/warp/src/container.rs b/plugins/warp/src/container.rs index b5e92bc7d1..79c243b3ee 100644 --- a/plugins/warp/src/container.rs +++ b/plugins/warp/src/container.rs @@ -39,6 +39,8 @@ pub enum ContainerError { SearchFailed(String), #[error("failed to commit source '{0}': {1}")] CommitFailed(SourceId, String), + #[error("container error encountered: {0}")] + Custom(String), } /// Represents the ID for a single container source. @@ -224,6 +226,7 @@ pub trait Container: Send + Sync + Display + Debug { /// to verify the permissions of the source. fn add_source(&mut self, path: SourcePath) -> ContainerResult; + // TODO: Make interior mutable. /// Flush changes made to a source. /// /// Because writing to a source can require file or network operations, we let the container @@ -293,7 +296,7 @@ pub trait Container: Send + Sync + Display + Debug { /// will do nothing. This function is blocking, so assume it will take a few seconds for a container /// that intends to fetch over the network. fn fetch_functions( - &mut self, + &self, _target: &Target, _tags: &[SourceTag], _functions: &[FunctionGUID], diff --git a/plugins/warp/src/container/network.rs b/plugins/warp/src/container/network.rs index 0ba515f9d9..307b44fa1f 100644 --- a/plugins/warp/src/container/network.rs +++ b/plugins/warp/src/container/network.rs @@ -3,10 +3,12 @@ use crate::container::{ Container, ContainerError, ContainerResult, ContainerSearchQuery, ContainerSearchResponse, SourceId, SourcePath, SourceTag, }; +use dashmap::DashMap; use directories::ProjectDirs; use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Display, Formatter}; use std::path::PathBuf; +use std::sync::RwLock; use warp::chunk::{Chunk, ChunkKind, CompressionType}; use warp::r#type::chunk::TypeChunk; use warp::r#type::guid::TypeGUID; @@ -28,15 +30,21 @@ pub struct NetworkContainer { client: NetworkClient, /// This is the store that the interface will write to; then we have special functions for pulling /// and pushing to the network source. - cache: DiskContainer, + cache: RwLock, /// Where to place newly created sources. /// /// This is typically a directory inside [`NetworkContainer::root_cache_location`]. cache_path: PathBuf, /// Populated when targets are queried. - known_targets: HashMap>, + /// + /// NOTE: This is a [`DashMap`] purely for the sake of interior mutability as we do not wish to hold + /// a write lock on the entire container while performing network operations. + known_targets: DashMap>, /// Populated when function sources are queried. - known_function_sources: HashMap>, + /// + /// NOTE: This is a [`DashMap`] purely for the sake of interior mutability as we do not wish to hold + /// a write lock on the entire container while performing network operations. + known_function_sources: DashMap>, /// Populated when user adds function, this is used for writing back to the server. added_chunks: HashMap>>, /// Populated when connecting to the server, this is used to determine which sources are writable. @@ -47,12 +55,12 @@ pub struct NetworkContainer { impl NetworkContainer { pub fn new(client: NetworkClient, cache_path: PathBuf, writable_sources: &[SourceId]) -> Self { - let mut container = Self { - cache: DiskContainer::new_from_dir(cache_path.clone()), + let container = Self { + cache: RwLock::new(DiskContainer::new_from_dir(cache_path.clone())), cache_path, client, - known_targets: HashMap::new(), - known_function_sources: HashMap::new(), + known_targets: DashMap::new(), + known_function_sources: DashMap::new(), added_chunks: HashMap::new(), writable_sources: writable_sources.into_iter().copied().collect(), }; @@ -74,7 +82,7 @@ impl NetworkContainer { /// # Caching policy /// /// The [`NetworkTargetId`] is unique and immutable, so they will be persisted indefinitely. - pub fn get_target_id(&mut self, target: &Target) -> Option { + pub fn get_target_id(&self, target: &Target) -> Option { // It's highly probable we have previously queried the target, check that first. if let Some(target_id) = self.known_targets.get(target) { return target_id.clone(); @@ -96,7 +104,7 @@ impl NetworkContainer { /// for now as the requests for functions come at the request of some user interaction. Any guid /// with no sources will still be cached. pub fn get_unseen_functions_source( - &mut self, + &self, target: Option<&Target>, tags: &[SourceTag], guids: &[FunctionGUID], @@ -157,12 +165,7 @@ impl NetworkContainer { /// Every request we store the returned objects on disk, this means that users will first /// query against the disk objects, then the server. This also means we need to cache functions f /// or which we have not received any functions for, as otherwise we would keep trying to query it. - pub fn pull_functions( - &mut self, - target: &Target, - source: &SourceId, - functions: &[FunctionGUID], - ) { + pub fn pull_functions(&self, target: &Target, source: &SourceId, functions: &[FunctionGUID]) { let target_id = self.get_target_id(target); let file = match self .client @@ -182,18 +185,25 @@ impl NetworkContainer { let functions: Vec<_> = sc.functions().collect(); // Probe the source before attempting to access it, as it might not exist locally. self.probe_source(*source); - match self.cache.add_functions(target, source, &functions) { - Ok(_) => tracing::debug!( - "Added {} functions into cached source '{}'", - functions.len(), - source - ), - Err(err) => tracing::error!( - "Failed to add {} function into cached source '{}': {}", - functions.len(), - source, - err - ), + + match self.cache.write() { + Ok(mut cache) => match cache.add_functions(target, source, &functions) { + Ok(_) => tracing::debug!( + "Added {} functions into cached source '{}'", + functions.len(), + source + ), + Err(err) => tracing::error!( + "Failed to add {} function into cached source '{}': {}", + functions.len(), + source, + err + ), + }, + Err(err) => { + tracing::error!("Failed to write to cache: {}", err); + return; + } } } // TODO; Probably want to pull type in with this. @@ -214,8 +224,13 @@ impl NetworkContainer { /// Probe the source to make sure it exists in the cache. Retrieving the name from the server. /// /// **This is blocking** - pub fn probe_source(&mut self, source_id: SourceId) { - if !self.cache.source_path(&source_id).is_ok() { + pub fn probe_source(&self, source_id: SourceId) { + let Ok(mut cache) = self.cache.write() else { + tracing::error!("Cannot probe source '{}', cache is poisoned", source_id); + return; + }; + + if !cache.source_path(&source_id).is_ok() { // Add the source to the cache. Using the source id and source name as the source path. match self.client.source_name(source_id) { Ok(source_name) => { @@ -224,7 +239,7 @@ impl NetworkContainer { .cache_path .join(source_id.to_string()) .join(source_name); - let _ = self.cache.insert_source(source_id, SourcePath(source_path)); + let _ = cache.insert_source(source_id, SourcePath(source_path)); } Err(e) => { tracing::error!("Failed to probe source '{}': {}", source_id, e); @@ -251,7 +266,10 @@ impl NetworkContainer { impl Container for NetworkContainer { fn sources(&self) -> ContainerResult> { - self.cache.sources() + self.cache + .read() + .map_err(|e| ContainerError::Custom(format!("Cache read error: {}", e)))? + .sources() } fn add_source(&mut self, path: SourcePath) -> ContainerResult { @@ -295,11 +313,17 @@ impl Container for NetworkContainer { } fn source_tags(&self, source: &SourceId) -> ContainerResult> { - self.cache.source_tags(source) + self.cache + .read() + .map_err(|e| ContainerError::Custom(format!("Cache read error: {}", e)))? + .source_tags(source) } fn source_path(&self, source: &SourceId) -> ContainerResult { - self.cache.source_path(source) + self.cache + .read() + .map_err(|e| ContainerError::Custom(format!("Cache read error: {}", e)))? + .source_path(source) } fn add_computed_types( @@ -310,7 +334,10 @@ impl Container for NetworkContainer { // NOTE: We must `add_computed_types` to the cache before we add the chunk, as `added_chunks` is // not consulted when retrieving types from the cache, if we fail to add the types to // the cache, we will not see them show up in the UI or when matching. - self.cache.add_computed_types(source, types)?; + self.cache + .write() + .map_err(|e| ContainerError::Custom(format!("Cache write error: {}", e)))? + .add_computed_types(source, types)?; let type_chunk = TypeChunk::new_with_computed(types).ok_or( ContainerError::CorruptedData("signature chunk failed to validate"), )?; @@ -320,7 +347,10 @@ impl Container for NetworkContainer { } fn remove_types(&mut self, source: &SourceId, guids: &[TypeGUID]) -> ContainerResult<()> { - self.cache.remove_types(source, guids) + self.cache + .write() + .map_err(|e| ContainerError::Custom(format!("Cache write error: {}", e)))? + .remove_types(source, guids) } fn add_functions( @@ -332,7 +362,10 @@ impl Container for NetworkContainer { // NOTE: We must `add_functions` to the cache before we add the chunk, as `added_chunks` is // not consulted when retrieving functions from the cache, if we fail to add the functions to // the cache, we will not see them show up in the UI or when matching. - self.cache.add_functions(target, source, functions)?; + self.cache + .write() + .map_err(|e| ContainerError::Custom(format!("Cache write error: {}", e)))? + .add_functions(target, source, functions)?; let signature_chunk = SignatureChunk::new(functions).ok_or( ContainerError::CorruptedData("signature chunk failed to validate"), )?; @@ -352,11 +385,14 @@ impl Container for NetworkContainer { functions: &[Function], ) -> ContainerResult<()> { // TODO: Wont persist, need to add remote removal. - self.cache.remove_functions(target, source, functions) + self.cache + .write() + .map_err(|e| ContainerError::Custom(format!("Cache write error: {}", e)))? + .remove_functions(target, source, functions) } fn fetch_functions( - &mut self, + &self, target: &Target, tags: &[SourceTag], functions: &[FunctionGUID], @@ -376,14 +412,20 @@ impl Container for NetworkContainer { } fn sources_with_type_guid(&self, guid: &TypeGUID) -> ContainerResult> { - self.cache.sources_with_type_guid(guid) + self.cache + .read() + .map_err(|e| ContainerError::Custom(format!("Cache read error: {}", e)))? + .sources_with_type_guid(guid) } fn sources_with_type_guids( &self, guids: &[TypeGUID], ) -> ContainerResult>> { - self.cache.sources_with_type_guids(guids) + self.cache + .read() + .map_err(|e| ContainerError::Custom(format!("Cache read error: {}", e)))? + .sources_with_type_guids(guids) } fn type_guids_with_name( @@ -391,11 +433,17 @@ impl Container for NetworkContainer { source: &SourceId, name: &str, ) -> ContainerResult> { - self.cache.type_guids_with_name(source, name) + self.cache + .read() + .map_err(|e| ContainerError::Custom(format!("Cache read error: {}", e)))? + .type_guids_with_name(source, name) } fn type_with_guid(&self, source: &SourceId, guid: &TypeGUID) -> ContainerResult> { - self.cache.type_with_guid(source, guid) + self.cache + .read() + .map_err(|e| ContainerError::Custom(format!("Cache read error: {}", e)))? + .type_with_guid(source, guid) } fn sources_with_function_guid( @@ -403,7 +451,10 @@ impl Container for NetworkContainer { target: &Target, guid: &FunctionGUID, ) -> ContainerResult> { - self.cache.sources_with_function_guid(target, guid) + self.cache + .read() + .map_err(|e| ContainerError::Custom(format!("Cache read error: {}", e)))? + .sources_with_function_guid(target, guid) } fn sources_with_function_guids( @@ -411,7 +462,10 @@ impl Container for NetworkContainer { target: &Target, guids: &[FunctionGUID], ) -> ContainerResult>> { - self.cache.sources_with_function_guids(target, guids) + self.cache + .read() + .map_err(|e| ContainerError::Custom(format!("Cache read error: {}", e)))? + .sources_with_function_guids(target, guids) } fn functions_with_guid( @@ -420,7 +474,10 @@ impl Container for NetworkContainer { source: &SourceId, guid: &FunctionGUID, ) -> ContainerResult> { - self.cache.functions_with_guid(target, source, guid) + self.cache + .read() + .map_err(|e| ContainerError::Custom(format!("Cache read error: {}", e)))? + .functions_with_guid(target, source, guid) } fn search(&self, query: &ContainerSearchQuery) -> ContainerResult { diff --git a/plugins/warp/src/plugin/ffi/container.rs b/plugins/warp/src/plugin/ffi/container.rs index a107a78538..79f45dcb4c 100644 --- a/plugins/warp/src/plugin/ffi/container.rs +++ b/plugins/warp/src/plugin/ffi/container.rs @@ -220,7 +220,7 @@ pub unsafe extern "C" fn BNWARPContainerFetchFunctions( count: usize, ) { let arc_container = ManuallyDrop::new(Arc::from_raw(container)); - let Ok(mut container) = arc_container.write() else { + let Ok(container) = arc_container.read() else { return; }; @@ -246,7 +246,7 @@ pub unsafe extern "C" fn BNWARPContainerGetSources( count: *mut usize, ) -> *mut BNWARPSource { let arc_container = ManuallyDrop::new(Arc::from_raw(container)); - let Ok(container) = arc_container.write() else { + let Ok(container) = arc_container.read() else { return std::ptr::null_mut(); }; From bd86cf5647915dec342440d83eb1d3295a2a914b Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Tue, 3 Mar 2026 07:57:33 -0800 Subject: [PATCH 02/19] [WARP] Server-side constraint matching Reduce networked functions by constraining on the returned set of functions on the server --- plugins/warp/api/python/warp.py | 10 ++++-- plugins/warp/api/warp.cpp | 9 +++-- plugins/warp/api/warp.h | 2 +- plugins/warp/api/warpcore.h | 2 +- plugins/warp/src/container.rs | 5 +++ plugins/warp/src/container/network.rs | 36 +++++++++++++------- plugins/warp/src/container/network/client.rs | 25 ++++++++++---- plugins/warp/src/plugin/ffi/container.rs | 8 +++-- plugins/warp/src/plugin/settings.rs | 6 ++-- plugins/warp/src/plugin/workflow.rs | 25 ++++++++++++-- plugins/warp/ui/shared/fetchdialog.cpp | 34 +++++------------- plugins/warp/ui/shared/fetchdialog.h | 3 +- plugins/warp/ui/shared/fetcher.cpp | 28 ++++++++++++--- plugins/warp/ui/shared/misc.h | 4 +-- 14 files changed, 129 insertions(+), 68 deletions(-) diff --git a/plugins/warp/api/python/warp.py b/plugins/warp/api/python/warp.py index a699ebbcb7..2ba1c68189 100644 --- a/plugins/warp/api/python/warp.py +++ b/plugins/warp/api/python/warp.py @@ -426,11 +426,17 @@ def remove_types(self, source: Source, guids: List[TypeGUID]) -> bool: core_guids[i] = guids[i].uuid return warpcore.BNWARPContainerRemoveTypes(self.handle, source.uuid, core_guids, count) - def fetch_functions(self, target: WarpTarget, guids: List[FunctionGUID], source_tags: Optional[List[str]] = None): + def fetch_functions(self, target: WarpTarget, guids: List[FunctionGUID], source_tags: Optional[List[str]] = None, constraints: Optional[List[ConstraintGUID]] = None): count = len(guids) core_guids = (warpcore.BNWARPFunctionGUID * count)() for i in range(count): core_guids[i] = guids[i].uuid + if constraints is None: + constraints = [] + constraints_count = len(constraints) + core_constraints = (warpcore.BNWARPConstraintGUID * constraints_count)() + for i in range(constraints_count): + core_constraints[i] = constraints[i].uuid if source_tags is None: source_tags = [] source_tags_ptr = (ctypes.c_char_p * len(source_tags))() @@ -438,7 +444,7 @@ def fetch_functions(self, target: WarpTarget, guids: List[FunctionGUID], source_ for i in range(len(source_tags)): source_tags_ptr[i] = source_tags[i].encode('utf-8') source_tags_array_ptr = ctypes.cast(source_tags_ptr, ctypes.POINTER(ctypes.c_char_p)) - warpcore.BNWARPContainerFetchFunctions(self.handle, target.handle, source_tags_array_ptr, source_tags_len, core_guids, count) + warpcore.BNWARPContainerFetchFunctions(self.handle, target.handle, source_tags_array_ptr, source_tags_len, core_guids, count, core_constraints, constraints_count) def get_sources_with_function_guid(self, target: WarpTarget, guid: FunctionGUID) -> List[Source]: count = ctypes.c_size_t() diff --git a/plugins/warp/api/warp.cpp b/plugins/warp/api/warp.cpp index c051e3c78f..debbbc00a7 100644 --- a/plugins/warp/api/warp.cpp +++ b/plugins/warp/api/warp.cpp @@ -353,7 +353,7 @@ bool Container::RemoveTypes(const Source &source, const std::vector &g return result; } -void Container::FetchFunctions(const Target &target, const std::vector &guids, const std::vector &tags) const +void Container::FetchFunctions(const Target &target, const std::vector &guids, const std::vector &tags, const std::vector &constraints) const { size_t count = guids.size(); BNWARPFunctionGUID *apiGuids = new BNWARPFunctionGUID[count]; @@ -363,9 +363,14 @@ void Container::FetchFunctions(const Target &target, const std::vector Container::GetSourcesWithFunctionGUID(const Target& target, const FunctionGUID &guid) const diff --git a/plugins/warp/api/warp.h b/plugins/warp/api/warp.h index 01f4aaee8e..ccb4da9706 100644 --- a/plugins/warp/api/warp.h +++ b/plugins/warp/api/warp.h @@ -409,7 +409,7 @@ namespace Warp { bool RemoveTypes(const Source &source, const std::vector &guids) const; - void FetchFunctions(const Target &target, const std::vector &guids, const std::vector &tags = {}) const; + void FetchFunctions(const Target &target, const std::vector &guids, const std::vector &tags = {}, const std::vector &constraints = {}) const; std::vector GetSourcesWithFunctionGUID(const Target &target, const FunctionGUID &guid) const; diff --git a/plugins/warp/api/warpcore.h b/plugins/warp/api/warpcore.h index c52485aace..21ef105ba5 100644 --- a/plugins/warp/api/warpcore.h +++ b/plugins/warp/api/warpcore.h @@ -128,7 +128,7 @@ extern "C" WARP_FFI_API bool BNWARPContainerRemoveFunctions(BNWARPContainer* container, const BNWARPTarget* target, const BNWARPSource* source, BNWARPFunction** functions, size_t count); WARP_FFI_API bool BNWARPContainerRemoveTypes(BNWARPContainer* container, const BNWARPSource* source, BNWARPTypeGUID* types, size_t count); - WARP_FFI_API void BNWARPContainerFetchFunctions(BNWARPContainer* container, BNWARPTarget* target, const char** sourceTags, size_t sourceTagCount, const BNWARPTypeGUID* guids, size_t count); + WARP_FFI_API void BNWARPContainerFetchFunctions(BNWARPContainer* container, BNWARPTarget* target, const char** sourceTags, size_t sourceTagCount, const BNWARPFunctionGUID* guids, size_t count, const BNWARPConstraintGUID* constraints, size_t constraintCount); 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); diff --git a/plugins/warp/src/container.rs b/plugins/warp/src/container.rs index 79c243b3ee..4feed24cac 100644 --- a/plugins/warp/src/container.rs +++ b/plugins/warp/src/container.rs @@ -9,6 +9,7 @@ use thiserror::Error; use uuid::Uuid; 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; @@ -295,11 +296,15 @@ pub trait Container: Send + Sync + Display + Debug { /// Typically, a container that resides only in memory has nothing to fetch, so the default implementation /// will do nothing. This function is blocking, so assume it will take a few seconds for a container /// that intends to fetch over the network. + /// + /// To constrain on the fetched functions, pass a list of [`ConstraintGUID`]s that will be + /// used to filter the fetched functions which do not contain at least one of the constraints. fn fetch_functions( &self, _target: &Target, _tags: &[SourceTag], _functions: &[FunctionGUID], + _constraints: &[ConstraintGUID], ) -> ContainerResult<()> { Ok(()) } diff --git a/plugins/warp/src/container/network.rs b/plugins/warp/src/container/network.rs index 307b44fa1f..881928446b 100644 --- a/plugins/warp/src/container/network.rs +++ b/plugins/warp/src/container/network.rs @@ -14,6 +14,7 @@ use warp::r#type::chunk::TypeChunk; use warp::r#type::guid::TypeGUID; use warp::r#type::{ComputedType, Type}; use warp::signature::chunk::SignatureChunk; +use warp::signature::constraint::ConstraintGUID; use warp::signature::function::{Function, FunctionGUID}; use warp::target::Target; use warp::{WarpFile, WarpFileHeader}; @@ -45,7 +46,7 @@ pub struct NetworkContainer { /// NOTE: This is a [`DashMap`] purely for the sake of interior mutability as we do not wish to hold /// a write lock on the entire container while performing network operations. known_function_sources: DashMap>, - /// Populated when user adds function, this is used for writing back to the server. + /// Populated when the user adds a function, this is used for writing back to the server. added_chunks: HashMap>>, /// Populated when connecting to the server, this is used to determine which sources are writable. /// @@ -165,18 +166,25 @@ impl NetworkContainer { /// Every request we store the returned objects on disk, this means that users will first /// query against the disk objects, then the server. This also means we need to cache functions f /// or which we have not received any functions for, as otherwise we would keep trying to query it. - pub fn pull_functions(&self, target: &Target, source: &SourceId, functions: &[FunctionGUID]) { + pub fn pull_functions( + &self, + target: &Target, + source: &SourceId, + functions: &[FunctionGUID], + constraints: &[ConstraintGUID], + ) { let target_id = self.get_target_id(target); - let file = match self - .client - .query_functions(target_id, Some(*source), functions) - { - Ok(file) => file, - Err(e) => { - tracing::error!("Failed to query functions: {}", e); - return; - } - }; + let file = + match self + .client + .query_functions(target_id, Some(*source), functions, constraints) + { + Ok(file) => file, + Err(e) => { + tracing::error!("Failed to query functions: {}", e); + return; + } + }; tracing::debug!("Got {} chunks from server", file.chunks.len()); for chunk in &file.chunks { @@ -396,16 +404,18 @@ impl Container for NetworkContainer { target: &Target, tags: &[SourceTag], functions: &[FunctionGUID], + constraints: &[ConstraintGUID], ) -> ContainerResult<()> { // NOTE: Blocking request to get the mapped function sources. let mapped_unseen_functions = self.get_unseen_functions_source(Some(&target), tags, functions); + // TODO: It would be nice to have a way to not have to pull through each source individually. // Actually get the function data for the unseen guids, we really only want to do this once per // session, anymore, and this is annoying! for (source, unseen_guids) in mapped_unseen_functions { // NOTE: Blocking request to get the function data in the container cache. - self.pull_functions(&target, &source, &unseen_guids); + self.pull_functions(&target, &source, &unseen_guids, constraints); } Ok(()) diff --git a/plugins/warp/src/container/network/client.rs b/plugins/warp/src/container/network/client.rs index 0b772dcc44..3d0af05797 100644 --- a/plugins/warp/src/container/network/client.rs +++ b/plugins/warp/src/container/network/client.rs @@ -7,12 +7,13 @@ use base64::Engine; use binaryninja::download::DownloadProvider; use serde::Deserialize; use serde_json::json; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::str::FromStr; use uuid::Uuid; use warp::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::target::Target; use warp::WarpFile; @@ -30,7 +31,8 @@ pub struct NetworkClient { impl NetworkClient { pub fn new(server_url: String, server_token: Option) -> Self { // TODO: This might want to be kept for the request header? - let mut headers: Vec<(String, String)> = vec![]; + let mut headers: Vec<(String, String)> = + vec![("Content-Encoding".to_string(), "gzip".to_string())]; if let Some(token) = &server_token { headers.push(("authorization".to_string(), format!("Bearer {}", token))); } @@ -214,13 +216,14 @@ impl NetworkClient { source: Option, source_tags: &[SourceTag], guids: &[FunctionGUID], + constraints: &[ConstraintGUID], ) -> serde_json::Value { - let guids_str: Vec = guids.iter().map(|g| g.to_string()).collect(); + let guids_str: HashSet = guids.iter().map(|g| g.to_string()).collect(); // TODO: The limit here needs to be somewhat flexible. But 1000 will do for now. let mut body = json!({ "format": "flatbuffer", "guids": guids_str, - "limit": 1000 + "limit": 10000, }); if let Some(target_id) = target { body["target_id"] = json!(target_id); @@ -231,6 +234,11 @@ impl NetworkClient { if !source_tags.is_empty() { body["source_tags"] = json!(source_tags); } + if !constraints.is_empty() { + let constraint_guids_str: HashSet = + constraints.iter().map(|g| g.to_string()).collect(); + body["constraints"] = json!(constraint_guids_str); + } body } @@ -244,13 +252,13 @@ impl NetworkClient { target: Option, source: Option, guids: &[FunctionGUID], + constraints: &[ConstraintGUID], ) -> Result, String> { let query_functions_url = format!("{}/api/v1/functions/query", self.server_url); // TODO: Allow for source tags? We really only need this in query_functions_source as that // TODO: is what prevents a undesired source from being "known" to the container. - let payload = Self::query_functions_body(target, source, &[], guids); + let payload = Self::query_functions_body(target, source, &[], guids, constraints); let mut inst = self.provider.create_instance().unwrap(); - let resp = inst.post_json(&query_functions_url, self.headers.clone(), &payload)?; if !resp.is_success() { return Err(format!( @@ -275,7 +283,10 @@ impl NetworkClient { ) -> Result>, String> { let query_functions_source_url = format!("{}/api/v1/functions/query/source", self.server_url); - let payload = Self::query_functions_body(target, None, tags, guids); + // NOTE: We do not filter by constraint guids here since this pass is only responsible for + // returning the source ids, not the actual function data, see [`NetworkClient::query_functions`] + // for the place where the constraints are applied, and _do_ matter. + let payload = Self::query_functions_body(target, None, tags, guids, &[]); let mut inst = self.provider.create_instance().unwrap(); let resp = inst.post_json(&query_functions_source_url, self.headers.clone(), &payload)?; diff --git a/plugins/warp/src/plugin/ffi/container.rs b/plugins/warp/src/plugin/ffi/container.rs index 79f45dcb4c..aa0c37d39a 100644 --- a/plugins/warp/src/plugin/ffi/container.rs +++ b/plugins/warp/src/plugin/ffi/container.rs @@ -5,7 +5,8 @@ use crate::container::{ }; use crate::convert::{from_bn_type, to_bn_type}; use crate::plugin::ffi::{ - BNWARPContainer, BNWARPFunction, BNWARPFunctionGUID, BNWARPSource, BNWARPTarget, BNWARPTypeGUID, + BNWARPConstraintGUID, BNWARPContainer, BNWARPFunction, BNWARPFunctionGUID, BNWARPSource, + BNWARPTarget, BNWARPTypeGUID, }; use binaryninja::architecture::CoreArchitecture; use binaryninja::binary_view::BinaryView; @@ -218,6 +219,8 @@ pub unsafe extern "C" fn BNWARPContainerFetchFunctions( source_tags_count: usize, guids: *const BNWARPFunctionGUID, count: usize, + constraints: *const BNWARPConstraintGUID, + constraints_count: usize, ) { let arc_container = ManuallyDrop::new(Arc::from_raw(container)); let Ok(container) = arc_container.read() else { @@ -234,8 +237,9 @@ pub unsafe extern "C" fn BNWARPContainerFetchFunctions( .collect(); let guids = unsafe { std::slice::from_raw_parts(guids, count) }; + let constraints = unsafe { std::slice::from_raw_parts(constraints, constraints_count) }; - if let Err(e) = container.fetch_functions(&target, &source_tags, guids) { + if let Err(e) = container.fetch_functions(&target, &source_tags, guids, constraints) { tracing::error!("Failed to fetch functions: {}", e); } } diff --git a/plugins/warp/src/plugin/settings.rs b/plugins/warp/src/plugin/settings.rs index 6469be0f18..896bee7f8a 100644 --- a/plugins/warp/src/plugin/settings.rs +++ b/plugins/warp/src/plugin/settings.rs @@ -40,7 +40,7 @@ pub struct PluginSettings { impl PluginSettings { pub const ALLOWED_SOURCE_TAGS_DEFAULT: [&'static str; 2] = ["official", "trusted"]; pub const ALLOWED_SOURCE_TAGS_SETTING: &'static str = "warp.fetcher.allowedSourceTags"; - pub const FETCH_BATCH_SIZE_DEFAULT: usize = 100; + pub const FETCH_BATCH_SIZE_DEFAULT: usize = 10000; pub const FETCH_BATCH_SIZE_SETTING: &'static str = "warp.fetcher.fetchBatchSize"; pub const LOAD_BUNDLED_FILES_DEFAULT: bool = true; pub const LOAD_BUNDLED_FILES_SETTING: &'static str = "warp.container.loadBundledFiles"; @@ -81,8 +81,8 @@ impl PluginSettings { let fetch_size_props = json!({ "title" : "Fetch Batch Limit", "type" : "number", - "minValue" : 1, - "maxValue" : 1000, + "minValue" : 100, + "maxValue" : 20000, "default" : Self::FETCH_BATCH_SIZE_DEFAULT, "description" : "The maximum number of functions to fetch in a single batch. This is used to limit the amount of functions to fetch at once, lowering this value will make the fetch process more comprehensive at the cost of more network requests.", "ignore" : [], diff --git a/plugins/warp/src/plugin/workflow.rs b/plugins/warp/src/plugin/workflow.rs index 1f4ef1016f..52c8e249bc 100644 --- a/plugins/warp/src/plugin/workflow.rs +++ b/plugins/warp/src/plugin/workflow.rs @@ -24,6 +24,7 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::time::Instant; use warp::r#type::class::function::{Location, RegisterLocation, StackLocation}; +use warp::signature::constraint::ConstraintGUID; use warp::signature::function::{Function, FunctionGUID}; use warp::target::Target; @@ -171,7 +172,7 @@ pub fn run_matcher(view: &BinaryView) { .maximum_possible_functions .is_some_and(|max| max < matched_functions.len() as u64) { - tracing::warn!( + tracing::debug!( "Skipping {}, too many possible functions: {}", guid, matched_functions.len() @@ -270,6 +271,20 @@ pub fn run_fetcher(view: &BinaryView) { let mut query_opts = QueryOptions::new_with_view(view); let plugin_settings = PluginSettings::from_settings(&view_settings, &mut query_opts); + let is_ignored_func = |f: &BNFunction| !f.function_tags(None, Some(IGNORE_TAG_NAME)).is_empty(); + + let constraints: Vec = view + .functions() + .iter() + // Skip functions that have the ignored tag! Otherwise, we will store their constraints. + .filter(|f| !is_ignored_func(f)) + .filter_map(|f| { + let function = try_cached_function_match(&f)?; + Some(function.constraints.into_iter().map(|c| c.guid)) + }) + .flatten() + .collect(); + let Some(function_set) = FunctionSet::from_view(view) else { background_task.finish(); return; @@ -285,8 +300,12 @@ pub fn run_fetcher(view: &BinaryView) { if background_task.is_cancelled() { break; } - let _ = - container.fetch_functions(target, &plugin_settings.allowed_source_tags, batch); + let _ = container.fetch_functions( + target, + &plugin_settings.allowed_source_tags, + batch, + &constraints, + ); } } }); diff --git a/plugins/warp/ui/shared/fetchdialog.cpp b/plugins/warp/ui/shared/fetchdialog.cpp index 328f00c253..44d7c2375d 100644 --- a/plugins/warp/ui/shared/fetchdialog.cpp +++ b/plugins/warp/ui/shared/fetchdialog.cpp @@ -67,12 +67,6 @@ WarpFetchDialog::WarpFetchDialog(BinaryViewRef bv, std::shared_ptr for (const auto& t : GetAllowedTagsFromView(m_bv)) AddListItem(m_tagsList, QString::fromStdString(t)); - // Batch size and matcher checkbox - m_batchSize = new QSpinBox(this); - m_batchSize->setRange(10, 1000); - m_batchSize->setValue(GetBatchSizeFromView(m_bv)); - m_batchSize->setToolTip("Number of functions to fetch in each batch"); - m_rerunMatcher = new QCheckBox("Re-run matcher after fetch", this); m_rerunMatcher->setChecked(true); @@ -83,7 +77,6 @@ WarpFetchDialog::WarpFetchDialog(BinaryViewRef bv, std::shared_ptr form->addRow(new QLabel("Container: "), m_containerCombo); form->addRow(new QLabel("Allowed Tags: "), tagWrapper); - form->addRow(new QLabel("Batch Size: "), m_batchSize); form->addRow(m_rerunMatcher); form->addRow(m_clearProcessed); @@ -144,7 +137,6 @@ void WarpFetchDialog::onAccept() if (idx > 0) // 0 == All Containers containerIndex = static_cast(idx - 1); - const auto batch = static_cast(m_batchSize->value()); const bool rerun = m_rerunMatcher->isChecked(); const auto tags = collectTags(); @@ -155,7 +147,7 @@ void WarpFetchDialog::onAccept() m_fetchProcessor->ClearProcessed(); // Execute the network fetch in batches - runBatchedFetch(containerIndex, tags, batch, rerun); + runBatchedFetch(containerIndex, tags, rerun); accept(); } @@ -169,7 +161,7 @@ void WarpFetchDialog::onReject() } void WarpFetchDialog::runBatchedFetch(const std::optional& containerIndex, - const std::vector& allowedTags, size_t batchSize, bool rerunMatcher) + const std::vector& allowedTags, bool rerunMatcher) { if (!m_bv) return; @@ -177,42 +169,34 @@ void WarpFetchDialog::runBatchedFetch(const std::optional& containerInde std::vector> funcs = m_bv->GetAnalysisFunctionList(); if (funcs.empty()) return; - const size_t totalFuncs = funcs.size(); - const size_t totalBatches = (totalFuncs + batchSize - 1) / batchSize; // Create a background task to show progress in the UI Ref task = - new BackgroundTask("Fetching WARP functions (0 / " + std::to_string(totalBatches) + ")", false); + new BackgroundTask("Fetching WARP functions (0 / " + std::to_string(funcs.size()) + ")", true); auto fetcher = m_fetchProcessor; auto bv = m_bv; // TODO: Too many captures in this thing lol. WorkerInteractiveEnqueue( - [fetcher, bv, funcs = std::move(funcs), batchSize, rerunMatcher, task, allowedTags]() mutable { + [fetcher, bv, funcs = std::move(funcs), rerunMatcher, task, allowedTags]() mutable { + const auto batchSize = GetBatchSizeFromView(bv); size_t processed = 0; - size_t batchIndex = 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); - - ++batchIndex; processed += thisBatchCount; - - task->SetProgressText("Fetching WARP functions (" + std::to_string(batchIndex) + " / " - + std::to_string((funcs.size() + batchSize - 1) / batchSize) + ")"); + task->SetProgressText("Fetching WARP functions (" + std::to_string(processed) + " / " + std::to_string(funcs.size()) + ")"); } task->Finish(); - // TODO: Print how long it took? - Logger("WARP Fetcher").LogInfo("Finished fetching WARP functions..."); + Logger("WARP Fetcher").LogInfo("Finished fetching WARP functions in %d seconds...", task->GetRuntimeSeconds()); if (rerunMatcher && bv) Warp::RunMatcher(*bv); diff --git a/plugins/warp/ui/shared/fetchdialog.h b/plugins/warp/ui/shared/fetchdialog.h index c642d31e40..72b8c57e42 100644 --- a/plugins/warp/ui/shared/fetchdialog.h +++ b/plugins/warp/ui/shared/fetchdialog.h @@ -22,7 +22,6 @@ class WarpFetchDialog : public QDialog QPushButton* m_removeTagBtn; QPushButton* m_resetTagBtn; - QSpinBox* m_batchSize; QCheckBox* m_rerunMatcher; QCheckBox* m_clearProcessed; @@ -51,7 +50,7 @@ private slots: std::vector collectTags() const; void runBatchedFetch(const std::optional& containerIndex, const std::vector& allowedTags, - size_t batchSize, bool rerunMatcher); + bool rerunMatcher); }; void RegisterWarpFetchFunctionsCommand(); diff --git a/plugins/warp/ui/shared/fetcher.cpp b/plugins/warp/ui/shared/fetcher.cpp index 51ab018c65..2eab910a9f 100644 --- a/plugins/warp/ui/shared/fetcher.cpp +++ b/plugins/warp/ui/shared/fetcher.cpp @@ -63,22 +63,40 @@ void WarpFetcher::FetchPendingFunctions(const std::vector& allo // Because we must fetch for a single target we map the function guids to the associated platform to perform fetches // for each. - std::map> platformMappedGuids; + std::map> platformMappedGuidSet; + std::map> platformMappedConstraintSet; for (const auto& func : requests) { - const auto guid = Warp::GetAnalysisFunctionGUID(*func); - if (!guid.has_value()) + const auto warpFunc = Warp::Function::Get(*func); + if (!warpFunc) continue; auto platform = func->GetPlatform(); - platformMappedGuids[platform].push_back(guid.value()); + 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. + const auto constraints = warpFunc->GetConstraints(); + std::vector constraintGuids; + constraintGuids.reserve(constraints.size()); + for (const auto& constraint : constraints) + constraintGuids.push_back(constraint.guid); + platformMappedConstraintSet[platform].insert(constraintGuids.begin(), constraintGuids.end()); } + std::map> platformMappedGuids; + for (const auto& [platform, guids] : platformMappedGuidSet) + platformMappedGuids[platform] = std::vector(guids.begin(), guids.end()); + + // We keep them in the set above so we don't duplicate a bunch for functions with the same set of constraint guids. + std::map> platformMappedConstraints; + for (const auto& [platform, guids] : platformMappedConstraintSet) + platformMappedConstraints[platform] = std::vector(guids.begin(), guids.end()); + for (const auto& [platform, guids] : platformMappedGuids) { m_logger->LogDebugF("Fetching {} functions for platform {}", guids.size(), platform->GetName()); auto target = Warp::Target::FromPlatform(*platform); for (const auto& container : Warp::Container::All()) - container->FetchFunctions(*target, guids, allowedTags); + container->FetchFunctions(*target, guids, allowedTags, platformMappedConstraints[platform]); std::lock_guard lock(m_requestMutex); for (const auto& guid : guids) diff --git a/plugins/warp/ui/shared/misc.h b/plugins/warp/ui/shared/misc.h index 97adcab00e..92e27fd88a 100644 --- a/plugins/warp/ui/shared/misc.h +++ b/plugins/warp/ui/shared/misc.h @@ -138,10 +138,10 @@ inline void SetTagsToView(const BinaryViewRef& view, const std::vectorSet(ALLOWED_TAGS_SETTING, tags, view); } -inline int GetBatchSizeFromView(const BinaryViewRef& view) +inline size_t GetBatchSizeFromView(const BinaryViewRef& view) { auto settings = BinaryNinja::Settings::Instance(); if (!settings->Contains(BATCH_SIZE_SETTING)) - return 100; + return 10000; return settings->Get(BATCH_SIZE_SETTING, view); } \ No newline at end of file From 46631bdd3bc4c656295f211c4ae6976798352984 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Tue, 3 Mar 2026 09:15:02 -0800 Subject: [PATCH 03/19] [WARP] Update the selected sidebar function when refocusing Should fix issue where opening the sidebar for the first time will not show anything in the selected function until the user clicks in the view frame --- plugins/warp/ui/plugin.cpp | 7 ++++++- plugins/warp/ui/plugin.h | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/warp/ui/plugin.cpp b/plugins/warp/ui/plugin.cpp index 66c80ba394..9421cc02a4 100644 --- a/plugins/warp/ui/plugin.cpp +++ b/plugins/warp/ui/plugin.cpp @@ -227,7 +227,6 @@ void WarpSidebarWidget::notifyViewChanged(ViewFrame* view) if (view == m_currentFrame) return; m_currentFrame = view; - // TODO: We need to set some stuff here prolly. } void WarpSidebarWidget::notifyViewLocationChanged(View* view, const ViewLocation& location) @@ -241,6 +240,12 @@ void WarpSidebarWidget::notifyViewLocationChanged(View* view, const ViewLocation m_currentFunctionWidget->SetCurrentFunction(function); } +void WarpSidebarWidget::focus() +{ + m_currentFunctionWidget->SetCurrentFunction(m_currentFrame->getViewLocation().getFunction()); + SidebarWidget::focus(); +} + WarpSidebarWidgetType::WarpSidebarWidgetType() : SidebarWidgetType(QImage(":/icons/images/warp.png"), "WARP") {} diff --git a/plugins/warp/ui/plugin.h b/plugins/warp/ui/plugin.h index e70b7afafc..0a4c630c04 100644 --- a/plugins/warp/ui/plugin.h +++ b/plugins/warp/ui/plugin.h @@ -39,6 +39,8 @@ class WarpSidebarWidget : public SidebarWidget void notifyViewChanged(ViewFrame*) override; void notifyViewLocationChanged(View*, const ViewLocation&) override; + + void focus() override; }; class WarpSidebarWidgetType : public SidebarWidgetType From 23d243fd42d9e6a2d81b1eb5a27a1a55f77b0561 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Tue, 3 Mar 2026 09:42:14 -0800 Subject: [PATCH 04/19] [WARP] Add a spinner to the possible matches widget while fetching from network A little extra pizzaz --- plugins/warp/ui/matches.cpp | 25 ++++++++++++++++++++++--- plugins/warp/ui/matches.h | 4 +++- plugins/warp/ui/plugin.cpp | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/plugins/warp/ui/matches.cpp b/plugins/warp/ui/matches.cpp index 289c110244..cede06ab8b 100644 --- a/plugins/warp/ui/matches.cpp +++ b/plugins/warp/ui/matches.cpp @@ -11,7 +11,7 @@ #include "warp.h" #include "shared/misc.h" -WarpCurrentFunctionWidget::WarpCurrentFunctionWidget() +WarpCurrentFunctionWidget::WarpCurrentFunctionWidget(QWidget* parent) : QWidget(parent) { // We must explicitly support no current function. m_current = nullptr; @@ -31,10 +31,27 @@ WarpCurrentFunctionWidget::WarpCurrentFunctionWidget() m_splitter = new QSplitter(Qt::Vertical); m_splitter->setContentsMargins(0, 0, 0, 0); + // Wrap the table and the spinner so that we can overlay the spinner on the table. + QWidget* tableWrapper = new QWidget(m_splitter); + QGridLayout* wrapperLayout = new QGridLayout(tableWrapper); + wrapperLayout->setContentsMargins(0, 0, 0, 0); + // Add a widget to display the matches. - m_tableWidget = new WarpFunctionTableWidget(this); + m_tableWidget = new WarpFunctionTableWidget(tableWrapper); m_tableWidget->setContentsMargins(0, 0, 0, 0); - m_splitter->addWidget(m_tableWidget); + + // Spinner for when we are fetching functions over the network. + m_spinner = new QProgressBar(tableWrapper); + m_spinner->setRange(0, 0); + m_spinner->setTextVisible(false); + m_spinner->setFixedHeight(6); + m_spinner->hide(); + + // The table has no alignment, so it expands to fill the entire cell. + wrapperLayout->addWidget(m_tableWidget, 0, 0); + wrapperLayout->addWidget(m_spinner, 0, 0, Qt::AlignBottom); + + m_splitter->addWidget(tableWrapper); // Add a widget to display the info about the selected function match. m_infoWidget = new WarpFunctionInfoWidget(this); @@ -145,10 +162,12 @@ void WarpCurrentFunctionWidget::SetCurrentFunction(FunctionRef current) if (!m_fetcher->m_requestInProgress.exchange(true)) { BinaryNinja::WorkerPriorityEnqueue([this]() { + QMetaObject::invokeMethod(this, [this] { m_spinner->show(); }, Qt::QueuedConnection); BinaryNinja::Ref bgTask = new BinaryNinja::BackgroundTask("Fetching WARP Functions...", true); const auto allowedTags = GetAllowedTagsFromView(m_current->GetView()); m_fetcher->FetchPendingFunctions(allowedTags); bgTask->Finish(); + QMetaObject::invokeMethod(this, [this] { m_spinner->hide(); }, Qt::QueuedConnection); }); } } diff --git a/plugins/warp/ui/matches.h b/plugins/warp/ui/matches.h index 6a2061efec..1599dd541b 100644 --- a/plugins/warp/ui/matches.h +++ b/plugins/warp/ui/matches.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "filter.h" #include "render.h" @@ -13,6 +14,7 @@ class WarpCurrentFunctionWidget : public QWidget FunctionRef m_current; QSplitter* m_splitter; + QProgressBar* m_spinner; WarpFunctionTableWidget* m_tableWidget; WarpFunctionInfoWidget* m_infoWidget; @@ -22,7 +24,7 @@ class WarpCurrentFunctionWidget : public QWidget std::shared_ptr m_fetcher; public: - explicit WarpCurrentFunctionWidget(); + explicit WarpCurrentFunctionWidget(QWidget* parent = nullptr); ~WarpCurrentFunctionWidget() override = default; diff --git a/plugins/warp/ui/plugin.cpp b/plugins/warp/ui/plugin.cpp index 9421cc02a4..e99059b98a 100644 --- a/plugins/warp/ui/plugin.cpp +++ b/plugins/warp/ui/plugin.cpp @@ -136,7 +136,7 @@ WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP") m_headerWidget->setLayout(headerLayout); QFrame* currentFunctionFrame = new QFrame(this); - m_currentFunctionWidget = new WarpCurrentFunctionWidget(); + m_currentFunctionWidget = new WarpCurrentFunctionWidget(this); QVBoxLayout* currentFunctionLayout = new QVBoxLayout(); currentFunctionLayout->setContentsMargins(0, 0, 0, 0); currentFunctionLayout->setSpacing(0); From 3528d544ce5c528ba8f35493578e98d9388a524f Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Wed, 4 Mar 2026 11:12:57 -0800 Subject: [PATCH 05/19] [WARP] Fix relocatable region selection failing to fallback to section collection Previously we only selected relocatable regions from the list of sections, now that we use the segment list we need a way to fallback to the section list of the segment information is problematic (e.g. based at zero), that fallback has not been triggering as there is a segment for the synthetic sections. Now when a user opens a firmware with only a single zero based segment it should fallback to the sections _and_ alert the user that they should fill out the section map (since that job is left to the user) --- plugins/warp/src/lib.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/warp/src/lib.rs b/plugins/warp/src/lib.rs index 1e622d5858..6889c79dfa 100644 --- a/plugins/warp/src/lib.rs +++ b/plugins/warp/src/lib.rs @@ -25,6 +25,8 @@ use warp::signature::basic_block::BasicBlockGUID; use warp::signature::function::{Function, FunctionGUID}; use warp::signature::variable::FunctionVariable; +use binaryninja::section::Semantics; +use binaryninja::segment::Segment; /// Re-export the warp crate that is used, this is useful for consumers of this crate. pub use warp; @@ -418,6 +420,15 @@ pub fn is_address_relocatable(relocatable_regions: &[Range], address: u64) /// Currently, segments are used by default, however, if the only segment is based at 0, then we fall /// back to using sections. pub fn relocatable_regions(view: &BinaryView) -> Vec> { + // We need to filter out the segment for the synthetic builtins, otherwise we can't get to + // the path where we look at sections for relocatable regions, this segment is always created + // for now we just filter, but in the future I would like to have something less jank. + let is_synthetic_segment = |segment: &Segment| { + view.sections_at(segment.address_range().start) + .iter() + .any(|sec| sec.auto_defined() && sec.semantics() == Semantics::External) + }; + // NOTE: We used to use sections because the image base for some object files would start // at zero, masking non-relocatable instructions, since then we have started adjusting the // image base to 0x10000 or higher so we can use segments directly, which improves the accuracy @@ -426,13 +437,14 @@ pub fn relocatable_regions(view: &BinaryView) -> Vec> { .segments() .iter() .filter(|s| s.address_range().start != 0) + .filter(|s| !is_synthetic_segment(s)) .map(|s| s.address_range()) .collect::>(); if ranges.is_empty() { // Realistically only happens if the only defined segment was based at 0, in which case - // we hope the user has set up correct sections. If not we are going to be masking off too many - // or too little instructions. + // we hope the user has set up correct sections. If not, we are going to be masking off too + // many or too little instructions. ranges = view .sections() .iter() From 50945fc974b78fb9566759ba9f777776ecf7896a Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 13 Mar 2026 12:19:41 -0700 Subject: [PATCH 06/19] [Rust] Misc docs --- rust/src/background_task.rs | 3 +++ rust/src/database.rs | 8 +++++--- rust/src/file_metadata.rs | 3 ++- rust/src/platform.rs | 4 +++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/rust/src/background_task.rs b/rust/src/background_task.rs index 79aec6832a..1d902c3ad4 100644 --- a/rust/src/background_task.rs +++ b/rust/src/background_task.rs @@ -70,6 +70,9 @@ impl BackgroundTask { Self { handle } } + /// Begin the [`BackgroundTask`], you must manually finish the task by calling [`BackgroundTask::finish`]. + /// + /// If you wish to automatically finish the task when leaving the scope, use [`BackgroundTask::enter`]. pub fn new(initial_text: &str, can_cancel: bool) -> Ref { let text = initial_text.to_cstr(); let handle = unsafe { BNBeginBackgroundTask(text.as_ptr(), can_cancel) }; diff --git a/rust/src/database.rs b/rust/src/database.rs index 4f2b3f6a8e..f0627306e6 100644 --- a/rust/src/database.rs +++ b/rust/src/database.rs @@ -30,7 +30,7 @@ impl Database { Ref::new(Self { handle }) } - /// Get a snapshot by its id, or None if no snapshot with that id exists + /// Get a [`Snapshot`] by its `id`, or `None` if no snapshot with that `id` exists. pub fn snapshot_by_id(&self, id: SnapshotId) -> Option> { let result = unsafe { BNGetDatabaseSnapshot(self.handle.as_ptr(), id.0) }; NonNull::new(result).map(|handle| unsafe { Snapshot::ref_from_raw(handle) }) @@ -113,8 +113,9 @@ impl Database { SnapshotId(new_id) } - /// Trim a snapshot's contents in the database by id, but leave the parent/child - /// hierarchy intact. Future references to this snapshot will return False for has_contents + /// Trim a snapshot's contents in the database but leave the parent/child hierarchy intact. + /// + /// NOTE: Future references to this snapshot will return `false` for [`Database::snapshot_has_data`] pub fn trim_snapshot(&self, id: SnapshotId) -> Result<(), ()> { if unsafe { BNTrimDatabaseSnapshot(self.handle.as_ptr(), id.0) } { Ok(()) @@ -193,6 +194,7 @@ impl Database { unsafe { KeyValueStore::ref_from_raw(NonNull::new(result).unwrap()) } } + /// Closes then reopens the database. pub fn reload_connection(&self) { unsafe { BNDatabaseReloadConnection(self.handle.as_ptr()) } } diff --git a/rust/src/file_metadata.rs b/rust/src/file_metadata.rs index 5862647f4c..ce320dc583 100644 --- a/rust/src/file_metadata.rs +++ b/rust/src/file_metadata.rs @@ -583,7 +583,8 @@ impl FileMetadata { /// Get the database attached to this file. /// - /// Only available if this file is a database, or has called [`FileMetadata::create_database`]. + /// Only available if this file is a database, or [`FileMetadata::create_database`] has previously + /// been called on this file. pub fn database(&self) -> Option> { let result = unsafe { BNGetFileMetadataDatabase(self.handle) }; NonNull::new(result).map(|handle| unsafe { Database::ref_from_raw(handle) }) diff --git a/rust/src/platform.rs b/rust/src/platform.rs index 46129b81a5..bf135fae71 100644 --- a/rust/src/platform.rs +++ b/rust/src/platform.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Contains all information related to the execution environment of the binary, mainly the calling conventions used +//! A [`Platform`] models the information related to the execution environment of the binary. use crate::{ architecture::{Architecture, CoreArchitecture}, @@ -29,6 +29,8 @@ use std::fmt::Debug; use std::ptr::NonNull; use std::{borrow::Borrow, ffi, ptr}; +/// A platform describes the target [`CoreArchitecture`] and platform-specific information such as +/// the calling conventions and generic types (think `HRESULT` on Windows). #[derive(PartialEq, Eq, Hash)] pub struct Platform { pub(crate) handle: *mut BNPlatform, From c23e98e6b1fba347134f0be0960157d8396b58a1 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Sat, 21 Mar 2026 21:12:43 -0700 Subject: [PATCH 07/19] [Rust] Impl `PartialEq`, `Eq` and `Hash` for `ProjectFile` --- rust/src/project/file.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/rust/src/project/file.rs b/rust/src/project/file.rs index c3114cd352..22bf9b4b93 100644 --- a/rust/src/project/file.rs +++ b/rust/src/project/file.rs @@ -11,6 +11,7 @@ use binaryninjacore_sys::{ BNProjectFileSetFolder, BNProjectFileSetName, }; use std::fmt::Debug; +use std::hash::Hash; use std::path::{Path, PathBuf}; use std::ptr::{null_mut, NonNull}; use std::time::SystemTime; @@ -155,6 +156,20 @@ impl Debug for ProjectFile { unsafe impl Send for ProjectFile {} unsafe impl Sync for ProjectFile {} +impl PartialEq for ProjectFile { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for ProjectFile {} + +impl Hash for ProjectFile { + fn hash(&self, state: &mut H) { + self.id().hash(state); + } +} + impl ToOwned for ProjectFile { type Owned = Ref; From 5a393e8f19bf61c972e3338cef32736d3a52ca6a Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Sat, 21 Mar 2026 21:12:50 -0700 Subject: [PATCH 08/19] [Rust] Impl `PartialEq`, `Eq` and `Hash` for `Project` --- rust/src/project.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/rust/src/project.rs b/rust/src/project.rs index f2743e80ad..fc750bcb1b 100644 --- a/rust/src/project.rs +++ b/rust/src/project.rs @@ -3,7 +3,8 @@ pub mod folder; use std::ffi::c_void; use std::fmt::Debug; -use std::path::Path; +use std::hash::Hash; +use std::path::{Path, PathBuf}; use std::ptr::{null_mut, NonNull}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -636,6 +637,20 @@ impl Debug for Project { } } +impl PartialEq for Project { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl Eq for Project {} + +impl Hash for Project { + fn hash(&self, state: &mut H) { + self.id().hash(state); + } +} + impl ToOwned for Project { type Owned = Ref; From d76ca99e369ed7384bb34192caa25f547b964479 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 13 Mar 2026 12:20:27 -0700 Subject: [PATCH 09/19] [Rust] Move `ObjectDestructor` to own module and add some extra documentation --- plugins/warp/src/cache.rs | 10 +- plugins/warp/src/plugin/debug.rs | 2 +- .../src/metadata/global_state.rs | 7 +- rust/src/lib.rs | 51 +--------- rust/src/object_destructor.rs | 93 +++++++++++++++++++ 5 files changed, 102 insertions(+), 61 deletions(-) create mode 100644 rust/src/object_destructor.rs diff --git a/plugins/warp/src/cache.rs b/plugins/warp/src/cache.rs index 3df2dbf8ed..d9a5a8d1a0 100644 --- a/plugins/warp/src/cache.rs +++ b/plugins/warp/src/cache.rs @@ -9,18 +9,14 @@ pub use type_reference::*; use binaryninja::binary_view::{BinaryView, BinaryViewExt}; use binaryninja::function::Function as BNFunction; +use binaryninja::object_destructor::{register_object_destructor, ObjectDestructor}; use binaryninja::rc::Guard; use binaryninja::rc::Ref as BNRef; -use binaryninja::ObjectDestructor; use std::hash::{DefaultHasher, Hash, Hasher}; pub fn register_cache_destructor() { - pub static mut CACHE_DESTRUCTOR: CacheDestructor = CacheDestructor; - #[allow(static_mut_refs)] - // SAFETY: This can be done as the backing data is an opaque ZST. - unsafe { - CACHE_DESTRUCTOR.register() - }; + let destructor = register_object_destructor(CacheDestructor); + std::mem::forget(destructor); } /// A unique view ID, used for caching. diff --git a/plugins/warp/src/plugin/debug.rs b/plugins/warp/src/plugin/debug.rs index d220d32954..4198ea303f 100644 --- a/plugins/warp/src/plugin/debug.rs +++ b/plugins/warp/src/plugin/debug.rs @@ -3,7 +3,7 @@ use crate::{build_function, cache}; use binaryninja::binary_view::BinaryView; use binaryninja::command::{Command, FunctionCommand}; use binaryninja::function::Function; -use binaryninja::ObjectDestructor; +use binaryninja::object_destructor::ObjectDestructor; pub struct DebugFunction; diff --git a/plugins/workflow_objc/src/metadata/global_state.rs b/plugins/workflow_objc/src/metadata/global_state.rs index db29af4981..6c128e22b5 100644 --- a/plugins/workflow_objc/src/metadata/global_state.rs +++ b/plugins/workflow_objc/src/metadata/global_state.rs @@ -1,11 +1,12 @@ use binaryninja::file_metadata::SessionId; +use binaryninja::object_destructor::register_object_destructor; use binaryninja::{ binary_view::{BinaryView, BinaryViewBase, BinaryViewExt}, file_metadata::FileMetadata, metadata::Metadata, + object_destructor::ObjectDestructor, rc::Ref, settings::{QueryOptions, Settings}, - ObjectDestructor, }; use dashmap::DashMap; use once_cell::sync::Lazy; @@ -66,8 +67,8 @@ pub struct GlobalState; impl GlobalState { pub fn register_cleanup() { - let observer = Box::leak(Box::new(ObjectLifetimeObserver)); - observer.register(); + let destructor = register_object_destructor(ObjectLifetimeObserver); + std::mem::forget(destructor); } fn id(bv: &BinaryView) -> SessionId { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 254dd2f809..5f23a9f6aa 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -65,6 +65,7 @@ pub mod low_level_il; pub mod main_thread; pub mod medium_level_il; pub mod metadata; +pub mod object_destructor; pub mod platform; pub mod progress; pub mod project; @@ -90,8 +91,6 @@ pub mod websocket; pub mod worker_thread; pub mod workflow; -use crate::file_metadata::FileMetadata; -use crate::function::Function; use crate::progress::{NoProgressCallback, ProgressCallback}; use crate::string::raw_to_string; use binary_view::BinaryView; @@ -428,54 +427,6 @@ pub fn memory_info() -> HashMap { usage } -/// The trait required for receiving core object destruction callbacks. -pub trait ObjectDestructor: 'static + Sync + Sized { - fn destruct_view(&self, _view: &BinaryView) {} - fn destruct_file_metadata(&self, _metadata: &FileMetadata) {} - fn destruct_function(&self, _func: &Function) {} - - unsafe extern "C" fn cb_destruct_binary_view(ctxt: *mut c_void, view: *mut BNBinaryView) { - ffi_wrap!("ObjectDestructor::destruct_view", { - let view_type = &*(ctxt as *mut Self); - let view = BinaryView { handle: view }; - view_type.destruct_view(&view); - }) - } - - unsafe extern "C" fn cb_destruct_file_metadata(ctxt: *mut c_void, file: *mut BNFileMetadata) { - ffi_wrap!("ObjectDestructor::destruct_file_metadata", { - let view_type = &*(ctxt as *mut Self); - let file = FileMetadata::from_raw(file); - view_type.destruct_file_metadata(&file); - }) - } - - unsafe extern "C" fn cb_destruct_function(ctxt: *mut c_void, func: *mut BNFunction) { - ffi_wrap!("ObjectDestructor::destruct_function", { - let view_type = &*(ctxt as *mut Self); - let func = Function { handle: func }; - view_type.destruct_function(&func); - }) - } - - unsafe fn as_callbacks(&'static mut self) -> BNObjectDestructionCallbacks { - BNObjectDestructionCallbacks { - context: std::mem::transmute(&self), - destructBinaryView: Some(Self::cb_destruct_binary_view), - destructFileMetadata: Some(Self::cb_destruct_file_metadata), - destructFunction: Some(Self::cb_destruct_function), - } - } - - fn register(&'static mut self) { - unsafe { BNRegisterObjectDestructionCallbacks(&mut self.as_callbacks()) }; - } - - fn unregister(&'static mut self) { - unsafe { BNUnregisterObjectDestructionCallbacks(&mut self.as_callbacks()) }; - } -} - pub fn version() -> String { unsafe { BnString::into_string(BNGetVersionString()) } } diff --git a/rust/src/object_destructor.rs b/rust/src/object_destructor.rs new file mode 100644 index 0000000000..aff5e4f0d0 --- /dev/null +++ b/rust/src/object_destructor.rs @@ -0,0 +1,93 @@ +//! Register callbacks for when core objects like [`BinaryView`]s or [`Function`]s are destroyed. + +use crate::binary_view::BinaryView; +use crate::file_metadata::FileMetadata; +use crate::function::Function; +use binaryninjacore_sys::*; +use std::ffi::c_void; + +/// Registers a destructor which will be called when certain core objects are about to be destroyed. +/// +/// Returns a handle to the registered destructor. The destructor will be unregistered when the handle is dropped. +/// +/// To keep the destructor alive forever, move the [`ObjectDestructorHandle`] into [`std::mem::ManuallyDrop`]. +#[must_use = "The destructor will be unregistered when the handle is dropped"] +pub fn register_object_destructor<'a, D: ObjectDestructor>( + destructor: D, +) -> ObjectDestructorHandle<'a, D> { + let destructor = Box::leak(Box::new(destructor)); + let callbacks = BNObjectDestructionCallbacks { + context: destructor as *mut _ as *mut c_void, + destructBinaryView: Some(cb_destruct_binary_view::), + destructFileMetadata: Some(cb_destruct_file_metadata::), + destructFunction: Some(cb_destruct_function::), + }; + let mut handle = ObjectDestructorHandle { + callbacks, + _life: std::marker::PhantomData, + }; + unsafe { BNRegisterObjectDestructionCallbacks(&mut handle.callbacks) }; + handle +} + +/// The handle for the [`ObjectDestructor`]. +/// +/// Once this handle is dropped, the destructor will be unregistered and the associated resources will be cleaned up. +pub struct ObjectDestructorHandle<'a, D: ObjectDestructor> { + callbacks: BNObjectDestructionCallbacks, + _life: std::marker::PhantomData<&'a D>, +} + +impl Drop for ObjectDestructorHandle<'_, D> { + fn drop(&mut self) { + unsafe { BNUnregisterObjectDestructionCallbacks(&mut self.callbacks) }; + let _ = unsafe { Box::from_raw(self.callbacks.context as *mut D) }; + } +} + +/// The trait required for receiving core object destruction callbacks. +/// +/// This is useful for cleaning up resources which are associated with a given core object. +pub trait ObjectDestructor: 'static + Sync + Sized { + /// Called when a [`BinaryView`] is about to be destroyed. + fn destruct_view(&self, _view: &BinaryView) {} + + /// Called when a [`FileMetadata`] is about to be destroyed. + fn destruct_file_metadata(&self, _metadata: &FileMetadata) {} + + /// Called when a [`Function`] is about to be destroyed. + fn destruct_function(&self, _func: &Function) {} +} + +unsafe extern "C" fn cb_destruct_binary_view( + ctxt: *mut c_void, + view: *mut BNBinaryView, +) { + ffi_wrap!("ObjectDestructor::destruct_view", { + let destructor = &*(ctxt as *mut D); + let view = BinaryView { handle: view }; + destructor.destruct_view(&view); + }) +} + +unsafe extern "C" fn cb_destruct_file_metadata( + ctxt: *mut c_void, + file: *mut BNFileMetadata, +) { + ffi_wrap!("ObjectDestructor::destruct_file_metadata", { + let destructor = &*(ctxt as *mut D); + let file = FileMetadata::from_raw(file); + destructor.destruct_file_metadata(&file); + }) +} + +unsafe extern "C" fn cb_destruct_function( + ctxt: *mut c_void, + func: *mut BNFunction, +) { + ffi_wrap!("ObjectDestructor::destruct_function", { + let destructor = &*(ctxt as *mut D); + let func = Function { handle: func }; + destructor.destruct_function(&func); + }) +} From 508cf5137dbb3e00315fa1299f81e826e0ac012f Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 13 Mar 2026 12:21:22 -0700 Subject: [PATCH 10/19] [Rust] Impl `BinaryViewEventHandler` for `Fn(&BinaryView)` So you can pass a closure to the register function --- rust/src/binary_view.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rust/src/binary_view.rs b/rust/src/binary_view.rs index bd26dad3bb..b65367a2dd 100644 --- a/rust/src/binary_view.rs +++ b/rust/src/binary_view.rs @@ -2709,6 +2709,12 @@ pub trait BinaryViewEventHandler: 'static + Sync { fn on_event(&self, binary_view: &BinaryView); } +impl BinaryViewEventHandler for F { + fn on_event(&self, binary_view: &BinaryView) { + self(binary_view); + } +} + /// Registers an event listener for binary view events. /// /// # Example From 4ef2c394451a6bc7c0ded24bdf8b2fa65de82fc3 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 13 Mar 2026 12:22:27 -0700 Subject: [PATCH 11/19] [Rust] More appropriate impls for `PartialEq` and `Hash` for `FileMetadata` Utilize the unique `session_id` of the `FileMetadata` on comparisons and when hashing. --- rust/src/file_metadata.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rust/src/file_metadata.rs b/rust/src/file_metadata.rs index ce320dc583..b58daa5587 100644 --- a/rust/src/file_metadata.rs +++ b/rust/src/file_metadata.rs @@ -22,6 +22,7 @@ use binaryninjacore_sys::*; use binaryninjacore_sys::{BNCreateDatabaseWithProgress, BNOpenExistingDatabaseWithProgress}; use std::ffi::c_void; use std::fmt::{Debug, Display, Formatter}; +use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use crate::progress::{NoProgressCallback, ProgressCallback}; @@ -108,7 +109,6 @@ unsafe impl RefCountable for SaveSettings { /// **Important**: Because [`FileMetadata`] holds a strong reference to the [`BinaryView`]s and those /// views hold a strong reference to the file metadata, to end the cyclic reference a call to the /// [`FileMetadata::close`] is required. -#[derive(PartialEq, Eq, Hash)] pub struct FileMetadata { pub(crate) handle: *mut BNFileMetadata, } @@ -612,6 +612,20 @@ impl Display for FileMetadata { } } +impl PartialEq for FileMetadata { + fn eq(&self, other: &Self) -> bool { + self.session_id() == other.session_id() + } +} + +impl Eq for FileMetadata {} + +impl Hash for FileMetadata { + fn hash(&self, state: &mut H) { + self.session_id().hash(state); + } +} + unsafe impl Send for FileMetadata {} unsafe impl Sync for FileMetadata {} From dff68dfb2d6177401d8bed632267e6a2e260ebb9 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 13 Mar 2026 12:24:18 -0700 Subject: [PATCH 12/19] [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 --- Cargo.lock | 6 - plugins/warp/Cargo.toml | 8 - plugins/warp/api/python/generator.cpp | 5 + plugins/warp/api/python/warp.py | 176 ++++++- plugins/warp/api/python/warp_enums.py | 13 + plugins/warp/api/warp.cpp | 210 +++++++- plugins/warp/api/warp.h | 92 +++- plugins/warp/api/warpcore.h | 82 +++- plugins/warp/build.rs | 4 - plugins/warp/demo/Cargo.toml | 8 - plugins/warp/demo/build.rs | 4 - plugins/warp/examples/headless/src/main.rs | 11 +- plugins/warp/src/container.rs | 24 + plugins/warp/src/lib.rs | 1 - plugins/warp/src/plugin.rs | 77 +-- plugins/warp/src/plugin/commit.rs | 149 ------ plugins/warp/src/plugin/create.rs | 265 ---------- plugins/warp/src/plugin/debug.rs | 49 -- plugins/warp/src/plugin/ffi.rs | 3 + plugins/warp/src/plugin/ffi/container.rs | 61 +-- plugins/warp/src/plugin/ffi/file.rs | 137 +++++- plugins/warp/src/plugin/ffi/function.rs | 19 +- plugins/warp/src/plugin/ffi/processor.rs | 187 ++++++++ plugins/warp/src/plugin/ffi/ty.rs | 75 +++ plugins/warp/src/plugin/file.rs | 41 -- plugins/warp/src/plugin/function.rs | 102 +--- plugins/warp/src/plugin/project.rs | 286 ----------- plugins/warp/src/processor.rs | 345 ++++---------- plugins/warp/src/report.rs | 185 ------- plugins/warp/src/templates/file.html | 37 -- plugins/warp/src/templates/file.json | 21 - plugins/warp/src/templates/file.md | 15 - plugins/warp/tests/processor.rs | 6 +- plugins/warp/ui/CMakeLists.txt | 8 +- plugins/warp/ui/containers.cpp | 195 +------- plugins/warp/ui/containers.h | 94 +--- plugins/warp/ui/matched.cpp | 11 +- plugins/warp/ui/matches.cpp | 35 +- plugins/warp/ui/plugin.cpp | 77 ++- plugins/warp/ui/plugin.h | 2 +- plugins/warp/ui/shared/chunk.cpp | 159 ++++++ plugins/warp/ui/shared/chunk.h | 28 ++ plugins/warp/ui/shared/commitdialog.cpp | 141 ++++++ plugins/warp/ui/shared/commitdialog.h | 74 +++ plugins/warp/ui/shared/fetchdialog.cpp | 73 +-- plugins/warp/ui/shared/fetchdialog.h | 2 - plugins/warp/ui/shared/fetcher.cpp | 3 +- plugins/warp/ui/shared/fetcher.h | 1 + plugins/warp/ui/shared/file.cpp | 71 +++ plugins/warp/ui/shared/file.h | 25 + plugins/warp/ui/shared/function.cpp | 7 +- plugins/warp/ui/shared/function.h | 7 +- plugins/warp/ui/shared/misc.cpp | 74 ++- plugins/warp/ui/shared/misc.h | 22 +- plugins/warp/ui/shared/processordialog.cpp | 451 ++++++++++++++++++ plugins/warp/ui/shared/processordialog.h | 127 +++++ plugins/warp/ui/shared/search.cpp | 22 +- .../ui/shared/selectprojectfilesdialog.cpp | 141 ++++++ .../warp/ui/shared/selectprojectfilesdialog.h | 26 + plugins/warp/ui/shared/source.cpp | 204 ++++++++ plugins/warp/ui/shared/source.h | 116 +++++ rust/src/data_buffer.rs | 7 + rust/src/project/file.rs | 2 +- 63 files changed, 2896 insertions(+), 2013 deletions(-) delete mode 100644 plugins/warp/src/plugin/commit.rs delete mode 100644 plugins/warp/src/plugin/create.rs delete mode 100644 plugins/warp/src/plugin/debug.rs create mode 100644 plugins/warp/src/plugin/ffi/processor.rs create mode 100644 plugins/warp/src/plugin/ffi/ty.rs delete mode 100644 plugins/warp/src/plugin/file.rs delete mode 100644 plugins/warp/src/plugin/project.rs delete mode 100644 plugins/warp/src/report.rs delete mode 100644 plugins/warp/src/templates/file.html delete mode 100644 plugins/warp/src/templates/file.json delete mode 100644 plugins/warp/src/templates/file.md create mode 100644 plugins/warp/ui/shared/chunk.cpp create mode 100644 plugins/warp/ui/shared/chunk.h create mode 100644 plugins/warp/ui/shared/commitdialog.cpp create mode 100644 plugins/warp/ui/shared/commitdialog.h create mode 100644 plugins/warp/ui/shared/file.cpp create mode 100644 plugins/warp/ui/shared/file.h create mode 100644 plugins/warp/ui/shared/processordialog.cpp create mode 100644 plugins/warp/ui/shared/processordialog.h create mode 100644 plugins/warp/ui/shared/selectprojectfilesdialog.cpp create mode 100644 plugins/warp/ui/shared/selectprojectfilesdialog.h create mode 100644 plugins/warp/ui/shared/source.cpp create mode 100644 plugins/warp/ui/shared/source.h diff --git a/Cargo.lock b/Cargo.lock index ed5c3382c0..ed74c1416c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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 13516c50c2..553a0c8f86 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 0fd231fede..9e72c2ec9b 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 2ba1c68189..b10b2c0991 100644 --- a/plugins/warp/api/python/warp.py +++ b/plugins/warp/api/python/warp.py @@ -4,11 +4,11 @@ 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 @@ def __repr__(self): return f"" +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"" + + @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 @@ def get_symbol(self, function: Function) -> Symbol: 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 @@ def source(self) -> Source: 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 @@ def add_functions(self, target: WarpTarget, source: Source, functions: List[Func 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 @@ def get_functions_with_guid(self, target: WarpTarget, source: Source, guid: Func 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 @@ def search(self, query: WarpContainerSearchQuery) -> Optional[WarpContainerRespo 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"" + + @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"" + + @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 86be3a6ca4..d3f9f393db 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 debbbc00a7..8c93260276 100644 --- a/plugins/warp/api/warp.cpp +++ b/plugins/warp/api/warp.cpp @@ -34,6 +34,41 @@ Ref Target::FromPlatform(const BinaryNinja::Platform &platform) return new Target(result); } +Type::Type(BNWARPType *type) +{ + m_object = type; +} + +std::optional 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::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 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 offset) { this->guid = guid; @@ -83,17 +118,17 @@ BinaryNinja::Ref Function::GetSymbol(const BinaryNinja::Fun return new BinaryNinja::Symbol(symbol); } -BinaryNinja::Ref Function::GetType(const BinaryNinja::Function &function) const +Ref 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 Function::GetConstraints() const { - size_t count; + size_t count = 0; BNWARPConstraint *constraints = BNWARPFunctionGetConstraints(m_object, &count); std::vector result; result.reserve(count); @@ -105,7 +140,7 @@ std::vector Function::GetConstraints() const std::vector Function::GetComments() const { - size_t count; + size_t count = 0; BNWARPFunctionComment *comments = BNWARPFunctionGetComments(m_object, &count); std::vector result; result.reserve(count); @@ -184,12 +219,12 @@ Source ContainerSearchItem::GetSource() const return BNWARPContainerSearchItemGetSource(m_object); } -BinaryNinja::Ref ContainerSearchItem::GetType(const BinaryNinja::Ref &arch) const +Ref 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 > Container::All() { - size_t count; + size_t count = 0; BNWARPContainer **containers = BNWARPGetContainers(&count); std::vector > result; result.reserve(count); @@ -264,7 +299,7 @@ std::string Container::GetName() const std::vector Container::GetSources() const { - size_t count; + size_t count = 0; BNWARPSource *sources = BNWARPContainerGetSources(m_object, &count); std::vector 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 > &types) const +bool Container::AddTypes(const Source &source, const std::vector> &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 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 result; result.reserve(count); @@ -387,7 +421,7 @@ std::vector Container::GetSourcesWithFunctionGUID(const Target& target, std::vector Container::GetSourcesWithTypeGUID(const TypeGUID &guid) const { - size_t count; + size_t count = 0; BNWARPSource *sources = BNWARPContainerGetSourcesWithTypeGUID(m_object, guid.Raw(), &count); std::vector result; result.reserve(count); @@ -399,7 +433,7 @@ std::vector Container::GetSourcesWithTypeGUID(const TypeGUID &guid) cons std::vector > 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 > result; result.reserve(count); @@ -409,16 +443,15 @@ std::vector > Container::GetFunctionsWithGUID(const Target& target return result; } -BinaryNinja::Ref Container::GetTypeWithGUID(const BinaryNinja::Architecture &arch, - const Source &source, const TypeGUID &guid) const +Ref 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 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 result; result.reserve(count); @@ -436,6 +469,139 @@ std::optional Container::Search(const ContainerSearchQu return ContainerSearchResponse::FromAPIObject(response); } +Chunk::Chunk(BNWARPChunk *chunk) +{ + m_object = chunk; +} + +Ref Chunk::GetTarget() const +{ + BNWARPTarget *target = BNWARPChunkGetTarget(m_object); + if (!target) + return nullptr; + return new Target(target); +} + +std::vector> Chunk::GetFunctions() const +{ + size_t count = 0; + BNWARPFunction** functions = BNWARPChunkGetFunctions(m_object, &count); + std::vector> 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> Chunk::GetTypes() const +{ + size_t count = 0; + BNWARPType** types = BNWARPChunkGetTypes(m_object, &count); + std::vector> 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::FromPath(const std::string &path) +{ + BNWARPFile *result = BNWARPNewFileFromPath(path.c_str()); + if (!result) + return nullptr; + return new File(result); +} + +std::vector> File::GetChunks() const +{ + size_t count = 0; + BNWARPChunk **chunks = BNWARPFileGetChunks(m_object, &count); + std::vector> 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 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 ccb4da9706..42e8ac703c 100644 --- a/plugins/warp/api/warp.h +++ b/plugins/warp/api/warp.h @@ -276,9 +276,23 @@ namespace Warp { static Ref FromPlatform(const BinaryNinja::Platform &platform); }; + class Type : public WarpRefCountObject + { + public: + explicit Type(BNWARPType *type); + + [[nodiscard]] static Ref FromAnalysisType(const BinaryNinja::Type &type, uint8_t confidence); + + std::optional GetName() const; + uint8_t GetConfidence() const; + + [[nodiscard]] BinaryNinja::Ref GetAnalysisType(BinaryNinja::Architecture* arch = nullptr) const; + }; + struct Constraint { - ConstraintGUID guid; + ConstraintGUID guid {}; std::optional offset; Constraint(ConstraintGUID guid, std::optional offset); @@ -312,7 +326,7 @@ namespace Warp { BinaryNinja::Ref GetSymbol(const BinaryNinja::Function &function) const; - BinaryNinja::Ref GetType(const BinaryNinja::Function &function) const; + Ref GetType() const; std::vector GetConstraints() const; @@ -354,7 +368,7 @@ namespace Warp { Source GetSource() const; - BinaryNinja::Ref GetType(const BinaryNinja::Ref &arch) const; + Ref GetType() const; std::string GetName() const; @@ -363,7 +377,7 @@ namespace Warp { struct ContainerSearchResponse { - std::vector > items; + std::vector> items; size_t offset; size_t total; @@ -379,7 +393,7 @@ namespace Warp { explicit Container(BNWARPContainer *container); /// Retrieve all available containers. - static std::vector > All(); + static std::vector> All(); /// Add a new container with the given name. static Ref Add(const std::string &name); @@ -399,13 +413,12 @@ namespace Warp { std::optional SourcePath(const Source &source) const; bool AddFunctions(const Target &target, const Source &source, - const std::vector > &functions) const; + const std::vector> &functions) const; - bool AddTypes(const BinaryNinja::BinaryView &view, const Source &source, - const std::vector > &types) const; + bool AddTypes(const Source &source, const std::vector> &types) const; bool RemoveFunctions(const Target &target, const Source &source, - const std::vector > &functions) const; + const std::vector> &functions) const; bool RemoveTypes(const Source &source, const std::vector &guids) const; @@ -418,14 +431,71 @@ namespace Warp { std::vector > GetFunctionsWithGUID(const Target &target, const Source &source, const FunctionGUID &guid) const; - BinaryNinja::Ref GetTypeWithGUID(const BinaryNinja::Architecture &arch, const Source &source, - const TypeGUID &guid) const; + Ref GetTypeWithGUID(const Source &source, const TypeGUID &guid) const; std::vector GetTypeGUIDsWithName(const Source &source, const std::string &name) const; std::optional Search(const ContainerSearchQuery &query) const; }; + class Chunk : public WarpRefCountObject + { + friend class File; + + public: + explicit Chunk(BNWARPChunk *chunk); + + Ref GetTarget() const; + + [[nodiscard]] std::vector> GetFunctions() const; + [[nodiscard]] std::vector> GetTypes() const; + }; + + class File : public WarpRefCountObject + { + public: + explicit File(BNWARPFile *file); + + static Ref FromPath(const std::string &path); + + [[nodiscard]] std::vector> GetChunks() const; + [[nodiscard]] BinaryNinja::DataBuffer ToDataBuffer() const; + }; + + class ProcessorState + { + public: + std::vector analyzingFiles; + std::vector 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 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 21ef105ba5..c1762ae62c 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 ba8cbb656e..25546941e1 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 662b41319a..7c9babad68 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 d0912f907b..4940fe7638 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 29d48f6517..2040f84a22 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> = 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 4feed24cac..bb5d7fd463 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 6889c79dfa..e0aaeed346 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 d163205b95..8192102d3d 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 4e8a9fee8a..0000000000 --- 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 { - 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 5f5527a7a6..0000000000 --- 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 { - 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 { - 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, 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 4198ea303f..0000000000 --- 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 c1f5acb8d6..bb6fb48306 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>; +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 aa0c37d39a..09f15d9974 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 951b5eb28d..3abfe5aaaa 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>>, +} + +impl From> 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 138c0a7b85..2eff9e4ab3 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 0000000000..83b47f5a32 --- /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 0000000000..6a16fdab69 --- /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::(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 cd5fb626b9..0000000000 --- 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 36db7d55f7..908bd0db31 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::() 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> = 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 b6081c1f1a..0000000000 --- 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> { - 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) { - 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 12403ed211..32310d3c25 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> { - 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 { - 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, - #[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 { - 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, + Selected = 0, #[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 { - 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 { - 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 { - 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 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, state: Arc, @@ -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), + ProjectFile(Ref), + BinaryView(Ref), +} + /// 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, - file_data: FileDataKindField, + file_data: IncludedDataField, included_functions: IncludedFunctionsField, - compression_type: CompressionTypeField, + compression_type: CompressionType, processed_file_callback: Option, - /// Regex pattern used to filter out files. - file_filter: Option, - // 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, + /// The list of entries to process. + entries: HashSet, + /// 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, } 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, 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, 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, _> = 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, 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, 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, ProcessingError> { - let filter_project_file = |file: &Guard| { - 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 8ba9cc8bb4..0000000000 --- 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 { - 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 { - 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 { - 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 { - 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 { - 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, -} - -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, -} - -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, - pub note: Option, -} diff --git a/plugins/warp/src/templates/file.html b/plugins/warp/src/templates/file.html deleted file mode 100644 index 7ea465ef3f..0000000000 --- a/plugins/warp/src/templates/file.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - {{ title }} - - -

{{ title }}

- -{% for chunk in chunks %} -
-

{{ chunk.title }}

-

Target: {{ chunk.target }}

-

Total items: {{ chunk.total_item_count }}

- - - - - - - - - - - {% for item in chunk.item_view %} - - - - - - {% endfor %} - -
GUIDNameNote
{{ item.guid }}{{ item.name or 'N/A' }}{{ item.note or 'N/A' }}
-
-{% endfor %} - - \ 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 764d35cd8c..0000000000 --- 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 433cb8e926..0000000000 --- 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 1274977930..835b33ec55 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 328289aca9..d95ea6a7e1 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 f800f9776e..49415a52b7 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(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 container, QWidget* parent) : QWidget(parent) { m_container = std::move(container); @@ -100,95 +11,14 @@ WarpContainerWidget::WarpContainerWidget(Warp::Ref 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 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 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 b95c32dbcc..935b552e6b 100644 --- a/plugins/warp/ui/containers.h +++ b/plugins/warp/ui/containers.h @@ -1,8 +1,5 @@ #pragma once -#include -#include -#include #include #include #include @@ -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 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(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 m_rows; - Warp::Ref m_container; -}; +#include "shared/source.h" class WarpContainerWidget : public QWidget { @@ -112,8 +23,7 @@ class WarpContainerWidget : public QWidget 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 786b9d26e8..f313293842 100644 --- a/plugins/warp/ui/matched.cpp +++ b/plugins/warp/ui/matched.cpp @@ -1,10 +1,7 @@ #include "matched.h" - -#include - #include "theme.h" -const char* WARP_APPLY_ACTIVITY = "analysis.warp.apply"; +#include 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 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 cede06ab8b..918312fdd3 100644 --- a/plugins/warp/ui/matches.cpp +++ b/plugins/warp/ui/matches.cpp @@ -1,15 +1,13 @@ -#include -#include - #include "matches.h" +#include "theme.h" +#include "warp.h" +#include "shared/misc.h" #include #include #include - -#include "theme.h" -#include "warp.h" -#include "shared/misc.h" +#include +#include 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) { WarpRemoveMatchDialog dlg(this, m_current); if (dlg.execute()) m_tableWidget->GetModel()->SetMatchedFunction(nullptr); }, [this](WarpFunctionItem* item, std::optional) { - if (item == nullptr) - return false; - Warp::Ref selectedFunction = item->GetFunction(); - if (!selectedFunction) - return false; - Warp::Ref matchedFunction = m_tableWidget->GetModel()->GetMatchedFunction(); - if (!matchedFunction) - return false; - return BNWARPFunctionsEqual(selectedFunction->m_object, matchedFunction->m_object); - }); + if (item == nullptr) + return false; + Warp::Ref selectedFunction = item->GetFunction(); + if (!selectedFunction) + return false; + Warp::Ref 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) { // Apply the source as the filter. diff --git a/plugins/warp/ui/plugin.cpp b/plugins/warp/ui/plugin.cpp index e99059b98a..0cf8deea6f 100644 --- a/plugins/warp/ui/plugin.cpp +++ b/plugins/warp/ui/plugin.cpp @@ -1,12 +1,15 @@ #include "plugin.h" - -#include - #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 +#include +#include 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 0a4c630c04..4ad43864fb 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 0000000000..911628ebe4 --- /dev/null +++ b/plugins/warp/ui/shared/chunk.cpp @@ -0,0 +1,159 @@ +#include "chunk.h" + +#include +#include +#include +#include +#include +#include + +#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 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 0000000000..a03a8eae57 --- /dev/null +++ b/plugins/warp/ui/shared/chunk.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include +#include "warp.h" + +class ChunkWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ChunkWidget(QWidget* parent = nullptr); + void setChunk(Warp::Ref chunk); + +private slots: + void onSearchTextChanged(const QString& text); + +private: + void populateTable(); + void updateCountLabel(); + + Warp::Ref 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 0000000000..1ea6f4bad8 --- /dev/null +++ b/plugins/warp/ui/shared/commitdialog.cpp @@ -0,0 +1,141 @@ +#include "commitdialog.h" +#include "file.h" +#include "misc.h" + +#include +#include +#include +#include +#include + +CommitDialog::CommitDialog(Warp::Ref 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::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 0000000000..ccbe8da183 --- /dev/null +++ b/plugins/warp/ui/shared/commitdialog.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 m_container; + Warp::Source m_source; + Warp::Ref m_file; + +public: + WarpCommitWorker(Warp::Ref container, Warp::Source source, Warp::Ref 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 file, QWidget* parent = nullptr); + +private slots: + void onContainerChanged(int index); + void onCreateNewSource(); + void onCommit(); + void onCommitFinished(bool success); + +private: + void populateContainers(); + + Warp::Ref m_file; + std::vector> 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 44d7c2375d..41dea3e02b 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 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& containerIndex, - const std::vector& allowedTags, bool rerunMatcher) +void WarpFetchDialog::runBatchedFetch( + const std::optional& containerIndex, const std::vector& allowedTags, bool rerunMatcher) { if (!m_bv) return; @@ -178,48 +178,27 @@ void WarpFetchDialog::runBatchedFetch(const std::optional& 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()) + ")"); - } - - 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"); + 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()); + + if (rerunMatcher && bv) + Warp::RunMatcher(*bv); + }); } diff --git a/plugins/warp/ui/shared/fetchdialog.h b/plugins/warp/ui/shared/fetchdialog.h index 72b8c57e42..68fca6c1f2 100644 --- a/plugins/warp/ui/shared/fetchdialog.h +++ b/plugins/warp/ui/shared/fetchdialog.h @@ -52,5 +52,3 @@ private slots: void runBatchedFetch(const std::optional& containerIndex, const std::vector& allowedTags, bool rerunMatcher); }; - -void RegisterWarpFetchFunctionsCommand(); diff --git a/plugins/warp/ui/shared/fetcher.cpp b/plugins/warp/ui/shared/fetcher.cpp index 2eab910a9f..f5aa626a9c 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& 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 constraintGuids; constraintGuids.reserve(constraints.size()); diff --git a/plugins/warp/ui/shared/fetcher.h b/plugins/warp/ui/shared/fetcher.h index 3464ecd4cd..ad38ed6019 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 m_pendingRequests; std::unordered_set m_processedGuids; + public: using CallbackId = uint64_t; using CompletionCallback = std::function; diff --git a/plugins/warp/ui/shared/file.cpp b/plugins/warp/ui/shared/file.cpp new file mode 100644 index 0000000000..70a169fe30 --- /dev/null +++ b/plugins/warp/ui/shared/file.cpp @@ -0,0 +1,71 @@ +#include "file.h" + +#include + +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 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(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(); + 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 0000000000..057fd2cda5 --- /dev/null +++ b/plugins/warp/ui/shared/file.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include "warp.h" +#include "chunk.h" + +class FileWidget : public QWidget +{ + Q_OBJECT + +public: + explicit FileWidget(QWidget* parent = nullptr); + void setFile(Warp::Ref file); + +private slots: + void onListSelectionChanged(); + +private: + Warp::Ref m_file; + QListWidget* m_list; + ChunkWidget* m_chunkWidget; + std::vector> 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 3b2451ab91..fd403d777a 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 type = m_function->GetType(*analysisFunction)) - tokenData = TokenData(*type, symbolName); + if (Warp::Ref 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)>& callback, const std::function)>& isValid) { diff --git a/plugins/warp/ui/shared/function.h b/plugins/warp/ui/shared/function.h index c6e5ba63c2..4cf226c7b8 100644 --- a/plugins/warp/ui/shared/function.h +++ b/plugins/warp/ui/shared/function.h @@ -112,10 +112,9 @@ class WarpFunctionTableWidget : public QWidget, public FilterTarget void RegisterContextMenuAction( const QString& name, const std::function)>& callback); - void RegisterContextMenuAction( - const QString &name, - const std::function)> &callback, - const std::function)> &isValid); + void RegisterContextMenuAction(const QString& name, + const std::function)>& callback, + const std::function)>& isValid); void SetFunctions(QVector functions); diff --git a/plugins/warp/ui/shared/misc.cpp b/plugins/warp/ui/shared/misc.cpp index 6de314b188..c88cee1042 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 92e27fd88a..e9d6fd43b5 100644 --- a/plugins/warp/ui/shared/misc.h +++ b/plugins/warp/ui/shared/misc.h @@ -67,6 +67,14 @@ class AddressColorDelegate final : public QStyledItemDelegate 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 @@ class WarpRemoveMatchDialog : public QDialog 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(BATCH_SIZE_SETTING, view); +} + +inline void RegisterPluginAction( + std::string name, std::function action, + std::function 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 0000000000..9e602b79df --- /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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(m_includedDataCombo->currentData().toInt()); + auto includedFunctions = + static_cast(m_includedFunctionsCombo->currentData().toInt()); + auto workerCount = static_cast(m_workerCountSpinBox->value()); + + m_processor = std::make_shared(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 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 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& 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(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 0000000000..3f69601d37 --- /dev/null +++ b/plugins/warp/ui/shared/processordialog.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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) +Q_DECLARE_METATYPE(BinaryNinja::Ref) + +// Worker to run the processor +class WarpProcessorWorker : public QThread +{ + Q_OBJECT + + std::shared_ptr m_processor; + +public: + WarpProcessorWorker(std::shared_ptr processor, QObject* parent = nullptr) : + QThread(parent), m_processor(std::move(processor)) + {} + + void run() override + { + Warp::Ref file = m_processor->Start(); + emit finishedProcessing(file); + } + +signals: + void finishedProcessing(Warp::Ref file); +}; + +class ProcessorDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ProcessorDialog(QWidget* parent = nullptr); + ~ProcessorDialog() override; + + void onAddBinaryView(BinaryNinja::Ref view); + void onAddProjectFiles(); + +private slots: + void onStartProcessing(); + void onProcessingFinished(Warp::Ref 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 view; + std::string path; + BinaryNinja::Ref project; + BinaryNinja::Ref projectFile; + QString displayName; + }; + + Warp::Ref m_file; + std::shared_ptr m_processor; + std::vector 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 a852605e6a..801aa23db7 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 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 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 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 0000000000..e8454cff59 --- /dev/null +++ b/plugins/warp/ui/shared/selectprojectfilesdialog.cpp @@ -0,0 +1,141 @@ +#include "selectprojectfilesdialog.h" + +#include +#include +#include +#include +#include + +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::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>(); + 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 items = from->selectedItems(); + for (auto* item : items) + to->addItem(from->takeItem(from->row(item))); + filterLists(); +} + +std::vector> SelectProjectFilesDialog::getSelectedFiles() const +{ + std::vector> files; + for (int i = 0; i < m_addingList->count(); ++i) + files.push_back(m_addingList->item(i)->data(Qt::UserRole).value>()); + return files; +} + +Ref 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 0000000000..ce5e8093ab --- /dev/null +++ b/plugins/warp/ui/shared/selectprojectfilesdialog.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +#include "binaryninjaapi.h" + +class SelectProjectFilesDialog : public QDialog +{ + Q_OBJECT + BinaryNinja::Ref 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> getSelectedFiles() const; + [[nodiscard]] BinaryNinja::Ref getSelectedProject() const; +}; diff --git a/plugins/warp/ui/shared/source.cpp b/plugins/warp/ui/shared/source.cpp new file mode 100644 index 0000000000..c77361516b --- /dev/null +++ b/plugins/warp/ui/shared/source.cpp @@ -0,0 +1,204 @@ +#include "source.h" + +#include +#include +#include +#include + +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(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 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 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 0000000000..7dad933380 --- /dev/null +++ b/plugins/warp/ui/shared/source.h @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include +#include +#include + +#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 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(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 m_rows; + Warp::Ref m_container; +}; + + +class WarpSourcesView : public QTableView +{ + Q_OBJECT + +public: + explicit WarpSourcesView(QWidget* parent = nullptr); + + void setContainer(Warp::Ref container); + bool addSource(); + + [[nodiscard]] WarpSourcesModel* sourceModel() const { return m_model; } + [[nodiscard]] std::optional sourceFromRow(int row) const; + +private: + WarpSourcesModel* m_model = nullptr; + Warp::Ref m_container; +}; \ No newline at end of file diff --git a/rust/src/data_buffer.rs b/rust/src/data_buffer.rs index 79b3b20bbc..f8d71088cb 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 22bf9b4b93..d016d377d4 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) -> Self { + pub unsafe fn from_raw(handle: NonNull) -> Self { Self { handle } } From 7cdd0da31acce195fe22b3668c706cc6e14a09cf Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 13 Mar 2026 14:03:40 -0700 Subject: [PATCH 13/19] [WARP] Warn when matching with relocatable regions in low address space The heuristics will check if a constant is within the relocatable regions and mask. If we are in a low address space we might be masking regular constants like 0x10. --- plugins/warp/src/plugin/workflow.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/warp/src/plugin/workflow.rs b/plugins/warp/src/plugin/workflow.rs index 52c8e249bc..c1728b5e51 100644 --- a/plugins/warp/src/plugin/workflow.rs +++ b/plugins/warp/src/plugin/workflow.rs @@ -44,6 +44,14 @@ impl Command for RunMatcher { "No relocatable regions found, for best results please define sections for the binary!" ); } + for region in regions { + if region.start < 0x1000 { + tracing::warn!( + "Relocatable region has a low start-address ({:0x}), if possible, please rebase the binary to a higher address!", + view.image_base() + ); + } + } run_matcher(&view); }); From 800f141291cae9d55e79b33c7de19444e3bcffa7 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 13 Mar 2026 16:14:44 -0700 Subject: [PATCH 14/19] [WARP] Update docs --- docs/guide/warp.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/guide/warp.md b/docs/guide/warp.md index e6b8695365..b15944cc88 100644 --- a/docs/guide/warp.md +++ b/docs/guide/warp.md @@ -232,6 +232,13 @@ When running the matcher manually, you may get a warning about no relocatable re sections or segments in your view. For WARP to work we must have some range of address space to work with, without it the function GUIDs will likely be inconsistent if the functions can be based at different addresses. +### "Relocatable region has a low start-address" warning + +WARP uses relocatable regions to determine relocatable addresses encoded in instructions, if you have a relocatable region +that covers a low address space, WARP may mask regular constants and other irrelevant instructions. This warning mostly +affects firmware binaries (or other mapped views), if you have not rebased the view to the correct image base, then you +should as it will fix this issue + ### Failed to connect to the server If you fail to connect to a WARP server, you will receive an error in the log. Outside typical network connectivity issues From af9486a33999d6e207ad1de59b3701238a7bb60c Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Mon, 16 Mar 2026 11:48:46 -0700 Subject: [PATCH 15/19] [Rust] Misc project module cleanup --- rust/src/project.rs | 21 +++++++++++++-------- rust/tests/project.rs | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/rust/src/project.rs b/rust/src/project.rs index fc750bcb1b..7188ed59d6 100644 --- a/rust/src/project.rs +++ b/rust/src/project.rs @@ -1,5 +1,4 @@ -pub mod file; -pub mod folder; +//! Represents a collection of files and folders within Binary Ninja. use std::ffi::c_void; use std::fmt::Debug; @@ -17,6 +16,9 @@ use crate::project::folder::ProjectFolder; use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; use crate::string::{BnString, IntoCStr}; +pub mod file; +pub mod folder; + pub struct Project { pub(crate) handle: NonNull, } @@ -30,6 +32,7 @@ impl Project { Ref::new(Self { handle }) } + /// All of the open [`Project`]s pub fn all_open() -> Array { let mut count = 0; let result = unsafe { BNGetOpenProjects(&mut count) }; @@ -37,7 +40,6 @@ impl Project { unsafe { Array::new(result, count, ()) } } - // TODO: Path here is actually local path? /// Create a new project /// /// * `path` - Path to the project directory (.bnpr) @@ -49,7 +51,6 @@ impl Project { NonNull::new(handle).map(|h| unsafe { Self::ref_from_raw(h) }) } - // TODO: Path here is actually local path? /// Open an existing project /// /// * `path` - Path to the project directory (.bnpr) or project metadata file (.bnpm) @@ -73,7 +74,7 @@ impl Project { } } - /// Close a open project + /// Close an open project pub fn close(&self) -> Result<(), ()> { if unsafe { BNProjectClose(self.handle.as_ptr()) } { Ok(()) @@ -87,9 +88,10 @@ impl Project { unsafe { BnString::into_string(BNProjectGetId(self.handle.as_ptr())) } } - /// Get the path of the project - pub fn path(&self) -> String { - unsafe { BnString::into_string(BNProjectGetPath(self.handle.as_ptr())) } + /// Get the path on disk for the project + pub fn path(&self) -> PathBuf { + let path_str = unsafe { BnString::into_string(BNProjectGetPath(self.handle.as_ptr())) }; + PathBuf::from(path_str) } /// Get the name of the project @@ -136,6 +138,7 @@ impl Project { unsafe { BNProjectRemoveMetadata(self.handle.as_ptr(), key_raw.as_ptr()) } } + /// Call this after updating the [`ProjectFolder`] to have the changes reflected in the database. pub fn push_folder(&self, file: &ProjectFolder) -> bool { unsafe { BNProjectPushFolder(self.handle.as_ptr(), file.handle.as_ptr()) } } @@ -212,6 +215,7 @@ impl Project { } } + // TODO: Rename create_folder_with_id and comment about the id being unique /// Recursively create files and folders in the project from a path on disk /// /// * `parent` - Parent folder in the project that will contain the new folder @@ -289,6 +293,7 @@ impl Project { } } + /// Call this after updating the [`ProjectFile`] to have the changes reflected in the database. pub fn push_file(&self, file: &ProjectFile) -> bool { unsafe { BNProjectPushFile(self.handle.as_ptr(), file.handle.as_ptr()) } } diff --git a/rust/tests/project.rs b/rust/tests/project.rs index 18676b2ef1..263dea7a58 100644 --- a/rust/tests/project.rs +++ b/rust/tests/project.rs @@ -26,7 +26,7 @@ fn create_delete_empty() { let project_path_received = project.path(); assert_eq!( canonicalize(&project_path).unwrap(), - canonicalize(project_path_received.to_string()).unwrap() + canonicalize(project_path_received).unwrap() ); let project_name_received = project.name(); assert_eq!(project_name, project_name_received.as_str()); From d1780992ddec123345a31a002a1d35a1b0c0492d Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Tue, 17 Mar 2026 20:28:04 -0700 Subject: [PATCH 16/19] [Python] Update function signatures of some type library APIs --- python/typelibrary.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/python/typelibrary.py b/python/typelibrary.py index 5dec0fd9a0..dc33fe3ee3 100644 --- a/python/typelibrary.py +++ b/python/typelibrary.py @@ -359,7 +359,7 @@ def type_container(self) -> 'typecontainer.TypeContainer': """ return typecontainer.TypeContainer(core.BNGetTypeLibraryTypeContainer(self.handle)) - def add_named_object(self, name: 'types.QualifiedName', type: 'types.Type') -> None: + def add_named_object(self, name: Union[types.QualifiedName, str], type: 'types.Type') -> None: """ `add_named_object` directly inserts a named object into the type library's object store. This is not done recursively, so care should be taken that types referring to other types @@ -380,7 +380,7 @@ def add_named_object(self, name: 'types.QualifiedName', type: 'types.Type') -> N raise ValueError("type must be a Type") core.BNAddTypeLibraryNamedObject(self.handle, name._to_core_struct(), type.handle) - def remove_named_object(self, name: 'types.QualifiedName') -> None: + def remove_named_object(self, name: Union[types.QualifiedName, str]) -> None: """ `remove_named_object` removes a named object from the type library's object store. This does not remove any types that are referenced by the object, only the object itself. @@ -388,6 +388,8 @@ def remove_named_object(self, name: 'types.QualifiedName') -> None: :param QualifiedName name: :rtype: None """ + if not isinstance(name, types.QualifiedName): + name = types.QualifiedName(name) core.BNRemoveTypeLibraryNamedObject(self.handle, name._to_core_struct()) def add_named_type(self, name: 'types.QualifiedNameType', type: 'types.Type') -> None: @@ -411,20 +413,24 @@ def add_named_type(self, name: 'types.QualifiedNameType', type: 'types.Type') -> raise ValueError("parameter type must be a Type") core.BNAddTypeLibraryNamedType(self.handle, name._to_core_struct(), type.handle) - def remove_named_type(self, name: 'types.QualifiedName') -> None: + def remove_named_type(self, name: Union[types.QualifiedName, str]) -> None: """ `remove_named_type` removes a named type from the type library's type store. This does not remove any objects that reference the type, only the type itself. """ + if not isinstance(name, types.QualifiedName): + name = types.QualifiedName(name) core.BNRemoveTypeLibraryNamedType(self.handle, name._to_core_struct()) - def add_type_source(self, name: types.QualifiedName, source: str) -> None: + def add_type_source(self, name: Union[types.QualifiedName, str], source: str) -> None: """ Manually flag NamedTypeReferences to the given QualifiedName as originating from another source TypeLibrary with the given dependency name. .. warning:: Use this api with extreme caution. """ + if not isinstance(name, types.QualifiedName): + name = types.QualifiedName(name) core.BNAddTypeLibraryNamedTypeSource(self.handle, types.QualifiedName(name)._to_core_struct(), source) def get_named_object(self, name: Union[types.QualifiedName, str]) -> Optional[types.Type]: From c88df04818e9cc7ba7031c407eb4596bf8cd4f27 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Tue, 17 Mar 2026 20:28:49 -0700 Subject: [PATCH 17/19] [WARP] Do a partial update of the sidebar UI when navigating instead of a full update Reduces unnecessary work --- plugins/warp/ui/plugin.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/warp/ui/plugin.cpp b/plugins/warp/ui/plugin.cpp index 0cf8deea6f..93900ed9e2 100644 --- a/plugins/warp/ui/plugin.cpp +++ b/plugins/warp/ui/plugin.cpp @@ -174,7 +174,12 @@ WarpSidebarWidget::WarpSidebarWidget(BinaryViewRef data) : SidebarWidget("WARP") m_fetcher = WarpFetcher::Global(); m_callbackId = m_fetcher->AddCompletionCallback([this]() { - ExecuteOnMainThread([this]() { Update(); }); + ExecuteOnMainThread([this]() { + // Instead of doing a full update after fetching, we only want to make sure the current function has + // up-to-date matches, since the other two tabs (all matches, container list) do not get populated with + // additional information or manage their own updates (e.g. container source list). + m_currentFunctionWidget->UpdateMatches(); + }); return KeepCallback; }); From b573220ea84dcdbb3537da9036bef62014199066 Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Tue, 17 Mar 2026 20:29:22 -0700 Subject: [PATCH 18/19] [BNTL] Fix misc doc comments missing --- plugins/bntl_utils/src/schema.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/bntl_utils/src/schema.rs b/plugins/bntl_utils/src/schema.rs index 2a9e6d50b1..e07b1144dc 100644 --- a/plugins/bntl_utils/src/schema.rs +++ b/plugins/bntl_utils/src/schema.rs @@ -5,9 +5,9 @@ use std::path::Path; #[derive(Deserialize, Debug)] pub struct BntlSchema { - // The list of library names this library depends on + /// The list of library names this library depends on pub dependencies: Vec, - // Maps internal type IDs or names to their external sources + /// Maps internal type IDs or names to their external sources pub type_sources: Vec, } @@ -35,8 +35,8 @@ impl BntlSchema { #[derive(Deserialize, Debug)] pub struct TypeSource { - // The components of the name, e.g., ["std", "string"] + /// The components of the name, e.g., ["std", "string"] pub name: Vec, - // The name of the dependency library it comes from + /// The name of the dependency library it comes from pub source: String, } From bba6a9cc33d3fae6f3df1a72c2b2f83647776e2f Mon Sep 17 00:00:00 2001 From: Mason Reed Date: Fri, 20 Mar 2026 18:04:40 -0700 Subject: [PATCH 19/19] [WARP] Sanitize server URLs --- plugins/warp/src/container/network/client.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/warp/src/container/network/client.rs b/plugins/warp/src/container/network/client.rs index 3d0af05797..ad31b0e577 100644 --- a/plugins/warp/src/container/network/client.rs +++ b/plugins/warp/src/container/network/client.rs @@ -46,7 +46,11 @@ impl NetworkClient { Self { provider, headers, - server_url, + // We place the '/' already in the materialized URLs we query, so strip it here. + server_url: server_url + .strip_suffix('/') + .unwrap_or(&server_url) + .to_string(), } }