summaryrefslogtreecommitdiff
path: root/plugins/warp/src
diff options
context:
space:
mode:
authorMason Reed <mason@vector35.com>2026-03-13 12:24:18 -0700
committerMason Reed <35282038+emesare@users.noreply.github.com>2026-03-24 18:46:48 -0700
commit3c88b11e5df33116580ac008e36092775df66135 (patch)
treecd64fbe149f05683ddabcccd0db7b87356f1f8cf /plugins/warp/src
parentf325aa7b6026a1daef84931baeb1f50d7da10c10 (diff)
[WARP] Improved UX and API
- Exposes WARP type objects directly - Adds processor API (for generating warp files directly) - Adds file and chunk API - Misc cleanup - Simplified the amount of commands - Replaced the "Create" commands with a purpose built processor dialog - Added a native QT viewer for WARP files - Simplified committing to a remote with a purpose built commit dialog
Diffstat (limited to 'plugins/warp/src')
-rw-r--r--plugins/warp/src/container.rs24
-rw-r--r--plugins/warp/src/lib.rs1
-rw-r--r--plugins/warp/src/plugin.rs77
-rw-r--r--plugins/warp/src/plugin/commit.rs149
-rw-r--r--plugins/warp/src/plugin/create.rs265
-rw-r--r--plugins/warp/src/plugin/debug.rs49
-rw-r--r--plugins/warp/src/plugin/ffi.rs3
-rw-r--r--plugins/warp/src/plugin/ffi/container.rs61
-rw-r--r--plugins/warp/src/plugin/ffi/file.rs137
-rw-r--r--plugins/warp/src/plugin/ffi/function.rs19
-rw-r--r--plugins/warp/src/plugin/ffi/processor.rs187
-rw-r--r--plugins/warp/src/plugin/ffi/ty.rs75
-rw-r--r--plugins/warp/src/plugin/file.rs41
-rw-r--r--plugins/warp/src/plugin/function.rs102
-rw-r--r--plugins/warp/src/plugin/project.rs286
-rw-r--r--plugins/warp/src/processor.rs345
-rw-r--r--plugins/warp/src/report.rs185
-rw-r--r--plugins/warp/src/templates/file.html37
-rw-r--r--plugins/warp/src/templates/file.json21
-rw-r--r--plugins/warp/src/templates/file.md15
20 files changed, 554 insertions, 1525 deletions
diff --git a/plugins/warp/src/container.rs b/plugins/warp/src/container.rs
index 4feed24c..bb5d7fd4 100644
--- a/plugins/warp/src/container.rs
+++ b/plugins/warp/src/container.rs
@@ -7,12 +7,14 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use thiserror::Error;
use uuid::Uuid;
+use warp::chunk::{Chunk, ChunkKind};
use warp::r#type::guid::TypeGUID;
use warp::r#type::{ComputedType, Type};
use warp::signature::constraint::ConstraintGUID;
use warp::signature::function::{Function, FunctionGUID};
use warp::symbol::Symbol;
use warp::target::Target;
+use warp::WarpFile;
pub mod disk;
pub mod memory;
@@ -291,6 +293,28 @@ pub trait Container: Send + Sync + Display + Debug {
functions: &[Function],
) -> ContainerResult<()>;
+ /// Add the `chunk`s data to the provided `source` if it exists and is writable.
+ fn add_chunk(&mut self, source: &SourceId, chunk: &Chunk) -> ContainerResult<()> {
+ match &chunk.kind {
+ ChunkKind::Signature(sc) => {
+ let functions: Vec<_> = sc.functions().collect();
+ self.add_functions(&chunk.header.target, source, &functions)
+ }
+ ChunkKind::Type(tc) => {
+ let types: Vec<_> = tc.types().collect();
+ self.add_computed_types(source, &types)
+ }
+ }
+ }
+
+ /// Add the `file` data to the provided `source` if it exists and is writable.
+ fn add_file(&mut self, source: &SourceId, file: &WarpFile) -> ContainerResult<()> {
+ for chunk in &file.chunks {
+ self.add_chunk(source, chunk)?;
+ }
+ Ok(())
+ }
+
/// Fetches WARP information for the associated functions.
///
/// Typically, a container that resides only in memory has nothing to fetch, so the default implementation
diff --git a/plugins/warp/src/lib.rs b/plugins/warp/src/lib.rs
index 6889c79d..e0aaeed3 100644
--- a/plugins/warp/src/lib.rs
+++ b/plugins/warp/src/lib.rs
@@ -35,7 +35,6 @@ pub mod container;
pub mod convert;
pub mod matcher;
pub mod processor;
-pub mod report;
/// Only used when compiled for cdylib target.
mod plugin;
diff --git a/plugins/warp/src/plugin.rs b/plugins/warp/src/plugin.rs
index d163205b..8192102d 100644
--- a/plugins/warp/src/plugin.rs
+++ b/plugins/warp/src/plugin.rs
@@ -9,27 +9,17 @@ use crate::plugin::render_layer::HighlightRenderLayer;
use crate::plugin::settings::PluginSettings;
use crate::{core_signature_dir, user_signature_dir};
use binaryninja::background_task::BackgroundTask;
-use binaryninja::command::{
- register_command, register_command_for_function, register_command_for_project,
- register_global_command,
-};
+use binaryninja::command::{register_command, register_command_for_function};
use binaryninja::is_ui_enabled;
use binaryninja::settings::{QueryOptions, Settings};
-mod commit;
-mod create;
mod ffi;
-mod file;
mod function;
mod load;
-mod project;
mod render_layer;
mod settings;
mod workflow;
-#[cfg(debug_assertions)]
-mod debug;
-
fn load_bundled_signatures() {
let global_bn_settings = Settings::new();
let plugin_settings =
@@ -166,39 +156,12 @@ fn plugin_init() -> bool {
workflow::RunMatcher {},
);
- #[cfg(debug_assertions)]
- register_command(
- "WARP\\Debug\\Cache",
- "Debug cache sizes... because...",
- debug::DebugCache {},
- );
-
- #[cfg(debug_assertions)]
- register_command(
- "WARP\\Debug\\Invalidate Caches",
- "Invalidate all WARP caches",
- debug::DebugInvalidateCache {},
- );
-
- #[cfg(debug_assertions)]
- register_command_for_function(
- "WARP\\Debug\\Function Signature",
- "Print the entire signature for the function",
- debug::DebugFunction {},
- );
-
register_command(
"WARP\\Load File",
- "Load file into the matcher, this does NOT kick off matcher analysis",
+ "Load WARP file",
load::LoadSignatureFile {},
);
- register_global_command(
- "WARP\\Commit File",
- "Commit file to a source",
- commit::CommitFile {},
- );
-
register_command_for_function(
"WARP\\Include Function",
"Add current function to the list of functions to add to the signature file",
@@ -217,42 +180,6 @@ fn plugin_init() -> bool {
function::RemoveFunction {},
);
- register_command_for_function(
- "WARP\\Copy GUID",
- "Copy the computed GUID for the function",
- function::CopyFunctionGUID {},
- );
-
- register_command(
- "WARP\\Find GUID",
- "Locate the function in the view using a GUID",
- function::FindFunctionFromGUID {},
- );
-
- register_command(
- "WARP\\Create\\From Current View",
- "Creates a signature file containing all selected functions",
- create::CreateFromCurrentView {},
- );
-
- register_global_command(
- "WARP\\Create\\From File(s)",
- "Creates a signature file containing all selected functions",
- create::CreateFromFiles {},
- );
-
- register_command(
- "WARP\\Show Report",
- "Creates a report for the selected file, displaying info on functions and types",
- file::ShowFileReport {},
- );
-
- register_command_for_project(
- "WARP\\Create\\From Project",
- "Create signature files from select project files",
- project::CreateSignatures {},
- );
-
true
}
diff --git a/plugins/warp/src/plugin/commit.rs b/plugins/warp/src/plugin/commit.rs
deleted file mode 100644
index 4e8a9fee..00000000
--- a/plugins/warp/src/plugin/commit.rs
+++ /dev/null
@@ -1,149 +0,0 @@
-//! Commit file to a source.
-
-use crate::cache::container::cached_containers;
-use crate::container::{SourceId, SourcePath};
-use crate::plugin::create::OpenFileField;
-use binaryninja::command::GlobalCommand;
-use binaryninja::interaction::{Form, FormInputField};
-use warp::chunk::ChunkKind;
-use warp::WarpFile;
-
-pub struct SelectedSourceField {
- sources: Vec<(SourceId, SourcePath)>,
-}
-
-impl SelectedSourceField {
- pub fn field(&self) -> FormInputField {
- FormInputField::Choice {
- prompt: "Selected Source".to_string(),
- choices: self
- .sources
- .iter()
- .map(|(id, path)| {
- // For display purposes we only want to show the last path item.
- let path_name = path
- .to_string()
- .rsplit_once('/')
- .map_or(path.to_string(), |(_, last_path_item)| {
- last_path_item.to_string()
- });
- // TODO: Probably have a truncation limit here, this is just for display after all.
- format!("{} ({})", path_name, id)
- })
- .collect(),
- default: None,
- value: 0,
- }
- }
-
- pub fn from_form(&self, form: &Form) -> Option<SourceId> {
- let field = form.get_field_with_name("Selected Source")?;
- let field_value = field.try_value_index()?;
- self.sources.get(field_value).map(|(id, _)| *id)
- }
-}
-
-pub struct CommitFile;
-
-impl CommitFile {
- pub fn selected_source_field() -> SelectedSourceField {
- let mut writable_sources = Vec::new();
- for container in cached_containers() {
- if let Ok(container) = container.read() {
- for source in container.sources().unwrap_or_default() {
- if let Ok(true) = container.is_source_writable(&source) {
- if let Ok(source_path) = container.source_path(&source) {
- writable_sources.push((source, source_path));
- }
- }
- }
- }
- }
- SelectedSourceField {
- sources: writable_sources,
- }
- }
-
- pub fn execute() -> Option<()> {
- let mut form = Form::new("Commit File");
-
- // Users are going to get confused between this and adding functions to a source then commiting.
- // So we should make it clear with a label, and also probably deprecate this command and replace it with "add functions to source" and "commit source".
- form.add_field(FormInputField::Label {
- prompt: "Commits a WARP file to an existing source, this is primarily used for committing to network containers".to_string()
- });
-
- form.add_field(OpenFileField::field());
- let source_field = Self::selected_source_field();
- form.add_field(source_field.field());
-
- if !form.prompt() {
- return None;
- }
-
- let open_file_path = OpenFileField::from_form(&form)?;
- let source_id = source_field.from_form(&form)?;
- tracing::info!("Committing file to source: {}", source_id);
-
- let bytes = std::fs::read(open_file_path).ok()?;
- let Some(warp_file) = WarpFile::from_bytes(&bytes) else {
- tracing::error!("Failed to parse warp file!");
- return None;
- };
-
- for container in cached_containers() {
- let Ok(mut container) = container.write() else {
- continue;
- };
-
- if let Ok(true) = container.is_source_writable(&source_id) {
- // TODO: We need to find a sane way to do this procedure through the FFI.
- for chunk in &warp_file.chunks {
- match &chunk.kind {
- ChunkKind::Signature(sc) => {
- let functions: Vec<_> = sc.functions().collect();
- tracing::info!(
- "Adding {} functions to source: {}",
- functions.len(),
- source_id
- );
- if let Err(e) = container.add_functions(
- &chunk.header.target,
- &source_id,
- &functions,
- ) {
- tracing::error!("Failed to add functions to source: {}", e);
- }
- }
- ChunkKind::Type(sc) => {
- let types: Vec<_> = sc.types().collect();
- tracing::info!("Adding {} types to source: {}", types.len(), source_id);
- if let Err(e) = container.add_computed_types(&source_id, &types) {
- tracing::error!("Failed to add types to source: {}", e);
- }
- }
- }
- }
- if let Err(e) = container.commit_source(&source_id) {
- tracing::error!("Failed to commit source: {}", e);
- }
- tracing::info!("Committed file to source: {}", source_id);
- return Some(());
- }
- }
-
- Some(())
- }
-}
-
-impl GlobalCommand for CommitFile {
- fn action(&self) {
- std::thread::spawn(move || {
- Self::execute();
- });
- }
-
- fn valid(&self) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/create.rs b/plugins/warp/src/plugin/create.rs
deleted file mode 100644
index 5f5527a7..00000000
--- a/plugins/warp/src/plugin/create.rs
+++ /dev/null
@@ -1,265 +0,0 @@
-use crate::processor::{
- new_processing_state_background_thread, CompressionTypeField, FileDataKindField,
- IncludedFunctionsField, SaveReportToDiskField, WarpFileProcessor,
-};
-use crate::report::{ReportGenerator, ReportKindField};
-use crate::{user_signature_dir, INCLUDE_TAG_NAME};
-use binaryninja::background_task::BackgroundTask;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::{Command, GlobalCommand};
-use binaryninja::file_metadata::FileMetadata;
-use binaryninja::interaction::form::{Form, FormInputField};
-use binaryninja::interaction::{MessageBoxButtonResult, MessageBoxButtonSet, MessageBoxIcon};
-use binaryninja::rc::Ref;
-use std::path::PathBuf;
-use std::thread;
-use warp::chunk::Chunk;
-use warp::WarpFile;
-
-pub struct SaveFileField;
-
-impl SaveFileField {
- pub fn field(view: &BinaryView) -> FormInputField {
- let file = view.file();
- let default_name = match file.project_file() {
- None => {
- // Not in a project, use the file name directly.
- file.display_name()
- }
- Some(project_file) => project_file.name(),
- };
- let signature_dir = user_signature_dir();
- let default_file_path = signature_dir.join(&default_name).with_extension("warp");
- FormInputField::SaveFileName {
- prompt: "File Path".to_string(),
- // TODO: This is called extension but is really a filter.
- extension: Some("*.warp".to_string()),
- default: Some(default_file_path.to_string_lossy().to_string()),
- value: None,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<PathBuf> {
- let field = form.get_field_with_name("File Path")?;
- let field_value = field.try_value_string()?;
- Some(PathBuf::from(field_value))
- }
-}
-
-pub struct OpenFileField;
-
-impl OpenFileField {
- pub fn field() -> FormInputField {
- FormInputField::OpenFileName {
- prompt: "Input File Path".to_string(),
- extension: None,
- default: None,
- value: None,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<PathBuf> {
- let field = form.get_field_with_name("Input File Path")?;
- let field_value = field.try_value_string()?;
- Some(PathBuf::from(field_value))
- }
-}
-
-pub struct CreateFromCurrentView;
-
-impl CreateFromCurrentView {
- pub fn execute(view: Ref<BinaryView>, external_file: bool) -> Option<()> {
- // Prompt the user first so that they can go do other things and not worry about a popup.
- let mut form = Form::new("Create From View");
-
- if external_file {
- form.add_field(OpenFileField::field());
- }
-
- form.add_field(SaveFileField::field(&view));
-
- let fd_field = FileDataKindField::default();
- form.add_field(fd_field.to_field());
-
- let compression_field = CompressionTypeField::default();
- form.add_field(compression_field.to_field());
-
- let mut included_field = IncludedFunctionsField::default();
- // If the view has the include tag, we better set the default to the selected functions.
- if view.tag_type_by_name(INCLUDE_TAG_NAME).is_some() {
- included_field = IncludedFunctionsField::Selected;
- }
- form.add_field(included_field.to_field());
-
- let report_field = ReportKindField::default();
- form.add_field(report_field.to_field());
- let report_to_disk_field = SaveReportToDiskField::default();
- form.add_field(report_to_disk_field.to_field());
-
- if !form.prompt() {
- return None;
- }
- let compression_type = CompressionTypeField::from_form(&form).unwrap_or_default();
- let file_path = SaveFileField::from_form(&form)?;
- let file_data_kind = FileDataKindField::from_form(&form).unwrap_or_default();
- let file_included_functions = IncludedFunctionsField::from_form(&form).unwrap_or_default();
- let report_kind = ReportKindField::from_form(&form).unwrap_or_default();
- let save_report_to_disk = SaveReportToDiskField::from_form(&form).unwrap_or_default();
- let open_file_path = OpenFileField::from_form(&form);
-
- // If we already have a file, prompt the user if they want to add the data.
- let mut existing_chunks = Vec::new();
- if file_path.exists() {
- let prompt_result = binaryninja::interaction::show_message_box(
- "Keep existing file data?",
- "The file already exists. Do you want to keep the existing data?",
- MessageBoxButtonSet::YesNoCancelButtonSet,
- MessageBoxIcon::QuestionIcon,
- );
-
- match prompt_result {
- MessageBoxButtonResult::NoButton => {
- // User wants to overwrite the file.
- }
- MessageBoxButtonResult::YesButton | MessageBoxButtonResult::OKButton => {
- // User wants to keep the existing data.
- let data = std::fs::read(&file_path).ok()?;
- let existing_file = WarpFile::from_owned_bytes(data)?;
- existing_chunks.extend(existing_file.chunks);
- }
- MessageBoxButtonResult::CancelButton => {
- tracing::info!(
- "User cancelled signature file creation, no operations were performed."
- );
- return None;
- }
- }
- }
-
- let processor = WarpFileProcessor::new()
- .with_compression_type(compression_type)
- .with_file_data(file_data_kind)
- .with_included_functions(file_included_functions);
-
- let file = match open_file_path {
- None => {
- // We are processing the current view. NOT an external file.
- // Reference path is just used for the state tracking. Does not need to be readable.
- let reference_path = file_path.clone();
- processor.process_view(reference_path, &view)
- }
- Some(open_file_path) => {
- // This thread will show the state in a background task.
- let background_task = BackgroundTask::new("Processing started...", true);
- new_processing_state_background_thread(background_task.clone(), processor.state());
- let file = processor.process(open_file_path);
- background_task.finish();
- file
- }
- };
-
- if let Err(err) = file {
- binaryninja::interaction::show_message_box(
- "Failed to create signature file",
- &err.to_string(),
- MessageBoxButtonSet::OKButtonSet,
- MessageBoxIcon::ErrorIcon,
- );
- tracing::error!("Failed to create signature file: {}", err);
- return None;
- }
-
- let background_task = BackgroundTask::new("Creating WARP File...", false);
- let mut file = file.unwrap();
- // Add back the existing chunks if the user selected to keep them.
- if !existing_chunks.is_empty() {
- file.chunks.extend(existing_chunks);
- // TODO: Make merging optional?
- // TODO: Merging can lose chunk data if it goes above the maximum table count.
- // TODO: We should probably solve that in the warp crate itself?
- file.chunks = Chunk::merge(&file.chunks, compression_type.into());
-
- // After merging, we should have at least one chunk. If not, merging actually removed data.
- if file.chunks.len() < 1 {
- tracing::error!(
- "Failed to merge chunks! Please report this, it should not happen."
- );
- return None;
- }
- }
-
- let file_bytes = file.to_bytes();
- let file_size = file_bytes.len();
- if std::fs::write(&file_path, file_bytes).is_err() {
- tracing::error!("Failed to write data to signature file!");
- }
- tracing::info!("Saved signature file to: '{}'", file_path.display());
- background_task.finish();
-
- // Show a report of the generate signatures, if desired.
- let report_generator = ReportGenerator::new();
- if let Some(report_string) = report_generator.report(&report_kind, &file) {
- if save_report_to_disk == SaveReportToDiskField::Yes {
- let report_ext = report_generator
- .report_extension(&report_kind)
- .unwrap_or_default();
- let report_path = file_path.with_extension(report_ext);
- let _ = std::fs::write(report_path, &report_string);
- }
-
- // The ReportWidget uses a QTextBrowser, which cannot render large files very well.
- if file_size > 10000000 {
- tracing::warn!("WARP report file is too large to show in the UI. Please see the report file on disk.");
- } else {
- match report_kind {
- ReportKindField::None => {}
- ReportKindField::Html => {
- view.show_html_report("Generated WARP File", report_string.as_str(), "");
- }
- ReportKindField::Markdown => {
- view.show_markdown_report(
- "Generated WARP File",
- report_string.as_str(),
- "",
- );
- }
- ReportKindField::Json => {
- view.show_plaintext_report("Generated WARP File", report_string.as_str());
- }
- }
- }
- }
-
- Some(())
- }
-}
-
-impl Command for CreateFromCurrentView {
- fn action(&self, view: &BinaryView) {
- let view = view.to_owned();
- thread::spawn(move || {
- CreateFromCurrentView::execute(view, false);
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
-
-pub struct CreateFromFiles;
-
-impl GlobalCommand for CreateFromFiles {
- fn action(&self) {
- let empty_file_metadata = FileMetadata::new();
- let empty_bv = BinaryView::from_data(&empty_file_metadata, &[]);
- thread::spawn(move || {
- CreateFromCurrentView::execute(empty_bv.to_owned(), true);
- empty_bv.file().close();
- });
- }
-
- fn valid(&self) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/debug.rs b/plugins/warp/src/plugin/debug.rs
deleted file mode 100644
index 4198ea30..00000000
--- a/plugins/warp/src/plugin/debug.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use crate::cache::container::for_cached_containers;
-use crate::{build_function, cache};
-use binaryninja::binary_view::BinaryView;
-use binaryninja::command::{Command, FunctionCommand};
-use binaryninja::function::Function;
-use binaryninja::object_destructor::ObjectDestructor;
-
-pub struct DebugFunction;
-
-impl FunctionCommand for DebugFunction {
- fn action(&self, _view: &BinaryView, func: &Function) {
- tracing::info!(
- "{:#?}",
- build_function(func, || func.lifted_il().ok(), false)
- );
- }
-
- fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
- true
- }
-}
-
-pub struct DebugCache;
-
-impl Command for DebugCache {
- fn action(&self, _view: &BinaryView) {
- for_cached_containers(|c| {
- tracing::info!("Container: {:#?}", c);
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
-
-pub struct DebugInvalidateCache;
-
-impl Command for DebugInvalidateCache {
- fn action(&self, view: &BinaryView) {
- let destructor = cache::CacheDestructor {};
- destructor.destruct_view(view);
- tracing::info!("Invalidated all WARP caches...");
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/ffi.rs b/plugins/warp/src/plugin/ffi.rs
index c1f5acb8..bb6fb483 100644
--- a/plugins/warp/src/plugin/ffi.rs
+++ b/plugins/warp/src/plugin/ffi.rs
@@ -1,6 +1,8 @@
mod container;
mod file;
mod function;
+mod processor;
+mod ty;
use binaryninjacore_sys::{
BNBasicBlock, BNBinaryView, BNFunction, BNLowLevelILFunction, BNPlatform,
@@ -48,6 +50,7 @@ pub type BNWARPTypeGUID = TypeGUID;
pub type BNWARPTarget = warp::target::Target;
pub type BNWARPFunction = warp::signature::function::Function;
pub type BNWARPContainer = RwLock<Box<dyn Container>>;
+pub type BNWARPType = warp::r#type::Type;
// TODO: Some sort of callback for loading functions
// TODO: Be able to run matcher for a specific file
diff --git a/plugins/warp/src/plugin/ffi/container.rs b/plugins/warp/src/plugin/ffi/container.rs
index aa0c37d3..09f15d99 100644
--- a/plugins/warp/src/plugin/ffi/container.rs
+++ b/plugins/warp/src/plugin/ffi/container.rs
@@ -3,17 +3,11 @@ use crate::container::disk::DiskContainer;
use crate::container::{
ContainerSearchItem, ContainerSearchItemKind, ContainerSearchQuery, SourcePath, SourceTag,
};
-use crate::convert::{from_bn_type, to_bn_type};
use crate::plugin::ffi::{
BNWARPConstraintGUID, BNWARPContainer, BNWARPFunction, BNWARPFunctionGUID, BNWARPSource,
- BNWARPTarget, BNWARPTypeGUID,
+ BNWARPTarget, BNWARPType, BNWARPTypeGUID,
};
-use binaryninja::architecture::CoreArchitecture;
-use binaryninja::binary_view::BinaryView;
-use binaryninja::rc::Ref;
use binaryninja::string::BnString;
-use binaryninja::types::Type;
-use binaryninjacore_sys::{BNArchitecture, BNBinaryView, BNType};
use std::collections::HashMap;
use std::ffi::{c_char, CStr};
use std::mem::ManuallyDrop;
@@ -98,32 +92,25 @@ pub unsafe extern "C" fn BNWARPContainerSearchItemGetSource(
#[no_mangle]
pub unsafe extern "C" fn BNWARPContainerSearchItemGetType(
- arch: *mut BNArchitecture,
item: *mut BNWARPContainerSearchItem,
-) -> *mut BNType {
- // NOTE: to convert the type, we must have an architecture.
- let arch = match !arch.is_null() {
- true => Some(CoreArchitecture::from_raw(arch)),
- false => None,
- };
-
+) -> *mut BNWARPType {
let item = ManuallyDrop::new(Arc::from_raw(item));
match &item.kind {
ContainerSearchItemKind::Source { .. } => std::ptr::null_mut(),
ContainerSearchItemKind::Function(func) => {
match &func.ty {
None => std::ptr::null_mut(),
- Some(ty) => {
- let bn_ty = to_bn_type(arch, &ty);
- // NOTE: The type ref has been pre-incremented for the caller.
- unsafe { Ref::into_raw(bn_ty) }.handle
+ Some(func_ty) => {
+ let arc_func_ty = Arc::new(func_ty.clone());
+ // NOTE: Freed by BNWARPFreeTypeReference
+ Arc::into_raw(arc_func_ty) as *mut BNWARPType
}
}
}
ContainerSearchItemKind::Type(ty) => {
- let bn_ty = to_bn_type(arch, &ty);
- // NOTE: The type ref has been pre-incremented for the caller.
- unsafe { Ref::into_raw(bn_ty) }.handle
+ let arc_ty = Arc::new(ty.clone());
+ // NOTE: Freed by BNWARPFreeTypeReference
+ Arc::into_raw(arc_ty) as *mut BNWARPType
}
ContainerSearchItemKind::Symbol(_) => std::ptr::null_mut(),
}
@@ -254,7 +241,7 @@ pub unsafe extern "C" fn BNWARPContainerGetSources(
return std::ptr::null_mut();
};
- // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ // NOTE: Leak the sources to be freed by BNWARPFreeUUIDList
let boxed_sources: Box<[_]> = container.sources().unwrap_or_default().into_boxed_slice();
*count = boxed_sources.len();
Box::into_raw(boxed_sources) as *mut BNWARPSource
@@ -389,14 +376,11 @@ pub unsafe extern "C" fn BNWARPContainerAddFunctions(
#[no_mangle]
pub unsafe extern "C" fn BNWARPContainerAddTypes(
- view: *mut BNBinaryView,
container: *mut BNWARPContainer,
source: *const BNWARPSource,
- types: *mut *mut BNType,
+ types: *mut *mut BNWARPType,
count: usize,
) -> bool {
- let view = unsafe { BinaryView::from_raw(view) };
-
let arc_container = ManuallyDrop::new(Arc::from_raw(container));
let Ok(mut container) = arc_container.write() else {
return false;
@@ -405,10 +389,11 @@ pub unsafe extern "C" fn BNWARPContainerAddTypes(
let source = unsafe { *source };
let types_ptr = std::slice::from_raw_parts(types, count);
+ // TODO: We have to clone the objects here to make the type checker happy.
+ // TODO: See about avoiding this later.
let types: Vec<_> = types_ptr
.iter()
- .map(|&t| Type::from_raw(t))
- .map(|ty| from_bn_type(&view, &ty, 255))
+ .map(|&t| unsafe { ManuallyDrop::new(Arc::from_raw(t)).as_ref().clone() })
.collect();
container.add_types(&source, &types).is_ok()
}
@@ -476,7 +461,7 @@ pub unsafe extern "C" fn BNWARPContainerGetSourcesWithFunctionGUID(
let guid = unsafe { *guid };
- // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ // NOTE: Leak the sources to be freed by BNWARPFreeUUIDList
let boxed_sources: Box<[_]> = container
.sources_with_function_guid(&target, &guid)
.unwrap_or_default()
@@ -498,7 +483,7 @@ pub unsafe extern "C" fn BNWARPContainerGetSourcesWithTypeGUID(
let guid = unsafe { *guid };
- // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ // NOTE: Leak the sources to be freed by BNWARPFreeUUIDList
let boxed_sources: Box<[_]> = container
.sources_with_type_guid(&guid)
.unwrap_or_default()
@@ -538,22 +523,17 @@ pub unsafe extern "C" fn BNWARPContainerGetFunctionsWithGUID(
Box::into_raw(raw_boxed_functions) as *mut *mut BNWARPFunction
}
-// TODO: Swap arch to Target?
#[no_mangle]
pub unsafe extern "C" fn BNWARPContainerGetTypeWithGUID(
- arch: *mut BNArchitecture,
container: *mut BNWARPContainer,
source: *const BNWARPSource,
guid: *const BNWARPTypeGUID,
-) -> *mut BNType {
+) -> *mut BNWARPType {
let arc_container = ManuallyDrop::new(Arc::from_raw(container));
let Ok(container) = arc_container.read() else {
return std::ptr::null_mut();
};
- // NOTE: to convert the type, we must have an architecture.
- let arch = CoreArchitecture::from_raw(arch);
-
let source = unsafe { *source };
let guid = unsafe { *guid };
@@ -561,9 +541,10 @@ pub unsafe extern "C" fn BNWARPContainerGetTypeWithGUID(
let Some(ty) = container.type_with_guid(&source, &guid).unwrap_or_default() else {
return std::ptr::null_mut();
};
- let function_type = to_bn_type(Some(arch), &ty);
- // NOTE: The type ref has been pre-incremented for the caller.
- unsafe { Ref::into_raw(function_type) }.handle
+
+ let arc_ty = Arc::new(ty);
+ // NOTE: Freed by BNWARPFreeTypeReference
+ Arc::into_raw(arc_ty) as *mut BNWARPType
}
#[no_mangle]
diff --git a/plugins/warp/src/plugin/ffi/file.rs b/plugins/warp/src/plugin/ffi/file.rs
index 951b5eb2..3abfe5aa 100644
--- a/plugins/warp/src/plugin/ffi/file.rs
+++ b/plugins/warp/src/plugin/ffi/file.rs
@@ -1,11 +1,43 @@
+use crate::plugin::ffi::{BNWARPFunction, BNWARPTarget, BNWARPType};
+use binaryninja::data_buffer::DataBuffer;
+use binaryninjacore_sys::BNDataBuffer;
use std::ffi::c_char;
+use std::mem::ManuallyDrop;
use std::sync::Arc;
-use warp::WarpFile;
+use warp::chunk::ChunkKind;
+use warp::{WarpFile, WarpFileHeader};
-pub type BNWARPFile = WarpFile<'static>;
+/// A [`WarpFile`] wrapper that uses reference counting to manage its chunks lifetime.
+///
+/// Used primarily when passing to the C FFI so that chunks do not need to have a complicated lifetime.
+pub struct RcWarpFile {
+ pub header: WarpFileHeader,
+ pub chunks: Vec<Arc<warp::chunk::Chunk<'static>>>,
+}
+
+impl From<WarpFile<'static>> for RcWarpFile {
+ fn from(file: WarpFile<'static>) -> Self {
+ let chunks = file.chunks.into_iter().map(|c| Arc::new(c)).collect();
+ Self {
+ header: file.header,
+ chunks,
+ }
+ }
+}
+
+impl From<&RcWarpFile> for WarpFile<'static> {
+ fn from(rc_file: &RcWarpFile) -> Self {
+ let chunks = rc_file.chunks.iter().map(|c| (**c).clone()).collect();
+ Self {
+ header: rc_file.header.clone(),
+ chunks,
+ }
+ }
+}
+
+pub type BNWARPFile = RcWarpFile;
-// TODO: At some point we may want to expose chunks directly. For now we will just enumerate all of them.
-// pub type BNWARPChunk = warp::chunk::Chunk<'static>;
+pub type BNWARPChunk = warp::chunk::Chunk<'static>;
// TODO: From bytes as well.
#[no_mangle]
@@ -20,7 +52,78 @@ pub unsafe extern "C" fn BNWARPNewFileFromPath(path: *mut c_char) -> *mut BNWARP
let Some(file) = WarpFile::from_owned_bytes(bytes) else {
return std::ptr::null_mut();
};
- Arc::into_raw(Arc::new(file)) as *mut BNWARPFile
+ let rc_file = RcWarpFile::from(file);
+ Arc::into_raw(Arc::new(rc_file)) as *mut BNWARPFile
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFileGetChunks(
+ file: *mut BNWARPFile,
+ count: *mut usize,
+) -> *mut *mut BNWARPChunk {
+ let arc_file = ManuallyDrop::new(Arc::from_raw(file));
+ *count = arc_file.chunks.len();
+ let boxed_chunks: Box<[_]> = arc_file
+ .chunks
+ .iter()
+ .map(|c| Arc::into_raw(c.clone()))
+ .collect();
+ Box::into_raw(boxed_chunks) as *mut *mut BNWARPChunk
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFileToDataBuffer(file: *mut BNWARPFile) -> *mut BNDataBuffer {
+ let arc_file = ManuallyDrop::new(Arc::from_raw(file));
+ let warp_file = WarpFile::from(arc_file.as_ref());
+ let buffer = DataBuffer::new(&warp_file.to_bytes());
+ buffer.into_raw()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPChunkGetTarget(chunk: *const BNWARPChunk) -> *mut BNWARPTarget {
+ let chunk = unsafe { &*chunk };
+ let chunk_target = chunk.header.target.clone();
+ Arc::into_raw(Arc::new(chunk_target)) as *mut BNWARPTarget
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPChunkGetFunctions(
+ chunk: *const BNWARPChunk,
+ count: *mut usize,
+) -> *mut *mut BNWARPFunction {
+ let chunk = unsafe { &*chunk };
+ match &chunk.kind {
+ ChunkKind::Signature(sc) => {
+ let boxed_funcs: Box<[_]> = sc
+ .functions()
+ .into_iter()
+ .map(|f| Arc::into_raw(Arc::new(f.clone())))
+ .collect();
+ *count = boxed_funcs.len();
+ Box::into_raw(boxed_funcs) as *mut *mut BNWARPFunction
+ }
+ ChunkKind::Type(_) => std::ptr::null_mut(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPChunkGetTypes(
+ chunk: *const BNWARPChunk,
+ count: *mut usize,
+) -> *mut *mut BNWARPType {
+ let chunk = unsafe { &*chunk };
+ match &chunk.kind {
+ ChunkKind::Signature(_) => std::ptr::null_mut(),
+ ChunkKind::Type(tc) => {
+ let boxed_types: Box<[_]> = tc
+ .types()
+ .into_iter()
+ .map(|t| Arc::into_raw(Arc::new(t.ty.clone())))
+ .collect();
+ *count = boxed_types.len();
+ Box::into_raw(boxed_types) as *mut *mut BNWARPType
+ }
+ }
}
#[no_mangle]
@@ -36,3 +139,27 @@ pub unsafe extern "C" fn BNWARPFreeFileReference(file: *mut BNWARPFile) {
}
Arc::decrement_strong_count(file);
}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewChunkReference(chunk: *mut BNWARPChunk) -> *mut BNWARPChunk {
+ Arc::increment_strong_count(chunk);
+ chunk
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeChunkReference(chunk: *mut BNWARPChunk) {
+ if chunk.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(chunk);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeChunkList(chunks: *mut *mut BNWARPChunk, count: usize) {
+ let chunks_ptr = std::ptr::slice_from_raw_parts_mut(chunks, count);
+ let chunks = unsafe { Box::from_raw(chunks_ptr) };
+ for chunk in chunks {
+ // NOTE: The chunks themselves should also be arc.
+ BNWARPFreeChunkReference(chunk);
+ }
+}
diff --git a/plugins/warp/src/plugin/ffi/function.rs b/plugins/warp/src/plugin/ffi/function.rs
index 138c0a7b..2eff9e4a 100644
--- a/plugins/warp/src/plugin/ffi/function.rs
+++ b/plugins/warp/src/plugin/ffi/function.rs
@@ -1,11 +1,11 @@
use crate::build_function;
use crate::cache::{insert_cached_function_match, try_cached_function_match};
-use crate::convert::{to_bn_symbol_at_address, to_bn_type};
-use crate::plugin::ffi::{BNWARPConstraint, BNWARPFunction, BNWARPFunctionGUID};
+use crate::convert::to_bn_symbol_at_address;
+use crate::plugin::ffi::{BNWARPConstraint, BNWARPFunction, BNWARPFunctionGUID, BNWARPType};
use binaryninja::function::Function;
use binaryninja::rc::Ref;
use binaryninja::string::BnString;
-use binaryninjacore_sys::{BNFunction, BNSymbol, BNType};
+use binaryninjacore_sys::{BNFunction, BNSymbol};
use std::ffi::c_char;
use std::mem::ManuallyDrop;
use std::sync::Arc;
@@ -111,19 +111,14 @@ pub unsafe extern "C" fn BNWARPFunctionGetSymbolName(function: *mut BNWARPFuncti
}
#[no_mangle]
-pub unsafe extern "C" fn BNWARPFunctionGetType(
- function: *mut BNWARPFunction,
- analysis_function: *mut BNFunction,
-) -> *mut BNType {
- let analysis_function = Function::from_raw(analysis_function);
+pub unsafe extern "C" fn BNWARPFunctionGetType(function: *mut BNWARPFunction) -> *mut BNWARPType {
// We do not own function so we should not drop.
let function = ManuallyDrop::new(Arc::from_raw(function));
match &function.ty {
Some(func_ty) => {
- let arch = analysis_function.arch();
- let function_type = to_bn_type(Some(arch), func_ty);
- // NOTE: The type ref has been pre-incremented for the caller.
- unsafe { Ref::into_raw(function_type) }.handle
+ let arc_func_ty = Arc::new(func_ty.clone());
+ // NOTE: Freed by BNWARPFreeTypeReference
+ Arc::into_raw(arc_func_ty) as *mut BNWARPType
}
None => std::ptr::null_mut(),
}
diff --git a/plugins/warp/src/plugin/ffi/processor.rs b/plugins/warp/src/plugin/ffi/processor.rs
new file mode 100644
index 00000000..83b47f5a
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi/processor.rs
@@ -0,0 +1,187 @@
+use crate::plugin::ffi::file::{BNWARPFile, RcWarpFile};
+use crate::processor::{
+ IncludedDataField, IncludedFunctionsField, ProcessingFileState, WarpFileProcessor,
+ WarpFileProcessorEntry,
+};
+use binaryninja::binary_view::BinaryView;
+use binaryninja::project::file::ProjectFile;
+use binaryninja::project::Project;
+use binaryninjacore_sys::{BNBinaryView, BNProject, BNProjectFile};
+use std::ffi::{c_char, CStr, CString};
+use std::mem::ManuallyDrop;
+use std::path::PathBuf;
+use std::ptr::NonNull;
+use std::sync::Arc;
+use warp::chunk::CompressionType;
+
+pub type BNWARPProcessor = WarpFileProcessor;
+
+#[repr(C)]
+pub struct BNWARPProcessorState {
+ cancelled: bool,
+ unprocessed_files_count: usize,
+ processed_files_count: usize,
+ analyzing_files: *mut *mut c_char,
+ analyzing_files_count: usize,
+ processing_files: *mut *mut c_char,
+ processing_files_count: usize,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewProcessor(
+ included_data: IncludedDataField,
+ included_functions: IncludedFunctionsField,
+ worker_count: usize,
+) -> *mut BNWARPProcessor {
+ let processor = WarpFileProcessor::new()
+ .with_file_data(included_data)
+ .with_included_functions(included_functions)
+ .with_compression_type(CompressionType::Zstd)
+ .with_entry_worker_count(worker_count);
+ Box::into_raw(Box::new(processor))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorAddPath(
+ processor: *mut BNWARPProcessor,
+ path: *const c_char,
+) {
+ let mut processor = ManuallyDrop::new(Box::from_raw(processor));
+ let path_cstr = unsafe { CStr::from_ptr(path) };
+ let path = PathBuf::from(path_cstr.to_str().unwrap());
+ // TODO: Not thread safe.
+ processor.add_entry(WarpFileProcessorEntry::Path(path));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorAddProject(
+ processor: *mut BNWARPProcessor,
+ project: *mut BNProject,
+) {
+ let mut processor = ManuallyDrop::new(Box::from_raw(processor));
+ let project = Project::from_raw(NonNull::new(project).unwrap());
+ // TODO: Not thread safe.
+ processor.add_entry(WarpFileProcessorEntry::Project(project.to_owned()));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorAddProjectFile(
+ processor: *mut BNWARPProcessor,
+ project_file: *mut BNProjectFile,
+) {
+ let mut processor = ManuallyDrop::new(Box::from_raw(processor));
+ let project_file = ProjectFile::from_raw(NonNull::new(project_file).unwrap());
+ // TODO: Not thread safe.
+ processor.add_entry(WarpFileProcessorEntry::ProjectFile(project_file.to_owned()));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorAddBinaryView(
+ processor: *mut BNWARPProcessor,
+ view: *mut BNBinaryView,
+) {
+ let mut processor = ManuallyDrop::new(Box::from_raw(processor));
+ let view = BinaryView::from_raw(view);
+ // TODO: Not thread safe.
+ processor.add_entry(WarpFileProcessorEntry::BinaryView(view.to_owned()));
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorStart(processor: *mut BNWARPProcessor) -> *mut BNWARPFile {
+ let mut processor = ManuallyDrop::new(Box::from_raw(processor));
+ // TODO: Not thread safe.
+ match processor.process_entries() {
+ Ok(file) => {
+ let rc_file = RcWarpFile::from(file);
+ Arc::into_raw(Arc::new(rc_file)) as *mut BNWARPFile
+ }
+ Err(_) => std::ptr::null_mut(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorIsCancelled(processor: *mut BNWARPProcessor) -> bool {
+ let processor = ManuallyDrop::new(Box::from_raw(processor));
+ processor
+ .state()
+ .cancelled
+ .load(std::sync::atomic::Ordering::Relaxed)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorCancel(processor: *mut BNWARPProcessor) {
+ let processor = ManuallyDrop::new(Box::from_raw(processor));
+ processor
+ .state()
+ .cancelled
+ .store(true, std::sync::atomic::Ordering::Relaxed)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPProcessorGetState(
+ processor: *mut BNWARPProcessor,
+) -> BNWARPProcessorState {
+ let processor = ManuallyDrop::new(Box::from_raw(processor));
+ let processor_state = processor.state();
+
+ let mut unprocessed_files_count = 0;
+ let mut processed_files_count = 0;
+ let mut analyzing_files = Vec::new();
+ let mut processing_files = Vec::new();
+
+ for file_state in &processor_state.files {
+ match file_state.value() {
+ ProcessingFileState::Unprocessed => unprocessed_files_count += 1,
+ ProcessingFileState::Analyzing => analyzing_files.push(file_state.key().clone()),
+ ProcessingFileState::Processing => processing_files.push(file_state.key().clone()),
+ ProcessingFileState::Processed => processed_files_count += 1,
+ }
+ }
+
+ let raw_analyzing_files: Box<[_]> = analyzing_files
+ .into_iter()
+ .map(|p| CString::new(p.to_str().unwrap()).unwrap().into_raw())
+ .collect();
+ let raw_analyzing_files_count = raw_analyzing_files.len();
+ let raw_analyzing_files_ptr = Box::into_raw(raw_analyzing_files);
+
+ let raw_processing_files: Box<[_]> = processing_files
+ .into_iter()
+ .map(|p| CString::new(p.to_str().unwrap()).unwrap().into_raw())
+ .collect();
+ let raw_processing_files_count = raw_processing_files.len();
+ let raw_processing_files_ptr = Box::into_raw(raw_processing_files);
+
+ BNWARPProcessorState {
+ cancelled: processor_state
+ .cancelled
+ .load(std::sync::atomic::Ordering::Relaxed),
+ unprocessed_files_count,
+ processed_files_count,
+ analyzing_files: raw_analyzing_files_ptr as *mut *mut c_char,
+ analyzing_files_count: raw_analyzing_files_count,
+ processing_files: raw_processing_files_ptr as *mut *mut c_char,
+ processing_files_count: raw_processing_files_count,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeProcessor(processor: *mut BNWARPProcessor) {
+ let _ = Box::from_raw(processor);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeProcessorState(state: BNWARPProcessorState) {
+ let a_files_ptr =
+ std::ptr::slice_from_raw_parts_mut(state.analyzing_files, state.analyzing_files_count);
+ let a_files_boxed = unsafe { Box::from_raw(a_files_ptr) };
+ for path in a_files_boxed.iter() {
+ let _ = CString::from_raw(*path);
+ }
+ let p_files_ptr =
+ std::ptr::slice_from_raw_parts_mut(state.processing_files, state.processing_files_count);
+ let p_files_boxed = unsafe { Box::from_raw(p_files_ptr) };
+ for path in p_files_boxed.iter() {
+ let _ = CString::from_raw(*path);
+ }
+}
diff --git a/plugins/warp/src/plugin/ffi/ty.rs b/plugins/warp/src/plugin/ffi/ty.rs
new file mode 100644
index 00000000..6a16fdab
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi/ty.rs
@@ -0,0 +1,75 @@
+use crate::convert::{from_bn_type, to_bn_type};
+use crate::plugin::ffi::BNWARPType;
+use binaryninja::architecture::CoreArchitecture;
+use binaryninja::binary_view::BinaryView;
+use binaryninja::file_metadata::FileMetadata;
+use binaryninja::rc::Ref as BnRef;
+use binaryninja::string::BnString;
+use binaryninja::types::Type;
+use binaryninjacore_sys::{BNArchitecture, BNType};
+use std::ffi::c_char;
+use std::mem::ManuallyDrop;
+use std::sync::Arc;
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetType(
+ analysis_type: *mut BNType,
+ confidence: u8,
+) -> *mut BNWARPType {
+ let analysis_type = Type::from_raw(analysis_type);
+ // TODO: This will leak a bunch of memory, but we need to remove the view requirement anyways.
+ let binary_view = BinaryView::from_data(&FileMetadata::new(), &[]);
+ let ty = from_bn_type(&binary_view, &analysis_type, confidence);
+ Arc::into_raw(Arc::new(ty)) as *mut BNWARPType
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPTypeGetName(ty: *mut BNWARPType) -> *mut c_char {
+ let ty = ManuallyDrop::new(Arc::from_raw(ty));
+ match ty.name.as_deref() {
+ Some(name) => BnString::into_raw(BnString::new(name)),
+ None => std::ptr::null_mut(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPTypeGetConfidence(ty: *mut BNWARPType) -> u8 {
+ let ty = ManuallyDrop::new(Arc::from_raw(ty));
+ ty.confidence
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPTypeGetAnalysisType(
+ arch: *mut BNArchitecture,
+ ty: *mut BNWARPType,
+) -> *mut BNType {
+ let ty = ManuallyDrop::new(Arc::from_raw(ty));
+ let analysis_ty = match arch.is_null() {
+ true => to_bn_type::<CoreArchitecture>(None, &ty),
+ false => to_bn_type(Some(CoreArchitecture::from_raw(arch)), &ty),
+ };
+ BnRef::into_raw(analysis_ty).handle
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewTypeReference(ty: *mut BNWARPType) -> *mut BNWARPType {
+ Arc::increment_strong_count(ty);
+ ty
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeTypeReference(ty: *mut BNWARPType) {
+ if ty.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(ty);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeTypeList(types: *mut *mut BNWARPType, count: usize) {
+ let types_ptr = std::ptr::slice_from_raw_parts_mut(types, count);
+ let types = unsafe { Box::from_raw(types_ptr) };
+ for ty in types {
+ unsafe { BNWARPFreeTypeReference(ty) };
+ }
+}
diff --git a/plugins/warp/src/plugin/file.rs b/plugins/warp/src/plugin/file.rs
deleted file mode 100644
index cd5fb626..00000000
--- a/plugins/warp/src/plugin/file.rs
+++ /dev/null
@@ -1,41 +0,0 @@
-use crate::report::ReportGenerator;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::Command;
-
-pub struct ShowFileReport;
-
-impl Command for ShowFileReport {
- fn action(&self, view: &BinaryView) {
- let view = view.to_owned();
- std::thread::spawn(move || {
- let Some(path) =
- binaryninja::interaction::get_open_filename_input("Select file to show", "*.warp")
- else {
- return;
- };
-
- let Ok(bytes) = std::fs::read(&path) else {
- tracing::error!("Failed to read file: {:?}", path);
- return;
- };
-
- let Some(file) = warp::WarpFile::from_bytes(&bytes) else {
- tracing::error!("Failed to parse file: {:?}", path);
- return;
- };
-
- let report_generator = ReportGenerator::new();
- if let Some(html_string) = report_generator.html_report(&file) {
- view.show_html_report(
- &format!("WARP File: {}", path.to_string_lossy()),
- html_string.as_str(),
- "",
- );
- }
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/function.rs b/plugins/warp/src/plugin/function.rs
index 36db7d55..908bd0db 100644
--- a/plugins/warp/src/plugin/function.rs
+++ b/plugins/warp/src/plugin/function.rs
@@ -1,18 +1,10 @@
-use crate::cache::{
- cached_function_guid, insert_cached_function_match, try_cached_function_guid,
- try_cached_function_match,
-};
+use crate::cache::{insert_cached_function_match, try_cached_function_match};
use crate::{
get_warp_ignore_tag_type, get_warp_include_tag_type, IGNORE_TAG_NAME, INCLUDE_TAG_NAME,
};
-use binaryninja::background_task::BackgroundTask;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::{Command, FunctionCommand};
+use binaryninja::binary_view::BinaryView;
+use binaryninja::command::FunctionCommand;
use binaryninja::function::{Function, FunctionUpdateType};
-use binaryninja::rc::Guard;
-use rayon::iter::ParallelIterator;
-use std::thread;
-use warp::signature::function::FunctionGUID;
pub struct IncludeFunction;
@@ -101,91 +93,3 @@ impl FunctionCommand for RemoveFunction {
try_cached_function_match(func).is_some()
}
}
-
-pub struct CopyFunctionGUID;
-
-impl FunctionCommand for CopyFunctionGUID {
- fn action(&self, _view: &BinaryView, func: &Function) {
- let Some(guid) = cached_function_guid(func, || func.lifted_il().ok()) else {
- tracing::error!("Could not get guid for copied function");
- return;
- };
- tracing::info!(
- "Function GUID for {:?}... {}",
- func.symbol().short_name(),
- guid
- );
- if let Ok(mut clipboard) = arboard::Clipboard::new() {
- let _ = clipboard.set_text(guid.to_string());
- }
- }
-
- fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
- true
- }
-}
-
-pub struct FindFunctionFromGUID;
-
-impl Command for FindFunctionFromGUID {
- fn action(&self, view: &BinaryView) {
- let Some(guid_str) = binaryninja::interaction::get_text_line_input(
- "Function GUID",
- "Find Function from GUID",
- ) else {
- return;
- };
-
- let Ok(searched_guid) = guid_str.parse::<FunctionGUID>() else {
- tracing::error!("Failed to parse function guid... {}", guid_str);
- return;
- };
-
- tracing::info!("Searching functions for GUID... {}", searched_guid);
- let funcs = view.functions();
- let view = view.to_owned();
- thread::spawn(move || {
- let background_task = BackgroundTask::new(
- &format!("Searching functions for GUID... {}", searched_guid),
- false,
- );
-
- // Only run this for functions which have already generated a GUID.
- let matched: Vec<Guard<Function>> = funcs
- .par_iter()
- .filter(|func| {
- try_cached_function_guid(func).is_some_and(|guid| guid == searched_guid)
- })
- .collect();
-
- if matched.is_empty() {
- tracing::info!("No matches found for GUID... {}", searched_guid);
- } else {
- for func in &matched {
- // Also navigate the user, as that is probably what they want.
- if matched.len() == 1 {
- let current_view = view.file().current_view();
- if view
- .file()
- .navigate_to(&current_view, func.start())
- .is_err()
- {
- tracing::error!(
- "Failed to navigate to found function 0x{:0x} in view {}",
- func.start(),
- current_view
- );
- }
- }
- tracing::info!("Match found at function... 0x{:0x}", func.start());
- }
- }
-
- background_task.finish();
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/project.rs b/plugins/warp/src/plugin/project.rs
deleted file mode 100644
index b6081c1f..00000000
--- a/plugins/warp/src/plugin/project.rs
+++ /dev/null
@@ -1,286 +0,0 @@
-use crate::processor::{
- new_processing_state_background_thread, CompressionTypeField, FileDataKindField,
- FileFilterField, ProcessingFileState, RequestAnalysisField, WarpFileProcessor,
-};
-use crate::report::{ReportGenerator, ReportKindField};
-use binaryninja::background_task::BackgroundTask;
-use binaryninja::command::ProjectCommand;
-use binaryninja::interaction::{Form, FormInputField};
-use binaryninja::project::folder::ProjectFolder;
-use binaryninja::project::Project;
-use binaryninja::rc::Ref;
-use binaryninja::worker_thread::{set_worker_thread_count, worker_thread_count};
-use rayon::ThreadPoolBuilder;
-use regex::Regex;
-use std::path::Path;
-use std::thread;
-use std::time::Instant;
-use warp::WarpFile;
-
-pub struct CreateSignaturesForm {
- form: Form,
-}
-
-impl CreateSignaturesForm {
- pub fn new(_project: &Project) -> CreateSignaturesForm {
- let mut form = Form::new("Create Signature File");
- form.add_field(Self::file_data_field());
- form.add_field(Self::file_filter_field());
- form.add_field(Self::generated_report_field());
- form.add_field(Self::compression_type_field());
- form.add_field(Self::save_individual_files_field());
- form.add_field(Self::skip_existing_warp_files_field());
- form.add_field(Self::request_analysis_field());
- form.add_field(Self::processing_thread_count_field());
- Self { form }
- }
-
- pub fn file_data_field() -> FormInputField {
- FileDataKindField::default().to_field()
- }
-
- pub fn file_data_kind(&self) -> FileDataKindField {
- FileDataKindField::from_form(&self.form).unwrap_or_default()
- }
-
- pub fn file_filter_field() -> FormInputField {
- FileFilterField::to_field()
- }
-
- pub fn file_filter(&self) -> Option<Result<Regex, regex::Error>> {
- FileFilterField::from_form(&self.form)
- }
-
- pub fn generated_report_field() -> FormInputField {
- ReportKindField::default().to_field()
- }
-
- pub fn generated_report_kind(&self) -> ReportKindField {
- ReportKindField::from_form(&self.form).unwrap_or_default()
- }
-
- pub fn compression_type_field() -> FormInputField {
- CompressionTypeField::default().to_field()
- }
-
- pub fn compression_type(&self) -> CompressionTypeField {
- CompressionTypeField::from_form(&self.form).unwrap_or_default()
- }
-
- pub fn save_individual_files_field() -> FormInputField {
- FormInputField::Checkbox {
- prompt: "Save individual files".to_string(),
- default: None,
- value: false,
- }
- }
-
- pub fn save_individual_files(&self) -> bool {
- let field = self.form.get_field_with_name("Save individual files");
- let field_value = field.and_then(|f| f.try_value_int()).unwrap_or(0);
- match field_value {
- 1 => true,
- _ => false,
- }
- }
-
- pub fn skip_existing_warp_files_field() -> FormInputField {
- FormInputField::Checkbox {
- prompt: "Skip existing WARP files".to_string(),
- default: Some(true),
- value: false,
- }
- }
-
- pub fn skip_existing_warp_files(&self) -> bool {
- let field = self.form.get_field_with_name("Skip existing WARP files");
- let field_value = field.and_then(|f| f.try_value_int()).unwrap_or(0);
- match field_value {
- 1 => true,
- _ => false,
- }
- }
-
- pub fn request_analysis_field() -> FormInputField {
- RequestAnalysisField::default().to_field()
- }
-
- pub fn request_analysis(&self) -> RequestAnalysisField {
- RequestAnalysisField::from_form(&self.form).unwrap_or_default()
- }
-
- pub fn processing_thread_count_field() -> FormInputField {
- let default = rayon::current_num_threads();
- FormInputField::Integer {
- prompt: "Processing threads".to_string(),
- default: Some(default as i64),
- value: 0,
- }
- }
-
- pub fn processing_thread_count(&self) -> usize {
- let field = self.form.get_field_with_name("Processing threads");
- let worker_thread_count = worker_thread_count();
- field
- .and_then(|f| f.try_value_int())
- .unwrap_or(worker_thread_count as i64)
- .abs() as usize
- }
-
- pub fn prompt(&mut self) -> bool {
- self.form.prompt()
- }
-}
-
-pub struct CreateSignatures;
-
-impl CreateSignatures {
- pub fn execute(project: Ref<Project>) {
- let mut form = CreateSignaturesForm::new(&project);
- if !form.prompt() {
- return;
- }
- let file_data_kind = form.file_data_kind();
- let report_kind = form.generated_report_kind();
- let compression_type = form.compression_type();
- let save_individual_files = form.save_individual_files();
- let skip_existing_warp_files = form.skip_existing_warp_files();
- let request_analysis = form.request_analysis();
- let processing_thread_count = form.processing_thread_count();
-
- // Save the warp file to the project.
- let save_warp_file = move |project: &Project,
- folder: Option<&ProjectFolder>,
- name: &str,
- warp_file: &WarpFile| {
- if project
- .create_file(&warp_file.to_bytes(), folder, name, "")
- .is_err()
- {
- tracing::error!("Failed to create project file!");
- }
-
- let report = ReportGenerator::new();
- if let Some(generated) = report.report(&report_kind, &warp_file) {
- let ext = report.report_extension(&report_kind).unwrap_or_default();
- let file_name = format!("{}_report.{}", name, ext);
- if project
- .create_file(&generated.into_bytes(), folder, &file_name, "Warp file")
- .is_err()
- {
- tracing::error!("Failed to create project file!");
- }
- }
- };
-
- // Optional callback for saving off the individual project files.
- let callback_project = project.clone();
- let save_individual_files_cb = move |path: &Path, file: &WarpFile| {
- if file.chunks.is_empty() {
- tracing::debug!("Skipping empty file: {}", path.display());
- return;
- }
- // The path returned will be the one on disk, so we will go and grab the project for it.
- let Some(project_file) = callback_project.file_by_path(path) else {
- tracing::error!("Failed to find project file for path: {}", path.display());
- return;
- };
- let project_file = project_file.to_owned();
- let file_name = format!("{}.warp", project_file.name());
- let project_folder = project_file.folder();
- save_warp_file(
- &callback_project,
- project_folder.as_deref(),
- &file_name,
- file,
- );
- };
-
- let mut processor = WarpFileProcessor::new()
- .with_file_data(file_data_kind)
- .with_compression_type(compression_type)
- .with_skip_warp_files(skip_existing_warp_files)
- .with_request_analysis(request_analysis == RequestAnalysisField::Yes);
-
- if save_individual_files {
- processor = processor.with_processed_file_callback(save_individual_files_cb);
- }
-
- // Construct the user-supplied file filter. This will filter files in the project only, files
- // in an archive will be considered a part of the archive file.
- if let Some(filter) = form.file_filter() {
- match filter {
- Ok(f) => {
- processor = processor.with_file_filter(f);
- }
- Err(err) => {
- tracing::error!("Failed to parse file filter: {}", err);
- tracing::error!(
- "Consider using a substring instead of a glob pattern, e.g. *.exe => exe"
- );
- return;
- }
- }
- }
-
- // This thread will show the state in a background task.
- let background_task = BackgroundTask::new("Processing started...", true);
- new_processing_state_background_thread(background_task.clone(), processor.state());
-
- let Ok(thread) = ThreadPoolBuilder::new()
- .num_threads(processing_thread_count)
- .build()
- else {
- tracing::error!("Failed to create processing thread pool!");
- return;
- };
-
- // We have to bump the number of worker threads up so that view destruction's and analysis
- // does not halt, using a multiple of three seems good. This is only temporary.
- let previous_worker_thread_count = worker_thread_count();
- let upgraded_thread_count = previous_worker_thread_count * 3;
- if upgraded_thread_count > previous_worker_thread_count {
- tracing::info!(
- "Setting worker thread count to {} for the duration of processing...",
- upgraded_thread_count
- );
- set_worker_thread_count(upgraded_thread_count);
- }
-
- let start = Instant::now();
- thread.scope(|_| match processor.process_project(&project) {
- Ok(warp_file) => {
- save_warp_file(&project, None, "generated.warp", &warp_file);
- }
- Err(e) => {
- tracing::error!("Failed to process project: {}", e);
- }
- });
-
- let processed_file_count = processor
- .state()
- .files_with_state(ProcessingFileState::Processed);
- tracing::info!(
- "Processing {} project files took: {:?}",
- processed_file_count,
- start.elapsed()
- );
- // Reset the worker thread count to the user specified; either way it will not persist.
- set_worker_thread_count(previous_worker_thread_count);
- // Tells the processing state thread to finish.
- background_task.finish();
- }
-}
-
-impl ProjectCommand for CreateSignatures {
- fn action(&self, project: &Project) {
- let project = project.to_owned();
- thread::spawn(move || {
- CreateSignatures::execute(project);
- });
- }
-
- fn valid(&self, _view: &Project) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/processor.rs b/plugins/warp/src/processor.rs
index 12403ed2..32310d3c 100644
--- a/plugins/warp/src/processor.rs
+++ b/plugins/warp/src/processor.rs
@@ -12,7 +12,7 @@ use dashmap::DashMap;
use rayon::iter::IntoParallelIterator;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rayon::prelude::ParallelSlice;
-use regex::Regex;
+use rayon::{ThreadPoolBuildError, ThreadPoolBuilder};
use serde_json::{json, Value};
use tempdir::TempDir;
use thiserror::Error;
@@ -21,14 +21,13 @@ use walkdir::WalkDir;
use binaryninja::background_task::BackgroundTask;
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
use binaryninja::function::Function as BNFunction;
-use binaryninja::interaction::{Form, FormInputField};
use binaryninja::project::file::ProjectFile;
use binaryninja::project::Project;
use binaryninja::rc::{Guard, Ref};
use crate::cache::cached_type_references;
use crate::convert::platform_to_target;
-use crate::{build_function, INCLUDE_TAG_ICON, INCLUDE_TAG_NAME};
+use crate::{build_function, INCLUDE_TAG_NAME};
use binaryninja::file_metadata::{SaveOption, SaveSettings};
use warp::chunk::{Chunk, ChunkKind, CompressionType};
use warp::r#type::chunk::TypeChunk;
@@ -76,142 +75,28 @@ pub enum ProcessingError {
#[error("Skipping file: {0}")]
SkippedFile(PathBuf),
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct FileFilterField;
-
-impl FileFilterField {
- pub fn to_field() -> FormInputField {
- FormInputField::TextLine {
- prompt: "File Filter".to_string(),
- default: None,
- value: None,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Result<Regex, regex::Error>> {
- let field = form.get_field_with_name("File Filter")?;
- let field_value = field.try_value_string()?;
- // TODO: This is pretty absurd but whatever.
- let pattern = if field_value.contains(['*', '.', '[', '(']) {
- // Assume it's a regex if it contains meta-characters.
- field_value
- } else {
- // Treat it as a substring
- format!(".*{}.*", regex::escape(&field_value))
- };
-
- Some(Regex::new(&pattern))
- }
+ #[error("Failed to create thread pool: {0}")]
+ ThreadPoolCreation(ThreadPoolBuildError),
}
+#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub enum FileDataKindField {
- Symbols,
- Signatures,
- Types,
+pub enum IncludedDataField {
+ Symbols = 0,
+ Signatures = 1,
+ Types = 2,
#[default]
- All,
-}
-
-impl FileDataKindField {
- pub fn to_field(&self) -> FormInputField {
- FormInputField::Choice {
- prompt: "File Data".to_string(),
- choices: vec![
- "Symbols".to_string(),
- "Signatures".to_string(),
- "Types".to_string(),
- "All".to_string(),
- ],
- default: Some(match self {
- Self::Symbols => 0,
- Self::Signatures => 1,
- Self::Types => 2,
- Self::All => 3,
- }),
- value: 0,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("File Data")?;
- let field_value = field.try_value_index()?;
- match field_value {
- 3 => Some(Self::All),
- 2 => Some(Self::Types),
- 1 => Some(Self::Signatures),
- 0 => Some(Self::Symbols),
- _ => None,
- }
- }
+ All = 3,
}
+#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum IncludedFunctionsField {
- Selected,
+ Selected = 0,
#[default]
- Annotated,
- All,
-}
-
-impl IncludedFunctionsField {
- pub fn to_field(&self) -> FormInputField {
- // If the user has selected any functions, change the default value of the included functions field.
- FormInputField::Choice {
- prompt: "Included Functions".to_string(),
- choices: vec![
- format!("Selected {}", INCLUDE_TAG_ICON),
- "Annotated".to_string(),
- "All".to_string(),
- ],
- default: Some(match self {
- Self::Selected => 0,
- Self::Annotated => 1,
- Self::All => 2,
- }),
- value: 0,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("Included Functions")?;
- let field_value = field.try_value_index()?;
- match field_value {
- 2 => Some(Self::All),
- 1 => Some(Self::Annotated),
- 0 => Some(Self::Selected),
- _ => None,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub enum SaveReportToDiskField {
- No,
- #[default]
- Yes,
-}
-
-impl SaveReportToDiskField {
- pub fn to_field(&self) -> FormInputField {
- FormInputField::Checkbox {
- prompt: "Save Report to Disk".to_string(),
- default: Some(true),
- value: false,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("Save Report to Disk")?;
- let field_value = field.try_value_int()?;
- match field_value {
- 1 => Some(Self::Yes),
- _ => Some(Self::No),
- }
- }
+ Annotated = 1,
+ All = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
@@ -221,64 +106,6 @@ pub enum RequestAnalysisField {
Yes,
}
-impl RequestAnalysisField {
- pub fn to_field(&self) -> FormInputField {
- FormInputField::Checkbox {
- prompt: "Request Analysis for BNDB's".to_string(),
- default: Some(true),
- value: false,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("Request Analysis for BNDB's")?;
- let field_value = field.try_value_int()?;
- match field_value {
- 1 => Some(Self::Yes),
- _ => Some(Self::No),
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub enum CompressionTypeField {
- None,
- #[default]
- Zstd,
-}
-
-impl CompressionTypeField {
- pub fn to_field(&self) -> FormInputField {
- FormInputField::Choice {
- prompt: "Compression Type".to_string(),
- choices: vec!["None".to_string(), "Zstd".to_string()],
- default: Some(match self {
- Self::None => 0,
- Self::Zstd => 1,
- }),
- value: 0,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("Compression Type")?;
- let field_value = field.try_value_index()?;
- match field_value {
- 1 => Some(Self::Zstd),
- _ => Some(Self::None),
- }
- }
-}
-
-impl From<CompressionTypeField> for CompressionType {
- fn from(field: CompressionTypeField) -> Self {
- match field {
- CompressionTypeField::None => CompressionType::None,
- CompressionTypeField::Zstd => CompressionType::Zstd,
- }
- }
-}
-
pub fn new_processing_state_background_thread(
task: Ref<BackgroundTask>,
state: Arc<ProcessingState>,
@@ -358,6 +185,15 @@ impl ProcessingState {
}
}
+/// An entry stored in the [`WarpFileProcessor`] to be processed.
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub enum WarpFileProcessorEntry {
+ Path(PathBuf),
+ Project(Ref<Project>),
+ ProjectFile(Ref<ProjectFile>),
+ BinaryView(Ref<BinaryView>),
+}
+
/// Create a new [`WarpFile`] from files, projects, and directories.
#[derive(Clone)]
pub struct WarpFileProcessor {
@@ -370,18 +206,20 @@ pub struct WarpFileProcessor {
// TODO: Databases will require regenerating LLIL in some cases, so we must support generating the LLIL.
/// The path to a folder to intake and output analysis artifacts.
cache_path: Option<PathBuf>,
- file_data: FileDataKindField,
+ file_data: IncludedDataField,
included_functions: IncludedFunctionsField,
- compression_type: CompressionTypeField,
+ compression_type: CompressionType,
processed_file_callback: Option<ProcessedFileCallback>,
- /// Regex pattern used to filter out files.
- file_filter: Option<Regex>,
- // TODO: Merge with file filter.
/// Whether to skip processing warp files.
skip_warp_files: bool,
/// Processor state, this is shareable between threads, so the processor and the consumer can
/// read / write to the state, use this if you want to show a progress indicator.
state: Arc<ProcessingState>,
+ /// The list of entries to process.
+ entries: HashSet<WarpFileProcessorEntry>,
+ /// When processing entries with [`WarpFileProcessor::process_entries`], this will
+ /// be used to specify the number of worker threads to use for processing entries.
+ entry_worker_count: Option<usize>,
}
impl WarpFileProcessor {
@@ -390,21 +228,23 @@ impl WarpFileProcessor {
analysis_settings: json!({
"analysis.linearSweep.autorun": false,
"analysis.signatureMatcher.autorun": false,
- "analysis.mode": "full",
+ "analysis.mode": "intermediate",
// Disable warp when opening views.
- "analysis.warp.guid": false,
+ "analysis.warp.guid": true,
"analysis.warp.matcher": false,
"analysis.warp.apply": false,
}),
- request_analysis: true,
+ // We expect the `build_function` call to be run, so this should be a fine default.
+ request_analysis: false,
cache_path: None,
file_data: Default::default(),
included_functions: Default::default(),
compression_type: Default::default(),
processed_file_callback: None,
- file_filter: None,
skip_warp_files: false,
state: Arc::new(ProcessingState::default()),
+ entries: HashSet::new(),
+ entry_worker_count: None,
}
}
@@ -428,7 +268,7 @@ impl WarpFileProcessor {
self
}
- pub fn with_file_data(mut self, file_data: FileDataKindField) -> Self {
+ pub fn with_file_data(mut self, file_data: IncludedDataField) -> Self {
self.file_data = file_data;
self
}
@@ -438,7 +278,7 @@ impl WarpFileProcessor {
self
}
- pub fn with_compression_type(mut self, compression_type: CompressionTypeField) -> Self {
+ pub fn with_compression_type(mut self, compression_type: CompressionType) -> Self {
self.compression_type = compression_type;
self
}
@@ -451,20 +291,13 @@ impl WarpFileProcessor {
self
}
- pub fn with_file_filter(mut self, file_filter: Regex) -> Self {
- self.file_filter = Some(file_filter);
+ pub fn with_skip_warp_files(mut self, skip: bool) -> Self {
+ self.skip_warp_files = skip;
self
}
- pub fn file_filter(&self, path: &Path) -> bool {
- match (&self.file_filter, path.to_str()) {
- (Some(filter), Some(path)) => filter.is_match(path),
- _ => true,
- }
- }
-
- pub fn with_skip_warp_files(mut self, skip: bool) -> Self {
- self.skip_warp_files = skip;
+ pub fn with_entry_worker_count(mut self, count: usize) -> Self {
+ self.entry_worker_count = Some(count);
self
}
@@ -485,7 +318,55 @@ impl WarpFileProcessor {
Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks))
}
- pub fn process(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> {
+ /// Add an entry to be processed later by [`WarpFileProcessor::process_entries`].
+ pub fn add_entry(&mut self, entry: WarpFileProcessorEntry) {
+ self.entries.insert(entry);
+ }
+
+ /// Process all entries in the processor, merging them into a single [`WarpFile`].
+ ///
+ /// The entries list will be cleared after processing to allow the processor to be reused.
+ ///
+ /// Because entries are processed in parallel, it is advised to set the worker count to a reasonable
+ /// amount to avoid excessive resource usage and to ensure optimal performance.
+ pub fn process_entries(&mut self) -> Result<WarpFile<'static>, ProcessingError> {
+ let thread_pool = match self.entry_worker_count {
+ Some(count) => ThreadPoolBuilder::new()
+ .num_threads(count)
+ .build()
+ .map_err(ProcessingError::ThreadPoolCreation)?,
+ None => ThreadPoolBuilder::new()
+ .build()
+ .map_err(ProcessingError::ThreadPoolCreation)?,
+ };
+
+ let unmerged_files: Result<Vec<_>, _> = thread_pool.install(|| {
+ self.entries
+ .par_iter()
+ .map(|e| self.process_entry(e))
+ .collect()
+ });
+ self.entries.clear();
+ self.merge_files(unmerged_files?)
+ }
+
+ pub fn process_entry(
+ &self,
+ entry: &WarpFileProcessorEntry,
+ ) -> Result<WarpFile<'static>, ProcessingError> {
+ match entry {
+ WarpFileProcessorEntry::Path(path) => self.process_path(path.clone()),
+ WarpFileProcessorEntry::Project(project) => self.process_project(&project),
+ WarpFileProcessorEntry::ProjectFile(project_file) => {
+ self.process_project_file(&project_file)
+ }
+ WarpFileProcessorEntry::BinaryView(view) => {
+ self.process_view(view.file().file_path(), &view)
+ }
+ }
+ }
+
+ pub fn process_path(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> {
let file = match path.extension() {
Some(ext) if ext == "a" || ext == "lib" || ext == "rlib" => {
self.process_archive(path.clone())
@@ -507,18 +388,7 @@ impl WarpFileProcessor {
}
pub fn process_project(&self, project: &Project) -> Result<WarpFile<'static>, ProcessingError> {
- let filter_project_file = |file: &Guard<ProjectFile>| {
- let path = project_file_path(file);
- self.file_filter(&path)
- };
-
- let files: Vec<_> = project
- .files()
- .iter()
- .filter(filter_project_file)
- .map(|f| f.to_owned())
- .collect();
-
+ let files = project.files();
// Inform the state of the new unprocessed project files.
for project_file in &files {
// NOTE: We use the on disk path here because the downstream file state uses that.
@@ -532,7 +402,7 @@ impl WarpFileProcessor {
.par_iter()
.map(|file| {
self.check_cancelled()?;
- self.process_project_file(file)
+ self.process_project_file(&file)
})
.filter_map(|res| match res {
Ok(result) => Some(Ok(result)),
@@ -686,7 +556,7 @@ impl WarpFileProcessor {
.into_iter()
.filter_map(|e| {
let path = e.ok()?.into_path();
- if path.is_file() && self.file_filter(&path) {
+ if path.is_file() {
Some(path)
} else {
None
@@ -706,7 +576,7 @@ impl WarpFileProcessor {
.inspect(|path| tracing::debug!("Processing file: {:?}", path))
.map(|path| {
self.check_cancelled()?;
- self.process(path)
+ self.process_path(path)
})
.filter_map(|res| match res {
Ok(result) => Some(Ok(result)),
@@ -800,7 +670,7 @@ impl WarpFileProcessor {
.set_file_state(path.clone(), ProcessingFileState::Processing);
let mut chunks = Vec::new();
- if self.file_data != FileDataKindField::Types {
+ if self.file_data != IncludedDataField::Types {
let mut signature_chunks = self.create_signature_chunks(view)?;
for (target, mut target_chunks) in signature_chunks.drain() {
for signature_chunk in target_chunks.drain(..) {
@@ -816,7 +686,7 @@ impl WarpFileProcessor {
}
}
- if self.file_data != FileDataKindField::Signatures {
+ if self.file_data != IncludedDataField::Signatures {
let type_chunk = self.create_type_chunk(view)?;
if type_chunk.raw_types().next().is_some() {
chunks.push(Chunk::new(
@@ -857,7 +727,8 @@ impl WarpFileProcessor {
let background_task = BackgroundTask::new(
&format!("Generating signatures... ({}/{})", 0, total_functions),
true,
- );
+ )
+ .enter();
// Create all of the "built" functions, for the chunk.
// NOTE: This does a bit of filtering to remove undesired functions, look at this if
@@ -883,7 +754,7 @@ impl WarpFileProcessor {
let built_function = build_function(
&func,
|| func.lifted_il().ok(),
- self.file_data == FileDataKindField::Symbols,
+ self.file_data == IncludedDataField::Symbols,
)?;
Some((target, built_function))
})
@@ -917,7 +788,6 @@ impl WarpFileProcessor {
})
.collect();
- background_task.finish();
chunks
}
@@ -944,7 +814,6 @@ impl Debug for WarpFileProcessor {
.field("file_data", &self.file_data)
.field("compression_type", &self.compression_type)
.field("included_functions", &self.included_functions)
- .field("file_filter", &self.file_filter)
.field("state", &self.state)
.field("cache_path", &self.cache_path)
.field("analysis_settings", &self.analysis_settings)
@@ -952,17 +821,3 @@ impl Debug for WarpFileProcessor {
.finish()
}
}
-
-fn project_file_path(file: &ProjectFile) -> PathBuf {
- // Recurse up the folders to build a string like /foldera/folderb/myfile
- let mut path = PathBuf::new();
- // Add file name
- path.push(file.name());
- // Recursively add parent folder names
- let mut current = file.folder();
- while let Some(folder) = current {
- path = PathBuf::from(folder.name()).join(path);
- current = folder.parent();
- }
- path
-}
diff --git a/plugins/warp/src/report.rs b/plugins/warp/src/report.rs
deleted file mode 100644
index 8ba9cc8b..00000000
--- a/plugins/warp/src/report.rs
+++ /dev/null
@@ -1,185 +0,0 @@
-use binaryninja::interaction::{Form, FormInputField};
-use minijinja::Environment;
-use serde::Serialize;
-use warp::chunk::{Chunk, ChunkKind};
-use warp::r#type::guid::TypeGUID;
-use warp::WarpFile;
-
-#[derive(Debug, Clone, Copy, PartialEq, Default)]
-pub enum ReportKindField {
- None,
- #[default]
- Html,
- Markdown,
- Json,
-}
-
-impl ReportKindField {
- pub fn to_field(&self) -> FormInputField {
- FormInputField::Choice {
- prompt: "Generated Report".to_string(),
- choices: vec![
- "None".to_string(),
- "HTML".to_string(),
- "Markdown".to_string(),
- "JSON".to_string(),
- ],
- default: Some(match self {
- Self::None => 0,
- Self::Html => 1,
- Self::Markdown => 2,
- Self::Json => 3,
- }),
- value: 0,
- }
- }
-
- pub fn from_form(form: &Form) -> Option<Self> {
- let field = form.get_field_with_name("Generated Report")?;
- let field_value = field.try_value_index()?;
- match field_value {
- 3 => Some(Self::Json),
- 2 => Some(Self::Markdown),
- 1 => Some(Self::Html),
- _ => Some(Self::None),
- }
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct ReportGenerator {
- environment: Environment<'static>,
-}
-
-impl ReportGenerator {
- pub fn new() -> Self {
- let mut environment = Environment::new();
- // Remove trailing lines for blocks, this is required for Markdown tables.
- environment.set_trim_blocks(true);
- minijinja_embed::load_templates!(&mut environment);
- Self { environment }
- }
-
- pub fn report(&self, kind: &ReportKindField, file: &WarpFile) -> Option<String> {
- match kind {
- ReportKindField::None => None,
- ReportKindField::Html => self.html_report(file),
- ReportKindField::Markdown => self.markdown_report(file),
- ReportKindField::Json => self.json_report(file),
- }
- }
-
- pub fn report_extension(&self, kind: &ReportKindField) -> Option<&'static str> {
- match kind {
- ReportKindField::None => None,
- ReportKindField::Html => Some("html"),
- ReportKindField::Markdown => Some("md"),
- ReportKindField::Json => Some("json"),
- }
- }
-
- pub fn html_report(&self, file: &WarpFile) -> Option<String> {
- let data = FileReportData::new(file);
- let tmpl = self.environment.get_template("file.html").ok()?;
- tmpl.render(data).ok()
- }
-
- pub fn markdown_report(&self, file: &WarpFile) -> Option<String> {
- let data = FileReportData::new(file);
- let tmpl = self.environment.get_template("file.md").ok()?;
- tmpl.render(data).ok()
- }
-
- pub fn json_report(&self, file: &WarpFile) -> Option<String> {
- let data = FileReportData::new(file);
- let tmpl = self.environment.get_template("file.json").ok()?;
- tmpl.render(data).ok()
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct FileReportData {
- pub title: String,
- // pub header: WarpFileHeader,
- pub chunks: Vec<ChunkReportData>,
-}
-
-impl FileReportData {
- pub fn new(file: &WarpFile) -> Self {
- Self {
- title: "Warp File Report".to_string(),
- // header: file.header.clone(),
- chunks: file
- .chunks
- .iter()
- .map(|chunk| ChunkReportData::new(chunk))
- .collect(),
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ChunkReportData {
- pub title: String,
- // pub header: ChunkHeader,
- pub target: String,
- pub total_item_count: usize,
- /// View into a (possible subset) of chunk items.
- pub item_view: Vec<ItemReportData>,
-}
-
-impl ChunkReportData {
- pub fn new(chunk: &Chunk) -> Self {
- // TODO: Set a limit for the number of items so we dont construct 10000000 items in the report.
- let items: Vec<_> = match &chunk.kind {
- ChunkKind::Signature(sc) => sc
- .raw_functions()
- .map(|f| ItemReportData {
- name: f.symbol().and_then(|s| s.name().map(|n| n.to_string())),
- guid: f.guid().to_string(),
- note: None,
- })
- .collect(),
- ChunkKind::Type(tc) => tc
- .raw_types()
- .map(|t| ItemReportData {
- name: t.type_().and_then(|s| s.name().map(|n| n.to_string())),
- guid: TypeGUID::from(t.guid()).to_string(),
- note: None,
- })
- .collect(),
- };
-
- let chunk_type = match &chunk.kind {
- ChunkKind::Signature(_) => "Signature".to_string(),
- ChunkKind::Type(_) => "Type".to_string(),
- };
-
- let size_in_kb = chunk.header.size as f64 / 1024.0;
- let formatted_size = format!("{:.1}kb", size_in_kb);
-
- // For the target show the platform, or the architecture if available.
- let target = chunk
- .header
- .target
- .platform
- .clone()
- .or_else(|| chunk.header.target.architecture.clone())
- .unwrap_or_else(|| "None".to_string());
-
- Self {
- title: format!("{} Chunk ({})", chunk_type, formatted_size),
- target,
- // header: chunk.header.clone(),
- total_item_count: items.len(),
- item_view: items,
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize)]
-pub struct ItemReportData {
- pub guid: String,
- pub name: Option<String>,
- pub note: Option<String>,
-}
diff --git a/plugins/warp/src/templates/file.html b/plugins/warp/src/templates/file.html
deleted file mode 100644
index 7ea465ef..00000000
--- a/plugins/warp/src/templates/file.html
+++ /dev/null
@@ -1,37 +0,0 @@
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>{{ title }}</title>
-</head>
-<body>
-<h1>{{ title }}</h1>
-
-{% for chunk in chunks %}
-<section>
- <h2>{{ chunk.title }}</h2>
- <p>Target: {{ chunk.target }}</p>
- <p>Total items: {{ chunk.total_item_count }}</p>
-
- <table>
- <thead>
- <tr>
- <th>GUID</th>
- <th>Name</th>
- <th>Note</th>
- </tr>
- </thead>
- <tbody>
- {% for item in chunk.item_view %}
- <tr>
- <td>{{ item.guid }}</td>
- <td>{{ item.name or 'N/A' }}</td>
- <td>{{ item.note or 'N/A' }}</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
-</section>
-{% endfor %}
-</body>
-</html> \ No newline at end of file
diff --git a/plugins/warp/src/templates/file.json b/plugins/warp/src/templates/file.json
deleted file mode 100644
index 764d35cd..00000000
--- a/plugins/warp/src/templates/file.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "title": "{{ title }}",
- "chunks": [
- {% for chunk in chunks %}
- {
- "title": "{{ chunk.title }}",
- "target": "{{ chunk.target }}",
- "total_item_count": {{ chunk.total_item_count }},
- "item_view": [
- {% for item in chunk.item_view %}
- {
- "guid": "{{ item.guid }}",
- "name": "{{ item.name or 'N/A' }}",
- "note": "{{ item.note or 'N/A' }}"
- }{% if not loop.last %},{% endif %}
- {% endfor %}
- ]
- }{% if not loop.last %},{% endif %}
- {% endfor %}
- ]
-} \ No newline at end of file
diff --git a/plugins/warp/src/templates/file.md b/plugins/warp/src/templates/file.md
deleted file mode 100644
index 433cb8e9..00000000
--- a/plugins/warp/src/templates/file.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# {{ title }}
-
-{% for chunk in chunks %}
-## {{ chunk.title }}
-
-Target: {{ chunk.target }}
-
-Total items: {{ chunk.total_item_count }}
-
-| GUID | Name | Note |
-|--------------|--------------|--------------|
-{% for item in chunk.item_view -%}
-| {{ item.guid }} | {{ item.name or 'N/A' }} | {{ item.note or 'N/A' }} |
-{% endfor %}
-{% endfor %}