summaryrefslogtreecommitdiff
path: root/plugins/warp/src
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/warp/src')
-rw-r--r--plugins/warp/src/bin/sigem.rs269
-rw-r--r--plugins/warp/src/cache.rs413
-rw-r--r--plugins/warp/src/cache/container.rs28
-rw-r--r--plugins/warp/src/cache/function.rs28
-rw-r--r--plugins/warp/src/cache/guid.rs193
-rw-r--r--plugins/warp/src/cache/type_reference.rs105
-rw-r--r--plugins/warp/src/container.rs266
-rw-r--r--plugins/warp/src/container/disk.rs402
-rw-r--r--plugins/warp/src/container/memory.rs307
-rw-r--r--plugins/warp/src/container/network.rs13
-rw-r--r--plugins/warp/src/convert.rs702
-rw-r--r--plugins/warp/src/convert/symbol.rs91
-rw-r--r--plugins/warp/src/convert/types.rs516
-rw-r--r--plugins/warp/src/lib.rs266
-rw-r--r--plugins/warp/src/matcher.rs557
-rw-r--r--plugins/warp/src/plugin.rs279
-rw-r--r--plugins/warp/src/plugin/add.rs72
-rw-r--r--plugins/warp/src/plugin/copy.rs29
-rw-r--r--plugins/warp/src/plugin/create.rs284
-rw-r--r--plugins/warp/src/plugin/debug.rs48
-rw-r--r--plugins/warp/src/plugin/ffi.rs205
-rw-r--r--plugins/warp/src/plugin/ffi/container.rs412
-rw-r--r--plugins/warp/src/plugin/ffi/function.rs228
-rw-r--r--plugins/warp/src/plugin/file.rs41
-rw-r--r--plugins/warp/src/plugin/find.rs57
-rw-r--r--plugins/warp/src/plugin/function.rs133
-rw-r--r--plugins/warp/src/plugin/load.rs182
-rw-r--r--plugins/warp/src/plugin/project.rs132
-rw-r--r--plugins/warp/src/plugin/render_layer.rs100
-rw-r--r--plugins/warp/src/plugin/settings.rs129
-rw-r--r--plugins/warp/src/plugin/types.rs57
-rw-r--r--plugins/warp/src/plugin/workflow.rs188
-rw-r--r--plugins/warp/src/processor.rs829
-rw-r--r--plugins/warp/src/report.rs185
-rw-r--r--plugins/warp/src/snapshots/warp_ninja__tests__snapshot__ctype.snap126
-rw-r--r--plugins/warp/src/snapshots/warp_ninja__tests__snapshot__fptostr.snap9
-rw-r--r--plugins/warp/src/snapshots/warp_ninja__tests__snapshot__mbslen.snap39
-rw-r--r--plugins/warp/src/snapshots/warp_ninja__tests__snapshot__memicmp.snap9
-rw-r--r--plugins/warp/src/snapshots/warp_ninja__tests__snapshot__strnicm.snap9
-rw-r--r--plugins/warp/src/snapshots/warp_ninja__tests__snapshot__wctype.snap117
-rw-r--r--plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atof.snap390
-rw-r--r--plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atoldbl.snap192
-rw-r--r--plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atox.snap186
-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
46 files changed, 5485 insertions, 3411 deletions
diff --git a/plugins/warp/src/bin/sigem.rs b/plugins/warp/src/bin/sigem.rs
deleted file mode 100644
index a50b4ae2..00000000
--- a/plugins/warp/src/bin/sigem.rs
+++ /dev/null
@@ -1,269 +0,0 @@
-use std::collections::HashSet;
-use std::fs::File;
-use std::io::Read;
-use std::path::{Path, PathBuf};
-
-use ar::Archive;
-use clap::{arg, Parser};
-use rayon::prelude::*;
-
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::function::Function as BNFunction;
-use binaryninja::rc::Guard as BNGuard;
-use binaryninja::settings::Settings;
-use serde_json::{json, Value};
-use walkdir::WalkDir;
-use warp::signature::Data;
-use warp_ninja::cache::{cached_type_references, register_cache_destructor};
-
-#[derive(Parser, Debug)]
-#[command(about, long_about)]
-/// A simple CLI utility to generate WARP signature files headlessly using Binary Ninja.
-///
-/// NOTE: This requires a headless compatible Binary Ninja, make sure it's in your path.
-struct Args {
- /// Path to create signatures from, this can be:
- /// - A binary (that can be opened with Binary Ninja)
- /// - A directory (all files will be merged)
- /// - An archive (with ext: a, lib, rlib)
- /// - A BNDB
- /// - A Signature file (sbin)
- #[arg(index = 1, verbatim_doc_comment)]
- path: PathBuf,
-
- /// The signature output file
- ///
- /// NOTE: If not specified the output will be the input path with the sbin extension
- /// as an example `mylib.a` will output `mylib.sbin`.
- #[arg(index = 2)]
- output: Option<PathBuf>,
-
- /// Should we overwrite output file
- ///
- /// NOTE: If the file exists we will exit early to prevent wasted effort.
- #[arg(short, long)]
- overwrite: Option<bool>,
-
- /// The external debug information file to use
- #[arg(short, long)]
- debug_info: Option<PathBuf>,
- // TODO: Add a file filter and default to filter out files starting with "."
-}
-
-fn default_settings(bn_settings: &Settings) -> Value {
- // TODO: Make these settings configurable through the CLI
- let mut settings = json!({
- "analysis.linearSweep.autorun": false,
- "analysis.signatureMatcher.autorun": false,
- "analysis.mode": "full",
- // The reason we need to do this is a little unfortunate.
- // Basically some of the COFF's have really low image bases that confuses
- // Analysis and also our basic block GUID when a constant value points to a low address section.
- // TODO: This might not exist, we should set this based on the view.
- "loader.imageBase": 0x1000000,
- });
-
- // If WARP is enabled we must turn it off to prevent matching on other stuff.
- if bn_settings.contains("analysis.warp.matcher") {
- settings["analysis.warp.matcher"] = json!(false);
- settings["analysis.warp.guid"] = json!(false);
- }
-
- settings
-}
-
-fn main() {
- let args = Args::parse();
- env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
-
- // TODO: After analysis finishes for a file we should save off the bndb to another directory called the bndb cache
- // TODO: This cache should be used before opening a file for first analysis.
-
- // TODO: We should resolve the path to something sensible in cases where user is passing CWD.
- // If no output file was given, just prepend binary with extension sbin
- let output_file = args
- .output
- .unwrap_or(args.path.to_owned())
- .with_extension("sbin");
-
- if output_file.exists() && !args.overwrite.unwrap_or(false) {
- log::info!("Output file already exists, skipping... {:?}", output_file);
- return;
- }
-
- log::debug!("Starting Binary Ninja session...");
- let _headless_session =
- binaryninja::headless::Session::new().expect("Failed to initialize session");
-
- // Adjust the amount of worker threads so that we can actually free BinaryViews.
- let worker_count = rayon::current_num_threads() * 4;
- log::debug!("Adjusting Binary Ninja worker count to {}...", worker_count);
- binaryninja::worker_thread::set_worker_thread_count(worker_count);
-
- // Make sure caches are flushed when the views get destructed.
- register_cache_destructor();
-
- let bn_settings = Settings::new();
- let settings = default_settings(&bn_settings);
-
- log::info!("Creating functions for {:?}...", args.path);
- let start = std::time::Instant::now();
- let data = data_from_file(&settings, &args.path)
- .expect("Failed to read data, check your license and Binary Ninja version!");
- log::info!("Functions created in {:?}", start.elapsed());
-
- // TODO: Add a way to override the symbol type to make it a different function symbol.
- // TODO: Right now the consumers must dictate that.
- // TODO: The binja_warp consumer sets this to library function fwiw
-
- if !data.functions.is_empty() {
- std::fs::write(&output_file, data.to_bytes()).expect("Failed to write functions to file");
- log::info!(
- "{} functions written to {:?}...",
- data.functions.len(),
- output_file
- );
- } else {
- log::warn!("No functions found for binary {:?}...", args.path);
- }
-}
-
-fn data_from_view(view: &BinaryView) -> Data {
- let mut data = Data::default();
- let is_function_named = |f: &BNGuard<BNFunction>| {
- !f.symbol().short_name().to_string_lossy().contains("sub_") || f.has_user_annotations()
- };
-
- data.functions = view
- .functions()
- .iter()
- .filter(is_function_named)
- .filter_map(|f| {
- let llil = f.low_level_il().ok()?;
- Some(warp_ninja::cache::cached_function(&f, &llil))
- })
- .collect::<Vec<_>>();
-
- if let Some(ref_ty_cache) = cached_type_references(view) {
- let referenced_types = ref_ty_cache
- .cache
- .iter()
- .filter_map(|t| t.to_owned())
- .collect::<Vec<_>>();
-
- data.types.extend(referenced_types);
- }
-
- data
-}
-
-fn data_from_archive<R: Read>(settings: &Value, mut archive: Archive<R>) -> Option<Data> {
- // TODO: I feel like this is a hack...
- let temp_dir = tempdir::TempDir::new("tmp_archive").ok()?;
- // Iterate through the entries in the ar file and make a temp dir with them
- let mut entry_files: HashSet<PathBuf> = HashSet::new();
- while let Some(entry) = archive.next_entry() {
- match entry {
- Ok(mut entry) => {
- let name = String::from_utf8_lossy(entry.header().identifier()).to_string();
- // Write entry data to a temp directory
- let output_path = temp_dir.path().join(&name);
- if !entry_files.contains(&output_path) {
- let mut output_file =
- File::create(&output_path).expect("Failed to create entry file");
- std::io::copy(&mut entry, &mut output_file).expect("Failed to read entry data");
- entry_files.insert(output_path);
- } else {
- log::debug!("Skipping already inserted entry: {}", name);
- }
- }
- Err(e) => {
- log::error!("Failed to read archive entry: {}", e);
- }
- }
- }
-
- // Create the data.
- let entry_data = entry_files
- .into_par_iter()
- .filter_map(|path| {
- log::debug!("Creating data for ENTRY {:?}...", path);
- data_from_file(settings, &path)
- })
- .collect::<Vec<_>>();
-
- Some(Data::merge(entry_data))
-}
-
-fn data_from_directory(settings: &Value, dir: PathBuf) -> Option<Data> {
- let files = WalkDir::new(dir)
- .into_iter()
- .filter_map(|e| {
- let path = e.ok()?.into_path();
- if path.is_file() {
- Some(path)
- } else {
- None
- }
- })
- .collect::<Vec<_>>();
-
- let unmerged_data = files
- .into_par_iter()
- .filter_map(|path| {
- log::info!("Creating data for FILE {:?}...", path);
- data_from_file(settings, &path)
- })
- .collect::<Vec<_>>();
-
- if !unmerged_data.is_empty() {
- Some(Data::merge(unmerged_data))
- } else {
- None
- }
-}
-
-fn data_from_file(settings: &Value, path: &Path) -> Option<Data> {
- match path.extension() {
- Some(ext) if ext == "a" || ext == "lib" || ext == "rlib" => {
- let archive_file = File::open(path).expect("Failed to open archive file");
- let archive = Archive::new(archive_file);
- data_from_archive(settings, archive)
- }
- Some(ext) if ext == "sbin" => {
- let contents = std::fs::read(path).ok()?;
- Data::from_bytes(&contents)
- }
- _ if path.is_dir() => data_from_directory(settings, path.into()),
- _ => {
- let path_str = path.to_str().unwrap();
- let view = binaryninja::load_with_options(path_str, true, Some(settings.to_string()))?;
- let data = data_from_view(&view);
- view.file().close();
- Some(data)
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- #[test]
- fn test_data_from_file() {
- env_logger::init();
- // TODO: Store oracles here to get more out of this test.
- let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap();
- let _headless_session =
- binaryninja::headless::Session::new().expect("Failed to initialize session");
- let bn_settings = Settings::new();
- let settings = default_settings(&bn_settings);
- for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") {
- let entry = entry.expect("Failed to read directory entry");
- let path = entry.path();
- if path.is_file() {
- let result = data_from_file(&settings, &path);
- assert!(result.is_some());
- }
- }
- }
-}
diff --git a/plugins/warp/src/cache.rs b/plugins/warp/src/cache.rs
index cc8dfded..09ad1444 100644
--- a/plugins/warp/src/cache.rs
+++ b/plugins/warp/src/cache.rs
@@ -1,29 +1,18 @@
-use crate::convert::{from_bn_symbol, from_bn_type_internal};
-use crate::{build_function, function_guid};
+pub mod container;
+pub mod function;
+pub mod guid;
+pub mod type_reference;
+
+pub use function::*;
+pub use guid::*;
+pub use type_reference::*;
+
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::confidence::MAX_CONFIDENCE;
use binaryninja::function::Function as BNFunction;
-use binaryninja::low_level_il::function::{FunctionMutability, LowLevelILFunction, NonSSA};
-use binaryninja::low_level_il::LowLevelILRegularFunction;
use binaryninja::rc::Guard;
use binaryninja::rc::Ref as BNRef;
-use binaryninja::symbol::Symbol as BNSymbol;
-use binaryninja::types::NamedTypeReference as BNNamedTypeReference;
use binaryninja::ObjectDestructor;
-use dashmap::mapref::one::Ref;
-use dashmap::DashMap;
-use std::collections::HashSet;
use std::hash::{DefaultHasher, Hash, Hasher};
-use std::sync::OnceLock;
-use warp::r#type::ComputedType;
-use warp::signature::function::constraints::FunctionConstraint;
-use warp::signature::function::{Function, FunctionGUID};
-
-pub static MATCHED_FUNCTION_CACHE: OnceLock<DashMap<ViewID, MatchedFunctionCache>> =
- OnceLock::new();
-pub static FUNCTION_CACHE: OnceLock<DashMap<ViewID, FunctionCache>> = OnceLock::new();
-pub static GUID_CACHE: OnceLock<DashMap<ViewID, GUIDCache>> = OnceLock::new();
-pub static TYPE_REF_CACHE: OnceLock<DashMap<ViewID, TypeRefCache>> = OnceLock::new();
pub fn register_cache_destructor() {
pub static mut CACHE_DESTRUCTOR: CacheDestructor = CacheDestructor;
@@ -34,351 +23,6 @@ pub fn register_cache_destructor() {
};
}
-pub fn cached_function_match<F>(function: &BNFunction, f: F) -> Option<Function>
-where
- F: Fn() -> Option<Function>,
-{
- let view = function.view();
- let view_id = ViewID::from(view.as_ref());
- let function_id = FunctionID::from(function);
- let function_cache = MATCHED_FUNCTION_CACHE.get_or_init(Default::default);
- match function_cache.get(&view_id) {
- Some(cache) => cache.get_or_insert(&function_id, f).to_owned(),
- None => {
- let cache = MatchedFunctionCache::default();
- let matched = cache.get_or_insert(&function_id, f).to_owned();
- function_cache.insert(view_id, cache);
- matched
- }
- }
-}
-
-pub fn try_cached_function_match(function: &BNFunction) -> Option<Function> {
- let view = function.view();
- let view_id = ViewID::from(view);
- let function_id = FunctionID::from(function);
- let function_cache = MATCHED_FUNCTION_CACHE.get_or_init(Default::default);
- function_cache
- .get(&view_id)?
- .get(&function_id)?
- .value()
- .to_owned()
-}
-
-pub fn cached_function(function: &BNFunction, llil: &LowLevelILRegularFunction) -> Function {
- let view = function.view();
- let view_id = ViewID::from(view.as_ref());
- let function_cache = FUNCTION_CACHE.get_or_init(Default::default);
- match function_cache.get(&view_id) {
- Some(cache) => cache.function(function, llil),
- None => {
- let cache = FunctionCache::default();
- let function = cache.function(function, llil);
- function_cache.insert(view_id, cache);
- function
- }
- }
-}
-
-pub fn cached_call_site_constraints(function: &BNFunction) -> HashSet<FunctionConstraint> {
- let view = function.view();
- let view_id = ViewID::from(view);
- let guid_cache = GUID_CACHE.get_or_init(Default::default);
- match guid_cache.get(&view_id) {
- Some(cache) => cache.call_site_constraints(function),
- None => {
- let cache = GUIDCache::default();
- let constraints = cache.call_site_constraints(function);
- guid_cache.insert(view_id, cache);
- constraints
- }
- }
-}
-
-pub fn cached_adjacency_constraints<F>(
- function: &BNFunction,
- filter: F,
-) -> HashSet<FunctionConstraint>
-where
- F: Fn(&BNFunction) -> bool,
-{
- let view = function.view();
- let view_id = ViewID::from(view);
- let guid_cache = GUID_CACHE.get_or_init(Default::default);
- match guid_cache.get(&view_id) {
- Some(cache) => cache.adjacency_constraints(function, filter),
- None => {
- let cache = GUIDCache::default();
- let constraints = cache.adjacency_constraints(function, filter);
- guid_cache.insert(view_id, cache);
- constraints
- }
- }
-}
-
-pub fn cached_function_guid<M: FunctionMutability>(
- function: &BNFunction,
- llil: &LowLevelILFunction<M, NonSSA>,
-) -> FunctionGUID {
- let view = function.view();
- let view_id = ViewID::from(view);
- let guid_cache = GUID_CACHE.get_or_init(Default::default);
- match guid_cache.get(&view_id) {
- Some(cache) => cache.function_guid(function, llil),
- None => {
- let cache = GUIDCache::default();
- let guid = cache.function_guid(function, llil);
- guid_cache.insert(view_id, cache);
- guid
- }
- }
-}
-
-pub fn try_cached_function_guid(function: &BNFunction) -> Option<FunctionGUID> {
- let view = function.view();
- let view_id = ViewID::from(view);
- let guid_cache = GUID_CACHE.get_or_init(Default::default);
- guid_cache.get(&view_id)?.try_function_guid(function)
-}
-
-pub fn cached_type_reference(
- view: &BinaryView,
- visited_refs: &mut HashSet<TypeRefID>,
- type_ref: &BNNamedTypeReference,
-) -> Option<ComputedType> {
- let view_id = ViewID::from(view);
- let type_ref_cache = TYPE_REF_CACHE.get_or_init(Default::default);
- match type_ref_cache.get(&view_id) {
- Some(cache) => cache.cached_type_reference(view, visited_refs, type_ref),
- None => {
- let cache = TypeRefCache::default();
- let ntr = cache.cached_type_reference(view, visited_refs, type_ref);
- type_ref_cache.insert(view_id, cache);
- ntr
- }
- }
-}
-
-pub fn cached_type_references(view: &BinaryView) -> Option<Ref<ViewID, TypeRefCache>> {
- let view_id = ViewID::from(view);
- let type_ref_cache = TYPE_REF_CACHE.get_or_init(Default::default);
- type_ref_cache.get(&view_id)
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct MatchedFunctionCache {
- pub cache: DashMap<FunctionID, Option<Function>>,
-}
-
-impl MatchedFunctionCache {
- pub fn get_or_insert<F>(
- &self,
- function_id: &FunctionID,
- f: F,
- ) -> Ref<'_, FunctionID, Option<Function>>
- where
- F: FnOnce() -> Option<Function>,
- {
- self.cache.get(function_id).unwrap_or_else(|| {
- self.cache.insert(*function_id, f());
- self.cache.get(function_id).unwrap()
- })
- }
-
- pub fn get(&self, function_id: &FunctionID) -> Option<Ref<'_, FunctionID, Option<Function>>> {
- self.cache.get(function_id)
- }
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct FunctionCache {
- pub cache: DashMap<FunctionID, Function>,
-}
-
-impl FunctionCache {
- pub fn function(&self, function: &BNFunction, llil: &LowLevelILRegularFunction) -> Function {
- let function_id = FunctionID::from(function);
- match self.cache.get(&function_id) {
- Some(function) => function.value().to_owned(),
- None => {
- let function = build_function(function, llil);
- self.cache.insert(function_id, function.clone());
- function
- }
- }
- }
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct GUIDCache {
- pub cache: DashMap<FunctionID, FunctionGUID>,
-}
-
-impl GUIDCache {
- pub fn call_site_constraints(&self, function: &BNFunction) -> HashSet<FunctionConstraint> {
- let view = function.view();
- let func_id = FunctionID::from(function);
- let func_start = function.start();
- let func_platform = function.platform();
- let mut constraints = HashSet::new();
- for call_site in &function.call_sites() {
- for cs_ref_addr in view.code_refs_from_addr(call_site.address, Some(function)) {
- match view.function_at(&func_platform, cs_ref_addr) {
- Some(cs_ref_func) => {
- // Call site is a function, constrain on it.
- let cs_ref_func_id = FunctionID::from(cs_ref_func.as_ref());
- if cs_ref_func_id != func_id {
- let call_site_offset: i64 =
- call_site.address.wrapping_sub(func_start) as i64;
- // TODO: If the function is thunk we should also insert the called function.
- constraints
- .insert(self.function_constraint(&cs_ref_func, call_site_offset));
- }
- }
- None => {
- // We could be dealing with an extern symbol, get the symbol as a constraint.
- let call_site_offset: i64 =
- call_site.address.wrapping_sub(func_start) as i64;
- if let Some(call_site_sym) = view.symbol_by_address(cs_ref_addr) {
- constraints.insert(
- self.function_constraint_from_symbol(
- &call_site_sym,
- call_site_offset,
- ),
- );
- }
- }
- }
- }
- }
- constraints
- }
-
- pub fn adjacency_constraints<F>(
- &self,
- function: &BNFunction,
- filter: F,
- ) -> HashSet<FunctionConstraint>
- where
- F: Fn(&BNFunction) -> bool,
- {
- let view = function.view();
- let func_id = FunctionID::from(function);
- let func_start = function.start();
- let mut constraints = HashSet::new();
-
- let mut func_addr_constraint = |func_start_addr| {
- // NOTE: We could potentially have dozens of functions all at the same start address.
- for curr_func in &view.functions_at(func_start_addr) {
- let curr_func_id = FunctionID::from(curr_func.as_ref());
- if curr_func_id != func_id && filter(curr_func.as_ref()) {
- // NOTE: For this to work the GUID has to have already been cached. If not it will just be the symbol.
- // Function adjacent to another function, constrain on the pattern.
- let curr_addr_offset = (func_start_addr as i64) - func_start as i64;
- constraints.insert(self.function_constraint(&curr_func, curr_addr_offset));
- }
- }
- };
-
- let mut before_func_start = func_start;
- for _ in 0..2 {
- before_func_start = view.function_start_before(before_func_start);
- func_addr_constraint(before_func_start);
- }
-
- let mut after_func_start = func_start;
- for _ in 0..2 {
- after_func_start = view.function_start_after(after_func_start);
- func_addr_constraint(after_func_start);
- }
-
- constraints
- }
-
- /// Construct a function constraint, must pass the offset at which it is located.
- pub fn function_constraint(&self, function: &BNFunction, offset: i64) -> FunctionConstraint {
- let guid = self.try_function_guid(function);
- let symbol = from_bn_symbol(&function.symbol());
- FunctionConstraint {
- guid,
- symbol: Some(symbol),
- offset,
- }
- }
-
- /// Construct a function constraint from a symbol, typically used for extern function call sites, must pass the offset at which it is located.
- pub fn function_constraint_from_symbol(
- &self,
- symbol: &BNSymbol,
- offset: i64,
- ) -> FunctionConstraint {
- let symbol = from_bn_symbol(symbol);
- FunctionConstraint {
- guid: None,
- symbol: Some(symbol),
- offset,
- }
- }
-
- pub fn function_guid<M: FunctionMutability>(
- &self,
- function: &BNFunction,
- llil: &LowLevelILFunction<M, NonSSA>,
- ) -> FunctionGUID {
- let function_id = FunctionID::from(function);
- match self.cache.get(&function_id) {
- Some(function_guid) => function_guid.value().to_owned(),
- None => {
- let function_guid = function_guid(function, llil);
- self.cache.insert(function_id, function_guid);
- function_guid
- }
- }
- }
-
- pub fn try_function_guid(&self, function: &BNFunction) -> Option<FunctionGUID> {
- let function_id = FunctionID::from(function);
- self.cache
- .get(&function_id)
- .map(|function_guid| function_guid.value().to_owned())
- }
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct TypeRefCache {
- pub cache: DashMap<TypeRefID, Option<ComputedType>>,
-}
-
-impl TypeRefCache {
- /// NOTE: No self-referential type must be used on this function.
- pub fn cached_type_reference(
- &self,
- view: &BinaryView,
- visited_refs: &mut HashSet<TypeRefID>,
- type_ref: &BNNamedTypeReference,
- ) -> Option<ComputedType> {
- let ntr_id = TypeRefID::from(type_ref);
- match self.cache.get(&ntr_id) {
- Some(cache) => cache.to_owned(),
- None => match type_ref.target(view) {
- Some(raw_ty) => {
- let computed_ty = ComputedType::new(from_bn_type_internal(
- view,
- visited_refs,
- &raw_ty,
- MAX_CONFIDENCE,
- ));
- self.cache
- .entry(ntr_id)
- .insert(Some(computed_ty))
- .to_owned()
- }
- None => self.cache.entry(ntr_id).insert(None).to_owned(),
- },
- }
- }
-}
-
/// A unique view ID, used for caching.
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ViewID(u64);
@@ -430,48 +74,11 @@ impl From<Guard<'_, BNFunction>> for FunctionID {
}
}
-/// A unique named type reference ID, used for caching.
-#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
-pub struct TypeRefID(u64);
-
-impl From<&BNNamedTypeReference> for TypeRefID {
- fn from(value: &BNNamedTypeReference) -> Self {
- let mut hasher = DefaultHasher::new();
- hasher.write(value.id().as_bytes());
- Self(hasher.finish())
- }
-}
-
-impl From<BNRef<BNNamedTypeReference>> for TypeRefID {
- fn from(value: BNRef<BNNamedTypeReference>) -> Self {
- Self::from(value.as_ref())
- }
-}
-
-impl From<Guard<'_, BNNamedTypeReference>> for TypeRefID {
- fn from(value: Guard<'_, BNNamedTypeReference>) -> Self {
- Self::from(value.as_ref())
- }
-}
-
pub struct CacheDestructor;
impl ObjectDestructor for CacheDestructor {
fn destruct_view(&self, view: &BinaryView) {
- // Clear caches as the view is no longer alive.
- let view_id = ViewID::from(view);
- if let Some(cache) = MATCHED_FUNCTION_CACHE.get() {
- cache.remove(&view_id);
- }
- if let Some(cache) = FUNCTION_CACHE.get() {
- cache.remove(&view_id);
- }
- if let Some(cache) = GUID_CACHE.get() {
- cache.remove(&view_id);
- }
- if let Some(cache) = TYPE_REF_CACHE.get() {
- cache.remove(&view_id);
- }
+ clear_type_ref_cache(view);
log::debug!("Removed WARP caches for {:?}", view.file().filename());
}
}
diff --git a/plugins/warp/src/cache/container.rs b/plugins/warp/src/cache/container.rs
new file mode 100644
index 00000000..59b31bad
--- /dev/null
+++ b/plugins/warp/src/cache/container.rs
@@ -0,0 +1,28 @@
+use crate::container::Container;
+use dashmap::DashMap;
+use std::ops::Deref;
+use std::sync::{Arc, OnceLock, RwLock};
+
+pub static CONTAINER_CACHE: OnceLock<DashMap<String, Arc<RwLock<Box<dyn Container>>>>> =
+ OnceLock::new();
+
+pub fn for_cached_containers(f: impl Fn(&dyn Container)) {
+ let containers_cache = CONTAINER_CACHE.get_or_init(Default::default);
+ for container in containers_cache.iter() {
+ if let Ok(guarded_container) = container.read() {
+ f(guarded_container.deref().as_ref());
+ }
+ }
+}
+
+// TODO: The static lifetime here is a little wierd... (we need it to Box)
+pub fn add_cached_container(container: impl Container + 'static) {
+ let containers_cache = CONTAINER_CACHE.get_or_init(Default::default);
+ let container_name = container.to_string();
+ containers_cache.insert(container_name, Arc::new(RwLock::new(Box::new(container))));
+}
+
+pub fn cached_containers() -> Vec<Arc<RwLock<Box<dyn Container>>>> {
+ let containers_cache = CONTAINER_CACHE.get_or_init(Default::default);
+ containers_cache.iter().map(|c| c.clone()).collect()
+}
diff --git a/plugins/warp/src/cache/function.rs b/plugins/warp/src/cache/function.rs
new file mode 100644
index 00000000..2fef05fd
--- /dev/null
+++ b/plugins/warp/src/cache/function.rs
@@ -0,0 +1,28 @@
+use binaryninja::function::{Function as BNFunction, FunctionUpdateType};
+use warp::signature::function::Function;
+
+/// Inserts a function match into the cache.
+///
+/// IMPORTANT: This will mark the function as needing updates, if you intend to fill in functions with
+/// no match (i.e. `None`), then you must change this function to prevent marking that as needing updates.
+/// However, it's perfectly valid to remove a match and need to update the function still, so be careful.
+pub fn insert_cached_function_match(function: &BNFunction, matched_function: Option<Function>) {
+ // NOTE: If we expect to run match_function multiple times on a function, we should move this elsewhere.
+ // Mark the function as needing updates so that reanalysis occurs on the function, and we apply the match.
+ function.mark_updates_required(FunctionUpdateType::FullAutoFunctionUpdate);
+ match matched_function {
+ Some(matched_function) => {
+ function.store_metadata("warp_matched_function", &matched_function.to_bytes(), false);
+ }
+ None => {
+ function.remove_metadata("warp_matched_function");
+ }
+ }
+}
+
+// TODO: This does allocations, and for every reanalysis.
+pub fn try_cached_function_match(function: &BNFunction) -> Option<Function> {
+ let metadata = function.query_metadata("warp_matched_function")?;
+ let raw_metadata = metadata.get_raw()?;
+ Function::from_bytes(&raw_metadata)
+}
diff --git a/plugins/warp/src/cache/guid.rs b/plugins/warp/src/cache/guid.rs
new file mode 100644
index 00000000..f1788a33
--- /dev/null
+++ b/plugins/warp/src/cache/guid.rs
@@ -0,0 +1,193 @@
+use crate::cache::FunctionID;
+use crate::convert::from_bn_symbol;
+use crate::function_guid;
+use binaryninja::binary_view::BinaryViewExt;
+use binaryninja::function::Function as BNFunction;
+use binaryninja::low_level_il::function::{FunctionMutability, LowLevelILFunction, NonSSA};
+use binaryninja::symbol::Symbol as BNSymbol;
+use std::collections::HashSet;
+use uuid::Uuid;
+use warp::signature::constraint::Constraint;
+use warp::signature::function::FunctionGUID;
+
+pub fn cached_function_guid<M: FunctionMutability>(
+ function: &BNFunction,
+ lifted_il: &LowLevelILFunction<M, NonSSA>,
+) -> FunctionGUID {
+ let cached_guid = try_cached_function_guid(function);
+ if let Some(cached_guid) = cached_guid {
+ return cached_guid;
+ }
+
+ let function_guid = function_guid(function, lifted_il);
+ function.store_metadata(
+ "warp_function_guid",
+ &function_guid.as_bytes().to_vec(),
+ false,
+ );
+ function_guid
+}
+
+pub fn try_cached_function_guid(function: &BNFunction) -> Option<FunctionGUID> {
+ let metadata = function.query_metadata("warp_function_guid")?;
+ let raw_metadata = metadata.get_raw()?;
+ let uuid = Uuid::from_slice(raw_metadata.as_slice()).ok()?;
+ Some(FunctionGUID::from(uuid))
+}
+
+pub fn cached_constraints<F>(function: &BNFunction, filter: F) -> HashSet<Constraint>
+where
+ F: Fn(&BNFunction) -> bool,
+{
+ // TODO: Implied constraints, symbol name, image offset
+ let cs_constraints = cached_call_site_constraints(function);
+ let adj_constraints = cached_adjacency_constraints(function, filter);
+ cs_constraints.union(&adj_constraints).cloned().collect()
+}
+
+pub fn cached_call_site_constraints(function: &BNFunction) -> HashSet<Constraint> {
+ let cache = ConstraintBuilder;
+ cache.call_site_constraints(function)
+}
+
+pub fn cached_adjacency_constraints<F>(function: &BNFunction, filter: F) -> HashSet<Constraint>
+where
+ F: Fn(&BNFunction) -> bool,
+{
+ let cache = ConstraintBuilder;
+ cache.adjacency_constraints(function, filter)
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct ConstraintBuilder;
+
+impl ConstraintBuilder {
+ pub fn call_site_constraints(&self, function: &BNFunction) -> HashSet<Constraint> {
+ let view = function.view();
+ let func_id = FunctionID::from(function);
+ let func_start = function.start();
+ let func_platform = function.platform();
+ let mut constraints = HashSet::new();
+ for call_site in &function.call_sites() {
+ for cs_ref_addr in view.code_refs_from_addr(call_site.address, Some(function)) {
+ match view.function_at(&func_platform, cs_ref_addr) {
+ Some(cs_ref_func) => {
+ // Call site is a function, constrain on it.
+ let cs_ref_func_id = FunctionID::from(cs_ref_func.as_ref());
+ if cs_ref_func_id != func_id {
+ let call_site_offset: i64 =
+ call_site.address.wrapping_sub(func_start) as i64;
+ // TODO: If the function is thunk we should also insert the called function.
+ constraints.extend(
+ self.related_function_constraint(&cs_ref_func, call_site_offset),
+ );
+ }
+ }
+ None => {
+ // We could be dealing with an extern symbol, get the symbol as a constraint.
+ let call_site_offset: i64 =
+ call_site.address.wrapping_sub(func_start) as i64;
+ if let Some(call_site_sym) = view.symbol_by_address(cs_ref_addr) {
+ constraints.insert(
+ self.related_symbol_constraint(&call_site_sym, call_site_offset),
+ );
+ }
+ }
+ }
+ }
+ }
+ constraints
+ }
+
+ pub fn adjacency_constraints<F>(&self, function: &BNFunction, filter: F) -> HashSet<Constraint>
+ where
+ F: Fn(&BNFunction) -> bool,
+ {
+ let view = function.view();
+ let func_id = FunctionID::from(function);
+ let func_start = function.start();
+ let mut constraints = HashSet::new();
+
+ let mut func_addr_constraint = |func_start_addr| {
+ // NOTE: We could potentially have dozens of functions all at the same start address.
+ for curr_func in &view.functions_at(func_start_addr) {
+ let curr_func_id = FunctionID::from(curr_func.as_ref());
+ if curr_func_id != func_id && filter(curr_func.as_ref()) {
+ // NOTE: For this to work the GUID has to have already been cached. If not it will just be the symbol.
+ // Function adjacent to another function, constrain on the pattern.
+ let curr_addr_offset = (func_start_addr as i64) - func_start as i64;
+ constraints
+ .extend(self.related_function_constraint(&curr_func, curr_addr_offset));
+ }
+ }
+ };
+
+ let mut before_func_start = func_start;
+ for _ in 0..2 {
+ before_func_start = view.function_start_before(before_func_start);
+ func_addr_constraint(before_func_start);
+ }
+
+ let mut after_func_start = func_start;
+ for _ in 0..2 {
+ after_func_start = view.function_start_after(after_func_start);
+ func_addr_constraint(after_func_start);
+ }
+
+ constraints
+ }
+
+ /// Construct a function constraint, must pass the offset at which it is located.
+ pub fn related_function_constraint(
+ &self,
+ function: &BNFunction,
+ offset: i64,
+ ) -> Vec<Constraint> {
+ let mut constraints = vec![];
+ if let Some(guid) = try_cached_function_guid(function) {
+ let guid_constraint = Constraint::from_function(&guid, Some(offset));
+ constraints.push(guid_constraint);
+ }
+ let symbol_constraint = self.related_symbol_constraint(&function.symbol(), offset);
+ constraints.push(symbol_constraint);
+ constraints
+ }
+
+ /// Construct a symbol constraint, must pass the offset at which it is located.
+ pub fn related_symbol_constraint(&self, symbol: &BNSymbol, offset: i64) -> Constraint {
+ let mut symbol = from_bn_symbol(symbol);
+ symbol.name = clean_symbol_name(&symbol.name);
+ Constraint::from_symbol(&symbol, Some(offset))
+ }
+}
+
+/// Cleans various internal symbol prefixes and suffixes for consistency.
+///
+/// This is very important for getting matching symbol constraints.
+///
+/// Examples:
+/// - "__imp__RemoveDirectoryW@4" -> "RemoveDirectoryW"
+/// - "__free_base" -> "free_base"
+/// - "__impl__free_base" -> "free_base"
+/// - "j___free_base" -> "free_base"
+/// - "j_free_base" -> "free_base"
+/// - "_free_base" -> "free_base"
+pub fn clean_symbol_name(symbol_name: &str) -> String {
+ // Handle MSVC-style imported symbols
+ let without_imp = symbol_name.strip_prefix("__imp__").unwrap_or(symbol_name);
+
+ // Handle jump thunk prefix
+ let without_jump = without_imp.strip_prefix("j_").unwrap_or(without_imp);
+
+ // Strip all remaining leading underscores
+ let mut result = without_jump;
+ while result.starts_with('_') {
+ result = &result[1..];
+ }
+
+ // Remove stdcall decoration (@N suffix)
+ match result.find('@') {
+ Some(pos) => result[..pos].to_string(),
+ None => result.to_string(),
+ }
+}
diff --git a/plugins/warp/src/cache/type_reference.rs b/plugins/warp/src/cache/type_reference.rs
new file mode 100644
index 00000000..0b07c139
--- /dev/null
+++ b/plugins/warp/src/cache/type_reference.rs
@@ -0,0 +1,105 @@
+use crate::cache::ViewID;
+use crate::convert::from_bn_type_internal;
+use binaryninja::binary_view::BinaryView;
+use binaryninja::confidence::MAX_CONFIDENCE;
+use binaryninja::rc::Guard;
+use binaryninja::rc::Ref as BNRef;
+use binaryninja::types::NamedTypeReference as BNNamedTypeReference;
+use dashmap::mapref::one::Ref;
+use dashmap::DashMap;
+use std::collections::HashSet;
+use std::hash::{DefaultHasher, Hasher};
+use std::sync::OnceLock;
+use warp::r#type::ComputedType;
+
+pub static TYPE_REF_CACHE: OnceLock<DashMap<ViewID, TypeRefCache>> = OnceLock::new();
+
+pub fn clear_type_ref_cache(view: &BinaryView) {
+ let view_id = ViewID::from(view);
+ if let Some(cache) = TYPE_REF_CACHE.get() {
+ cache.remove(&view_id);
+ }
+}
+
+pub fn cached_type_reference(
+ view: &BinaryView,
+ visited_refs: &mut HashSet<TypeRefID>,
+ type_ref: &BNNamedTypeReference,
+) -> Option<ComputedType> {
+ let view_id = ViewID::from(view);
+ let type_ref_cache = TYPE_REF_CACHE.get_or_init(Default::default);
+ match type_ref_cache.get(&view_id) {
+ Some(cache) => cache.cached_type_reference(view, visited_refs, type_ref),
+ None => {
+ let cache = TypeRefCache::default();
+ let ntr = cache.cached_type_reference(view, visited_refs, type_ref);
+ type_ref_cache.insert(view_id, cache);
+ ntr
+ }
+ }
+}
+
+pub fn cached_type_references(view: &BinaryView) -> Option<Ref<ViewID, TypeRefCache>> {
+ let view_id = ViewID::from(view);
+ let type_ref_cache = TYPE_REF_CACHE.get_or_init(Default::default);
+ type_ref_cache.get(&view_id)
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct TypeRefCache {
+ pub cache: DashMap<TypeRefID, Option<ComputedType>>,
+}
+
+impl TypeRefCache {
+ /// NOTE: No self-referential type must be used on this function.
+ pub fn cached_type_reference(
+ &self,
+ view: &BinaryView,
+ visited_refs: &mut HashSet<TypeRefID>,
+ type_ref: &BNNamedTypeReference,
+ ) -> Option<ComputedType> {
+ let ntr_id = TypeRefID::from(type_ref);
+ match self.cache.get(&ntr_id) {
+ Some(cache) => cache.to_owned(),
+ None => match type_ref.target(view) {
+ Some(raw_ty) => {
+ let computed_ty = ComputedType::new(from_bn_type_internal(
+ view,
+ visited_refs,
+ &raw_ty,
+ MAX_CONFIDENCE,
+ ));
+ self.cache
+ .entry(ntr_id)
+ .insert(Some(computed_ty))
+ .to_owned()
+ }
+ None => self.cache.entry(ntr_id).insert(None).to_owned(),
+ },
+ }
+ }
+}
+
+/// A unique named type reference ID, used for caching.
+#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct TypeRefID(u64);
+
+impl From<&BNNamedTypeReference> for TypeRefID {
+ fn from(value: &BNNamedTypeReference) -> Self {
+ let mut hasher = DefaultHasher::new();
+ hasher.write(value.id().as_bytes());
+ Self(hasher.finish())
+ }
+}
+
+impl From<BNRef<BNNamedTypeReference>> for TypeRefID {
+ fn from(value: BNRef<BNNamedTypeReference>) -> Self {
+ Self::from(value.as_ref())
+ }
+}
+
+impl From<Guard<'_, BNNamedTypeReference>> for TypeRefID {
+ fn from(value: Guard<'_, BNNamedTypeReference>) -> Self {
+ Self::from(value.as_ref())
+ }
+}
diff --git a/plugins/warp/src/container.rs b/plugins/warp/src/container.rs
new file mode 100644
index 00000000..8d720021
--- /dev/null
+++ b/plugins/warp/src/container.rs
@@ -0,0 +1,266 @@
+use crate::container::disk::NAMESPACE_DISK_SOURCE;
+use std::collections::HashMap;
+use std::fmt::{Debug, Display};
+use std::hash::Hash;
+use std::io;
+use std::path::{Path, PathBuf};
+use std::str::FromStr;
+use thiserror::Error;
+use uuid::Uuid;
+use warp::r#type::guid::TypeGUID;
+use warp::r#type::{ComputedType, Type};
+use warp::signature::function::{Function, FunctionGUID};
+use warp::target::Target;
+
+pub mod disk;
+pub mod memory;
+pub mod network;
+
+pub type ContainerResult<T> = Result<T, ContainerError>;
+
+#[derive(Debug, Error, PartialEq, Eq, Hash)]
+pub enum ContainerError {
+ #[error("source {0} was not found")]
+ SourceNotFound(SourceId),
+ #[error("source {0} is not writable")]
+ SourceNotWritable(SourceId),
+ #[error("source with path {0} already exists")]
+ SourceAlreadyExists(SourcePath),
+ #[error("source with path {0} cannot be created in container")]
+ CannotCreateSource(SourcePath),
+ #[error("operation failed due to corrupted data: {0}")]
+ CorruptedData(&'static str),
+ #[error("failed io operation: {0}")]
+ FailedIO(io::ErrorKind),
+ #[error("source {0} does not have an available path")]
+ SourcePathUnavailable(SourceId),
+}
+
+/// Represents the ID for a single container source.
+///
+/// A [`SourceId`] can be used in multiple separate containers, but **must** be unique in a container.
+///
+/// A source is used to relate types and functions separate from the container. This allows
+/// type name lookups and for containers which are bandwidth sensitive to exist.
+///
+/// An example of a bandwidth-sensitive container would be a container that pulls functions over
+/// the network instead of from memory.
+///
+/// This type is marked `repr(transparent)` to the underlying `[u8; 16]` type, so it is safe to use in FFI.
+#[repr(transparent)]
+#[derive(Clone, Debug, Eq, PartialEq, Hash, Copy)]
+pub struct SourceId(Uuid);
+
+impl SourceId {
+ pub fn new() -> Self {
+ Self(Uuid::new_v4())
+ }
+}
+
+impl From<Uuid> for SourceId {
+ fn from(value: Uuid) -> Self {
+ Self(value)
+ }
+}
+
+impl FromStr for SourceId {
+ type Err = uuid::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Uuid::parse_str(s).map(Into::into)
+ }
+}
+
+impl Display for SourceId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+/// Represents a unique path to a source.
+///
+/// This is used when first creating a source for a container, the path is given to the container
+/// as otherwise the user has no control over source creation and where the source is ultimately located.
+///
+/// While the underlying type is a [`PathBuf`], a source path can be really anything, the [`PathBuf`]
+/// just provides an easier way to join segments for nested source locations.
+#[derive(Clone, Debug, Eq, PartialEq, Hash)]
+pub struct SourcePath(PathBuf);
+
+impl SourcePath {
+ pub fn new(path: PathBuf) -> Self {
+ Self(path)
+ }
+
+ pub fn new_with_str(value: &str) -> Self {
+ Self(PathBuf::from(value))
+ }
+
+ pub fn to_source_id(&self) -> SourceId {
+ // TODO: This path is not relative to the disk container is it?
+ // TODO: The path here should be relative to the container I think?
+ // TODO: The above is important so that the id is the same across users.
+ let value: Vec<u8> = self.to_string().into_bytes();
+ SourceId(Uuid::new_v5(&NAMESPACE_DISK_SOURCE, &value))
+ }
+}
+
+impl AsRef<PathBuf> for SourcePath {
+ fn as_ref(&self) -> &PathBuf {
+ &self.0
+ }
+}
+
+impl AsRef<Path> for SourcePath {
+ fn as_ref(&self) -> &Path {
+ &self.0
+ }
+}
+
+impl Display for SourcePath {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0.display())
+ }
+}
+
+/// Storage for WARP information.
+///
+/// Containers are made up of sources, see [`SourceId`] for more details.
+pub trait Container: Send + Sync + Display + Debug {
+ /// Available container sources.
+ ///
+ /// NOTE: Due to the nature of some containers, this list of sources may be incomplete. Do not
+ /// rely on this list to retrieve data, instead prefer:
+ /// - [Container::sources_with_type_guid]
+ /// - [Container::sources_with_type_guids]
+ /// - [Container::sources_with_function_guid]
+ /// - [Container::sources_with_function_guids]
+ fn sources(&self) -> ContainerResult<Vec<SourceId>>;
+
+ /// Create a new source in the container or add the existing source at the given path to known sources.
+ ///
+ /// The returned [`SourceId`] can be used to add, query and remove information from the source.
+ ///
+ /// NOTE: Adding a source does **NOT** mean that it, and the data associated with it, has been
+ /// persisted, you **MUST** call [`Container::commit_source`] to persist the created source.
+ ///
+ /// NOTE: Adding a source does **NOT** mean that you can write to it, use [`Container::is_source_writable`]
+ /// to verify the permissions of the source.
+ fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId>;
+
+ /// Flush changes made to a source.
+ ///
+ /// Because writing to a source can require file or network operations, we let the container
+ /// offer the ability to hold off performing that operation until the data needs to be committed.
+ fn commit_source(&mut self, source: &SourceId) -> ContainerResult<bool>;
+
+ /// Whether the source can be written to.
+ ///
+ /// The source must be mutable to perform the following:
+ /// - [Container::add_types]
+ /// - [Container::add_computed_types]
+ /// - [Container::remove_types]
+ /// - [Container::add_functions]
+ /// - [Container::remove_functions]
+ fn is_source_writable(&self, source: &SourceId) -> ContainerResult<bool>;
+
+ /// Whether the source has uncommitted changes or not.
+ ///
+ /// NOTE: This is **NOT** whether the source has been committed at all, rather a flag to indicate
+ /// that a source has uncommitted changes.
+ fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool>;
+
+ /// Retrieve the [`SourcePath`] for the given source.
+ ///
+ /// NOTE: This does not have to be a filesystem path, its representation is dictated
+ /// by the implementation.
+ fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath>;
+
+ // TODO: Note about commit_source
+ fn add_types(&mut self, source: &SourceId, types: &[Type]) -> ContainerResult<()> {
+ let computed_types: Vec<_> = types.iter().cloned().map(ComputedType::new).collect();
+ self.add_computed_types(source, &computed_types)
+ }
+
+ // TODO: Note about commit_source
+ fn add_computed_types(
+ &mut self,
+ source: &SourceId,
+ types: &[ComputedType],
+ ) -> ContainerResult<()>;
+
+ // TODO: Note about commit_source
+ fn remove_types(&mut self, source: &SourceId, guids: &[TypeGUID]) -> ContainerResult<()>;
+
+ // TODO: Note about commit_source
+ fn add_functions(
+ &mut self,
+ target: &Target,
+ source: &SourceId,
+ functions: &[Function],
+ ) -> ContainerResult<()>;
+
+ // TODO: Note about commit_source
+ fn remove_functions(
+ &mut self,
+ target: &Target,
+ source: &SourceId,
+ functions: &[Function],
+ ) -> ContainerResult<()>;
+
+ /// Get the sources that contain a type with the given [`TypeGUID`].
+ fn sources_with_type_guid(&self, guid: &TypeGUID) -> ContainerResult<Vec<SourceId>>;
+
+ /// Plural version of [`Container::sources_with_type_guid`].
+ ///
+ /// Each source will have a list of the containing GUID's so that when looking up a source, you give
+ /// it only the GUID's that it knows about, for networking this means cutting down traffic significantly.
+ fn sources_with_type_guids(
+ &self,
+ guids: &[TypeGUID],
+ ) -> ContainerResult<HashMap<TypeGUID, Vec<SourceId>>>;
+
+ /// Retrieve all [`TypeGUID`]'s with the given name.
+ fn type_guids_with_name(&self, source: &SourceId, name: &str)
+ -> ContainerResult<Vec<TypeGUID>>;
+
+ fn type_with_guid(&self, source: &SourceId, guid: &TypeGUID) -> ContainerResult<Option<Type>>;
+
+ fn has_type_with_guid(&self, source: &SourceId, guid: &TypeGUID) -> ContainerResult<bool> {
+ Ok(self.type_with_guid(source, guid)?.is_some())
+ }
+
+ /// Get the sources that contain functions with the given [`FunctionGUID`].
+ fn sources_with_function_guid(
+ &self,
+ target: &Target,
+ guid: &FunctionGUID,
+ ) -> ContainerResult<Vec<SourceId>>;
+
+ // TODO: Allocating with Vec is not good.
+ /// Plural version of [`Container::sources_with_function_guid`].
+ ///
+ /// Each source will have a list of the containing GUID's so that when looking up a source you give
+ /// it only the GUID's that it knows about, for networking this means cutting down traffic significantly.
+ fn sources_with_function_guids(
+ &self,
+ target: &Target,
+ guids: &[FunctionGUID],
+ ) -> ContainerResult<HashMap<FunctionGUID, Vec<SourceId>>>;
+
+ fn functions_with_guid(
+ &self,
+ target: &Target,
+ source: &SourceId,
+ guid: &FunctionGUID,
+ ) -> ContainerResult<Vec<Function>>;
+
+ fn has_function_with_guid(
+ &self,
+ target: &Target,
+ source: &SourceId,
+ guid: &FunctionGUID,
+ ) -> ContainerResult<bool> {
+ Ok(!self.functions_with_guid(target, source, guid)?.is_empty())
+ }
+}
diff --git a/plugins/warp/src/container/disk.rs b/plugins/warp/src/container/disk.rs
new file mode 100644
index 00000000..ca685a5f
--- /dev/null
+++ b/plugins/warp/src/container/disk.rs
@@ -0,0 +1,402 @@
+use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath};
+use std::collections::HashMap;
+use std::fmt::{Debug, Display, Formatter};
+use std::hash::{Hash, Hasher};
+use std::path::PathBuf;
+use uuid::{uuid, Uuid};
+use walkdir::{DirEntry, WalkDir};
+use warp::chunk::{Chunk, ChunkKind, CompressionType};
+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::function::{Function, FunctionGUID};
+use warp::target::Target;
+use warp::{WarpFile, WarpFileHeader};
+
+pub const NAMESPACE_DISK_SOURCE: Uuid = uuid!("ea89e8ab-a27a-432b-8fbd-77b026cd5f41");
+
+// TODO: How to support remote projects? I.e. collaboration?
+pub struct DiskContainer {
+ pub name: String,
+ pub sources: HashMap<SourceId, DiskContainerSource>,
+}
+
+impl DiskContainer {
+ pub fn new(name: String, sources: HashMap<SourceId, DiskContainerSource>) -> Self {
+ Self { name, sources }
+ }
+
+ pub fn new_from_dir(dir_path: PathBuf) -> Self {
+ let source_from_entry = |entry: DirEntry| {
+ let path = SourcePath(entry.into_path());
+ let source_id = path.to_source_id();
+ let path_ext = path.0.extension().unwrap_or_default().to_str();
+ match (DiskContainerSource::new_from_path(path.clone()), path_ext) {
+ (Ok(source), _) => Some((source_id, source)),
+ (Err(err), Some("warp")) => {
+ log::error!("Failed to load source '{}' from disk: {}", path, err);
+ None
+ }
+ // We don't care to show errors loading for non-warp files.
+ (Err(_), _) => None,
+ }
+ };
+
+ // TODO: For now, any file that does not have the "warp" extension will be filtered out.
+ // TODO: cont. in the future we might want to remove this for convenience.
+ let name = dir_path.to_string_lossy().to_string();
+ let sources = WalkDir::new(dir_path)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|e| e.file_type().is_file())
+ .filter(|e| e.path().extension().is_some_and(|e| e == "warp"))
+ .filter_map(source_from_entry)
+ .collect();
+
+ Self::new(name, sources)
+ }
+}
+
+impl Container for DiskContainer {
+ fn sources(&self) -> ContainerResult<Vec<SourceId>> {
+ Ok(self.sources.keys().copied().collect())
+ }
+
+ fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId> {
+ // Disk sources have there source id computed from the path.
+ let source_id = path.to_source_id();
+ if self.sources.contains_key(&source_id) {
+ return Err(ContainerError::SourceAlreadyExists(path));
+ }
+ // NOTE: We let anyone add a file from anywhere on the file system because of this.
+ match path.0.exists() {
+ true => {
+ let disk_source = DiskContainerSource::new_from_path(path.clone())?;
+ self.sources.insert(source_id, disk_source);
+ Ok(source_id)
+ }
+ false => {
+ let file = WarpFile::new(WarpFileHeader::new(), vec![]);
+ let disk_source = DiskContainerSource::new(path, file);
+ self.sources.insert(source_id, disk_source);
+ Ok(source_id)
+ }
+ }
+ }
+
+ fn commit_source(&mut self, source: &SourceId) -> ContainerResult<bool> {
+ let disk_source = self
+ .sources
+ .get_mut(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+
+ disk_source.commit_to_disk()
+ }
+
+ fn is_source_writable(&self, source: &SourceId) -> ContainerResult<bool> {
+ let _disk_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ // TODO: I think this should be up to the container. (cant write to bundled files)
+ Ok(true)
+ }
+
+ fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool> {
+ let disk_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ Ok(disk_source.uncommitted)
+ }
+
+ fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> {
+ let disk_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ Ok(disk_source.path.clone())
+ }
+
+ fn add_computed_types(
+ &mut self,
+ source: &SourceId,
+ types: &[ComputedType],
+ ) -> ContainerResult<()> {
+ let disk_source = self
+ .sources
+ .get_mut(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+
+ disk_source.add_computed_types(types)
+ }
+
+ // TODO: I believe any remove has to happen immediately, i.e. we cant add an uncommitted for this?
+ fn remove_types(&mut self, source: &SourceId, _guids: &[TypeGUID]) -> ContainerResult<()> {
+ let _disk_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+
+ // TODO: Do this.
+ Err(ContainerError::SourceNotWritable(*source))
+ }
+
+ fn add_functions(
+ &mut self,
+ target: &Target,
+ source: &SourceId,
+ functions: &[Function],
+ ) -> ContainerResult<()> {
+ let disk_source = self
+ .sources
+ .get_mut(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+
+ disk_source.add_functions(target.clone(), functions)
+ }
+
+ // TODO: I believe any remove has to happen immediately, i.e. we cant add an uncommitted for this?
+ fn remove_functions(
+ &mut self,
+ _target: &Target,
+ source: &SourceId,
+ _functions: &[Function],
+ ) -> ContainerResult<()> {
+ let _disk_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+
+ // TODO: Do this.
+ Err(ContainerError::SourceNotWritable(*source))
+ }
+
+ fn sources_with_type_guid(&self, guid: &TypeGUID) -> ContainerResult<Vec<SourceId>> {
+ let sources = self
+ .sources
+ .iter()
+ .filter(|(_, source)| source.has_type_with_guid(guid))
+ .map(|(id, _)| *id)
+ .collect();
+ Ok(sources)
+ }
+
+ fn sources_with_type_guids<'a>(
+ &'a self,
+ guids: &'a [TypeGUID],
+ ) -> ContainerResult<HashMap<TypeGUID, Vec<SourceId>>> {
+ let mut result: HashMap<TypeGUID, Vec<SourceId>> = HashMap::new();
+ for (source_id, source) in &self.sources {
+ guids
+ .iter()
+ .filter(|guid| source.has_type_with_guid(guid))
+ .for_each(|guid| result.entry(*guid).or_default().push(*source_id));
+ }
+ Ok(result)
+ }
+
+ fn type_guids_with_name(
+ &self,
+ source: &SourceId,
+ name: &str,
+ ) -> ContainerResult<Vec<TypeGUID>> {
+ let disk_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ Ok(disk_source.type_guids_with_name(name))
+ }
+
+ fn type_with_guid(&self, source: &SourceId, guid: &TypeGUID) -> ContainerResult<Option<Type>> {
+ let disk_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ Ok(disk_source.type_with_guid(guid))
+ }
+
+ fn sources_with_function_guid(
+ &self,
+ target: &Target,
+ guid: &FunctionGUID,
+ ) -> ContainerResult<Vec<SourceId>> {
+ let sources = self
+ .sources
+ .iter()
+ .filter(|(_, source)| source.has_function_with_guid(target, guid))
+ .map(|(id, _)| *id)
+ .collect();
+ Ok(sources)
+ }
+
+ fn sources_with_function_guids<'a>(
+ &self,
+ target: &Target,
+ guids: &[FunctionGUID],
+ ) -> ContainerResult<HashMap<FunctionGUID, Vec<SourceId>>> {
+ let mut result: HashMap<FunctionGUID, Vec<SourceId>> = HashMap::new();
+ for (source_id, source) in &self.sources {
+ guids
+ .iter()
+ .filter(|guid| source.has_function_with_guid(target, guid))
+ .for_each(|guid| result.entry(*guid).or_default().push(*source_id));
+ }
+ Ok(result)
+ }
+
+ fn functions_with_guid(
+ &self,
+ target: &Target,
+ source: &SourceId,
+ guid: &FunctionGUID,
+ ) -> ContainerResult<Vec<Function>> {
+ let disk_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ Ok(disk_source.functions_with_guid(target, guid))
+ }
+}
+
+impl Display for DiskContainer {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.name)
+ }
+}
+
+impl Debug for DiskContainer {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("DiskContainer")
+ .field("name", &self.name)
+ .field("sources", &self.sources)
+ .finish()
+ }
+}
+
+pub struct DiskContainerSource {
+ pub path: SourcePath,
+ file: WarpFile<'static>,
+ uncommitted: bool,
+}
+
+impl DiskContainerSource {
+ pub fn new(path: SourcePath, file: WarpFile<'static>) -> Self {
+ Self {
+ path,
+ file,
+ uncommitted: false,
+ }
+ }
+
+ pub fn new_from_path(path: SourcePath) -> ContainerResult<Self> {
+ // TODO: To keep the lifetime out of DiskContainerSource we do not allow mapping file to memory.
+ let contents = std::fs::read(&path).map_err(|e| ContainerError::FailedIO(e.kind()))?;
+ let file = WarpFile::from_owned_bytes(contents).ok_or(ContainerError::CorruptedData(
+ "file data failed to validate",
+ ))?;
+ Ok(Self::new(path, file))
+ }
+
+ fn add_computed_types(&mut self, types: &[ComputedType]) -> ContainerResult<()> {
+ let type_chunk = TypeChunk::new_with_computed(types).ok_or(
+ ContainerError::CorruptedData("type chunk failed to validate"),
+ )?;
+ let chunk = Chunk::new(ChunkKind::Type(type_chunk), CompressionType::None);
+ self.file.chunks.push(chunk);
+ self.uncommitted = true;
+ Ok(())
+ }
+
+ fn add_functions(&mut self, target: Target, functions: &[Function]) -> ContainerResult<()> {
+ let signature_chunk = SignatureChunk::new(functions).ok_or(
+ ContainerError::CorruptedData("signature chunk failed to validate"),
+ )?;
+ let chunk = Chunk::new_with_target(
+ ChunkKind::Signature(signature_chunk),
+ CompressionType::None,
+ target,
+ );
+ self.file.chunks.push(chunk);
+ self.uncommitted = true;
+ Ok(())
+ }
+
+ fn commit_to_disk(&mut self) -> ContainerResult<bool> {
+ let file = self.file.to_bytes();
+ std::fs::write(&self.path, file).map_err(|e| ContainerError::FailedIO(e.kind()))?;
+ self.uncommitted = false;
+ Ok(true)
+ }
+
+ fn type_guids_with_name(&self, name: &str) -> Vec<TypeGUID> {
+ let mut found: Vec<TypeGUID> = Vec::new();
+ for chunk in &self.file.chunks {
+ if let ChunkKind::Type(tc) = &chunk.kind {
+ found.extend(
+ tc.raw_type_with_name(name)
+ .into_iter()
+ .map(|t| TypeGUID::from(t.guid())),
+ );
+ }
+ }
+ found
+ }
+
+ fn type_with_guid(&self, guid: &TypeGUID) -> Option<Type> {
+ self.file.chunks.iter().find_map(|chunk| {
+ if let ChunkKind::Type(tc) = &chunk.kind {
+ tc.type_with_guid(guid)
+ } else {
+ None
+ }
+ })
+ }
+
+ // TODO: When we support reading lazily instead of all in memory.
+ fn has_type_with_guid(&self, guid: &TypeGUID) -> bool {
+ self.type_with_guid(guid).is_some()
+ }
+
+ fn functions_with_guid(&self, target: &Target, guid: &FunctionGUID) -> Vec<Function> {
+ let mut found: Vec<Function> = Vec::new();
+ for chunk in &self.file.chunks {
+ if chunk.header.target != *target {
+ continue;
+ }
+ if let ChunkKind::Signature(sc) = &chunk.kind {
+ found.extend(sc.functions_with_guid(guid));
+ }
+ }
+ found
+ }
+
+ // TODO: When we support reading lazily instead of all in memory.
+ fn has_function_with_guid(&self, target: &Target, guid: &FunctionGUID) -> bool {
+ // TODO: How about we dont clone.
+ !self.functions_with_guid(target, guid).is_empty()
+ }
+}
+
+impl Hash for DiskContainerSource {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.path.hash(state);
+ }
+}
+
+impl Display for DiskContainerSource {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.path)
+ }
+}
+
+impl Debug for DiskContainerSource {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("DiskContainerSource")
+ .field("path", &self.path)
+ .field("file_header", &self.file.header)
+ .field("file_chunks", &self.file.chunks.len())
+ .finish()
+ }
+}
diff --git a/plugins/warp/src/container/memory.rs b/plugins/warp/src/container/memory.rs
new file mode 100644
index 00000000..cf53390e
--- /dev/null
+++ b/plugins/warp/src/container/memory.rs
@@ -0,0 +1,307 @@
+use crate::container::{Container, ContainerError, ContainerResult, SourceId, SourcePath};
+use std::collections::HashMap;
+use std::fmt::Display;
+use warp::r#type::guid::TypeGUID;
+use warp::r#type::{ComputedType, Type};
+use warp::signature::function::{Function, FunctionGUID};
+use warp::target::Target;
+
+#[derive(Debug, Clone, PartialEq, Eq, Default)]
+pub struct MemoryContainer {
+ sources: HashMap<SourceId, MemorySource>,
+}
+
+impl MemoryContainer {
+ pub fn new() -> Self {
+ MemoryContainer::default()
+ }
+
+ pub fn with_source(mut self, id: SourceId, source: MemorySource) -> Self {
+ self.sources.insert(id, source);
+ self
+ }
+
+ pub fn with_source_function(
+ mut self,
+ id: SourceId,
+ guid: FunctionGUID,
+ func: Function,
+ ) -> Self {
+ self.sources
+ .entry(id)
+ .or_default()
+ .functions
+ .entry(guid)
+ .or_default()
+ .push(func);
+ self
+ }
+
+ pub fn with_source_type(mut self, id: SourceId, guid: TypeGUID, ty: Type) -> Self {
+ self.sources.entry(id).or_default().types.insert(guid, ty);
+ self
+ }
+}
+
+impl Container for MemoryContainer {
+ fn sources(&self) -> ContainerResult<Vec<SourceId>> {
+ todo!()
+ }
+
+ fn add_source(&mut self, path: SourcePath) -> ContainerResult<SourceId> {
+ Err(ContainerError::CannotCreateSource(path))
+ }
+
+ fn commit_source(&mut self, _source: &SourceId) -> ContainerResult<bool> {
+ Ok(false)
+ }
+
+ fn is_source_writable(&self, source: &SourceId) -> ContainerResult<bool> {
+ let memory_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ Ok(memory_source.writable)
+ }
+
+ fn is_source_uncommitted(&self, source: &SourceId) -> ContainerResult<bool> {
+ let _memory_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ // NOTE: Memory containers do not have a notion of uncommitted data.
+ Ok(false)
+ }
+
+ fn source_path(&self, source: &SourceId) -> ContainerResult<SourcePath> {
+ Err(ContainerError::SourcePathUnavailable(*source))
+ }
+
+ fn add_computed_types(
+ &mut self,
+ source: &SourceId,
+ types: &[ComputedType],
+ ) -> ContainerResult<()> {
+ let memory_source = self
+ .sources
+ .get_mut(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ match memory_source.writable {
+ true => {
+ for ty in types {
+ memory_source.types.insert(ty.guid, ty.ty.clone());
+ }
+ Ok(())
+ }
+ false => Err(ContainerError::SourceNotWritable(*source)),
+ }
+ }
+
+ fn remove_types(&mut self, source: &SourceId, guids: &[TypeGUID]) -> ContainerResult<()> {
+ let memory_source = self
+ .sources
+ .get_mut(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ match memory_source.writable {
+ true => {
+ for guid in guids {
+ memory_source.types.remove(guid);
+ }
+ Ok(())
+ }
+ false => Err(ContainerError::SourceNotWritable(*source)),
+ }
+ }
+
+ fn add_functions(
+ &mut self,
+ _target: &Target,
+ source: &SourceId,
+ functions: &[Function],
+ ) -> ContainerResult<()> {
+ let memory_source = self
+ .sources
+ .get_mut(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ match memory_source.writable {
+ true => {
+ for function in functions {
+ memory_source
+ .functions
+ .entry(function.guid)
+ .or_default()
+ .push(function.clone());
+ }
+ Ok(())
+ }
+ false => Err(ContainerError::SourceNotWritable(*source)),
+ }
+ }
+
+ fn remove_functions(
+ &mut self,
+ _target: &Target,
+ source: &SourceId,
+ functions: &[Function],
+ ) -> ContainerResult<()> {
+ let memory_source = self
+ .sources
+ .get_mut(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ match memory_source.writable {
+ true => {
+ for function in functions {
+ if let Some(src_funcs) = memory_source.functions.get_mut(&function.guid) {
+ src_funcs.retain(|f| f != function);
+ if src_funcs.is_empty() {
+ memory_source.functions.remove(&function.guid);
+ }
+ }
+ }
+ Ok(())
+ }
+ false => Err(ContainerError::SourceNotWritable(*source)),
+ }
+ }
+
+ fn sources_with_type_guid(&self, guid: &TypeGUID) -> ContainerResult<Vec<SourceId>> {
+ let sources = self
+ .sources
+ .iter()
+ .filter(|(_, source)| source.has_type_with_guid(guid))
+ .map(|(id, _)| *id)
+ .collect();
+ Ok(sources)
+ }
+
+ fn sources_with_type_guids(
+ &self,
+ guids: &[TypeGUID],
+ ) -> ContainerResult<HashMap<TypeGUID, Vec<SourceId>>> {
+ let mut result: HashMap<TypeGUID, Vec<SourceId>> = HashMap::new();
+ for (source_id, source) in &self.sources {
+ guids
+ .iter()
+ .filter(|guid| source.has_type_with_guid(guid))
+ .for_each(|guid| result.entry(*guid).or_default().push(*source_id));
+ }
+ Ok(result)
+ }
+
+ fn type_guids_with_name(
+ &self,
+ source: &SourceId,
+ name: &str,
+ ) -> ContainerResult<Vec<TypeGUID>> {
+ let memory_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ Ok(memory_source.type_guids_with_name(name))
+ }
+
+ fn type_with_guid(&self, source: &SourceId, guid: &TypeGUID) -> ContainerResult<Option<Type>> {
+ let memory_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ Ok(memory_source.type_with_guid(guid))
+ }
+
+ fn sources_with_function_guid(
+ &self,
+ _target: &Target,
+ guid: &FunctionGUID,
+ ) -> ContainerResult<Vec<SourceId>> {
+ let sources = self
+ .sources
+ .iter()
+ .filter(|(_, source)| source.has_function_with_guid(guid))
+ .map(|(id, _)| *id)
+ .collect();
+ Ok(sources)
+ }
+
+ fn sources_with_function_guids(
+ &self,
+ _target: &Target,
+ guids: &[FunctionGUID],
+ ) -> ContainerResult<HashMap<FunctionGUID, Vec<SourceId>>> {
+ let mut result: HashMap<FunctionGUID, Vec<SourceId>> = HashMap::new();
+ for (source_id, source) in &self.sources {
+ guids
+ .iter()
+ .filter(|guid| source.has_function_with_guid(guid))
+ .for_each(|guid| result.entry(*guid).or_default().push(*source_id));
+ }
+ Ok(result)
+ }
+
+ fn functions_with_guid(
+ &self,
+ _target: &Target,
+ source: &SourceId,
+ guid: &FunctionGUID,
+ ) -> ContainerResult<Vec<Function>> {
+ let memory_source = self
+ .sources
+ .get(source)
+ .ok_or(ContainerError::SourceNotFound(*source))?;
+ Ok(memory_source.functions_with_guid(guid))
+ }
+}
+
+impl Display for MemoryContainer {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str("MemoryContainer")
+ }
+}
+
+/// An in-memory store of functions.
+///
+/// This is typically an overlay on top of a container source.
+#[derive(Eq, PartialEq, Debug, Clone)]
+pub struct MemorySource {
+ pub writable: bool,
+ pub functions: HashMap<FunctionGUID, Vec<Function>>,
+ pub types: HashMap<TypeGUID, Type>,
+ pub named_types: HashMap<String, Vec<TypeGUID>>,
+}
+
+impl MemorySource {
+ pub fn type_guids_with_name(&self, name: &str) -> Vec<TypeGUID> {
+ // TODO: The function here is a little goofy.
+ // TODO: This is cloned.
+ self.named_types.get(name).cloned().unwrap_or_default()
+ }
+
+ pub fn type_with_guid(&self, guid: &TypeGUID) -> Option<Type> {
+ // TODO: This is cloned.
+ self.types.get(guid).cloned()
+ }
+
+ pub fn functions_with_guid(&self, guid: &FunctionGUID) -> Vec<Function> {
+ // TODO: The function here is a little goofy.
+ // TODO: This is cloned.
+ self.functions.get(guid).cloned().unwrap_or_default()
+ }
+
+ pub fn has_type_with_guid(&self, guid: &TypeGUID) -> bool {
+ self.type_with_guid(guid).is_some()
+ }
+
+ pub fn has_function_with_guid(&self, guid: &FunctionGUID) -> bool {
+ !self.functions_with_guid(guid).is_empty()
+ }
+}
+
+impl Default for MemorySource {
+ fn default() -> Self {
+ Self {
+ writable: true,
+ functions: HashMap::new(),
+ types: HashMap::new(),
+ named_types: HashMap::new(),
+ }
+ }
+}
diff --git a/plugins/warp/src/container/network.rs b/plugins/warp/src/container/network.rs
new file mode 100644
index 00000000..ffbe6108
--- /dev/null
+++ b/plugins/warp/src/container/network.rs
@@ -0,0 +1,13 @@
+pub struct NetworkContainer {}
+
+// TODO: The container is populated as the user is navigating a binary
+// TODO: We need to have a few helper functions here to post and pull
+// TODO: Then in the interface we operate off the network cache
+// TODO: The network cache could just be a disk container? Or disk container sources?
+// TODO: We should also store the cache on the filesystem for a certain time, will need to timestamp
+// TODO: When we commit we need to actually POST i believe.
+// TODO: There needs to be a setting that adjusts the sweep size of functions at the cursor.
+// TODO: Probably need a callback or something to tell the network containers to refresh from the network.
+// TODO: The network container should never instantiate itself, unless its gurenteed to not have any data in it?
+
+// TODO: Need to PUSH chunks and PULL chunks
diff --git a/plugins/warp/src/convert.rs b/plugins/warp/src/convert.rs
index b6c2f7b4..8272768b 100644
--- a/plugins/warp/src/convert.rs
+++ b/plugins/warp/src/convert.rs
@@ -1,678 +1,54 @@
-use std::collections::HashSet;
+pub mod symbol;
+pub mod types;
-use binaryninja::architecture::Architecture as BNArchitecture;
-use binaryninja::architecture::ArchitectureExt;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::calling_convention::CoreCallingConvention as BNCallingConvention;
-use binaryninja::confidence::{Conf as BNConf, MAX_CONFIDENCE};
-use binaryninja::rc::Ref as BNRef;
-use binaryninja::symbol::{Symbol as BNSymbol, SymbolType as BNSymbolType};
-use binaryninja::types::{
- BaseStructure as BNBaseStructure, EnumerationBuilder as BNEnumerationBuilder,
- FunctionParameter as BNFunctionParameter, MemberAccess as BNMemberAccess, MemberAccess,
- MemberScope as BNMemberScope, NamedTypeReference, NamedTypeReference as BNNamedTypeReference,
- NamedTypeReferenceClass, StructureBuilder as BNStructureBuilder,
- StructureMember as BNStructureMember,
-};
-use binaryninja::types::{
- StructureType as BNStructureType, Type as BNType, TypeClass as BNTypeClass,
-};
+use binaryninja::function::Comment as BNComment;
+use binaryninja::function::Function as BNFunction;
+use binaryninja::platform::Platform;
+use binaryninja::rc::Ref;
+pub use symbol::*;
+pub use types::*;
+use warp::signature::comment::FunctionComment;
+use warp::target::Target;
-use crate::cache::{cached_type_reference, TypeRefID};
-use warp::r#type::class::array::ArrayModifiers;
-use warp::r#type::class::function::{Location, RegisterLocation};
-use warp::r#type::class::pointer::PointerAddressing;
-use warp::r#type::class::structure::StructureMemberModifiers;
-use warp::r#type::class::{
- ArrayClass, BooleanClass, CallingConvention, CharacterClass, EnumerationClass,
- EnumerationMember, FloatClass, FunctionClass, FunctionMember, IntegerClass, PointerClass,
- ReferrerClass, StructureClass, StructureMember, TypeClass,
-};
-use warp::r#type::Type;
-use warp::symbol::class::SymbolClass;
-use warp::symbol::{Symbol, SymbolModifiers};
-
-pub fn from_bn_symbol(raw_symbol: &BNSymbol) -> Symbol {
- // TODO: Use this?
- let _is_export = raw_symbol.external();
- let symbol_name = raw_symbol.raw_name().to_string_lossy().to_string();
- match raw_symbol.sym_type() {
- BNSymbolType::ImportAddress => {
- Symbol::new(
- symbol_name,
- SymbolClass::Function,
- // TODO: External = symbolic i guess
- SymbolModifiers::External,
- )
- }
- BNSymbolType::Data => {
- Symbol::new(
- symbol_name,
- // TODO: Data?
- SymbolClass::Data,
- SymbolModifiers::default(),
- )
- }
- BNSymbolType::Symbolic => {
- Symbol::new(
- symbol_name,
- SymbolClass::Function,
- // TODO: External = symbolic i guess
- SymbolModifiers::External,
- )
- }
- BNSymbolType::LocalLabel => {
- // TODO: This is a placeholder for another symbol.
- Symbol::new(symbol_name, SymbolClass::Data, SymbolModifiers::External)
- }
- BNSymbolType::External => Symbol::new(
- symbol_name,
- // TODO: External data?
- SymbolClass::Function,
- SymbolModifiers::External,
- ),
- BNSymbolType::ImportedData => {
- Symbol::new(symbol_name, SymbolClass::Data, SymbolModifiers::External)
- }
- BNSymbolType::LibraryFunction | BNSymbolType::Function => Symbol::new(
- symbol_name,
- SymbolClass::Function,
- SymbolModifiers::default(),
- ),
- BNSymbolType::ImportedFunction => Symbol::new(
- symbol_name,
- SymbolClass::Function,
- // TODO: Exported?
- SymbolModifiers::External,
- ),
- }
-}
-
-pub fn to_bn_symbol_at_address(view: &BinaryView, symbol: &Symbol, addr: u64) -> BNRef<BNSymbol> {
- let is_external = symbol.modifiers.contains(SymbolModifiers::External);
- let _is_exported = symbol.modifiers.contains(SymbolModifiers::Exported);
- let symbol_type = match symbol.class {
- SymbolClass::Function if is_external => BNSymbolType::ImportedFunction,
- // TODO: We should instead make it a Function, however due to the nature of the imports we are setting them to library for now.
- SymbolClass::Function => BNSymbolType::LibraryFunction,
- SymbolClass::Data if is_external => BNSymbolType::ImportedData,
- SymbolClass::Data => BNSymbolType::Data,
- };
- let raw_name = symbol.name.as_str();
- let mut symbol_builder = BNSymbol::builder(symbol_type, &symbol.name, addr);
- // Demangle symbol name (short is with simplifications).
- if let Some(arch) = view.default_arch() {
- if let Some((full_name, _)) =
- binaryninja::demangle::demangle_generic(&arch, raw_name, Some(view), false)
- {
- symbol_builder = symbol_builder.full_name(full_name);
- }
- if let Some((short_name, _)) =
- binaryninja::demangle::demangle_generic(&arch, raw_name, Some(view), false)
- {
- symbol_builder = symbol_builder.short_name(short_name);
- }
- }
- symbol_builder.create()
-}
-
-pub fn from_bn_type(view: &BinaryView, raw_ty: &BNType, confidence: u8) -> Type {
- from_bn_type_internal(view, &mut HashSet::new(), raw_ty, confidence)
-}
-
-pub fn from_bn_type_internal(
- view: &BinaryView,
- visited_refs: &mut HashSet<TypeRefID>,
- raw_ty: &BNType,
- confidence: u8,
-) -> Type {
- let bytes_to_bits = |val| val * 8;
- let raw_ty_bit_width = bytes_to_bits(raw_ty.width());
- let type_class = match raw_ty.type_class() {
- BNTypeClass::VoidTypeClass => TypeClass::Void,
- BNTypeClass::BoolTypeClass => {
- let bool_class = BooleanClass { width: None };
- TypeClass::Boolean(bool_class)
- }
- BNTypeClass::IntegerTypeClass => {
- let signed = raw_ty.is_signed().contents;
- let width = Some(raw_ty_bit_width as u16);
- if signed && width == Some(8) {
- // NOTE: if its an i8, its a char.
- let char_class = CharacterClass { width: None };
- TypeClass::Character(char_class)
- } else {
- let int_class = IntegerClass { width, signed };
- TypeClass::Integer(int_class)
- }
- }
- BNTypeClass::FloatTypeClass => {
- let float_class = FloatClass {
- width: Some(raw_ty_bit_width as u16),
- };
- TypeClass::Float(float_class)
- }
- // TODO: Union?????
- BNTypeClass::StructureTypeClass => {
- let raw_struct = raw_ty.get_structure().unwrap();
-
- let mut members = raw_struct
- .members()
- .into_iter()
- .map(|raw_member| {
- let bit_offset = bytes_to_bits(raw_member.offset);
- let mut modifiers = StructureMemberModifiers::empty();
- // If this member is not public mark it as internal.
- modifiers.set(
- StructureMemberModifiers::Internal,
- !matches!(raw_member.access, MemberAccess::PublicAccess),
- );
- StructureMember {
- name: Some(raw_member.name),
- offset: bit_offset,
- ty: from_bn_type_internal(
- view,
- visited_refs,
- &raw_member.ty.contents,
- raw_member.ty.confidence,
- ),
- modifiers,
- }
- })
- .collect::<Vec<_>>();
-
- // Add base structures as flattened members
- let base_to_member_iter = raw_struct.base_structures().into_iter().map(|base_struct| {
- let bit_offset = bytes_to_bits(base_struct.offset);
- let mut modifiers = StructureMemberModifiers::empty();
- modifiers.set(StructureMemberModifiers::Flattened, true);
- let base_struct_ty = from_bn_type_internal(
- view,
- visited_refs,
- &BNType::named_type(&base_struct.ty),
- MAX_CONFIDENCE,
- );
- StructureMember {
- name: base_struct_ty.name.to_owned(),
- offset: bit_offset,
- ty: base_struct_ty,
- modifiers,
- }
- });
- members.extend(base_to_member_iter);
-
- // TODO: Check if union
- let struct_class = StructureClass::new(members);
- TypeClass::Structure(struct_class)
- }
- BNTypeClass::EnumerationTypeClass => {
- let raw_enum = raw_ty.get_enumeration().unwrap();
-
- let enum_ty_signed = raw_ty.is_signed().contents;
- let enum_ty = Type::builder::<String, _>()
- .class(TypeClass::Integer(IntegerClass {
- width: Some(raw_ty_bit_width as u16),
- signed: enum_ty_signed,
- }))
- .build();
-
- let members = raw_enum
- .members()
- .into_iter()
- .map(|raw_member| EnumerationMember {
- name: Some(raw_member.name),
- constant: raw_member.value,
- })
- .collect();
-
- let enum_class = EnumerationClass::new(enum_ty, members);
- TypeClass::Enumeration(enum_class)
- }
- BNTypeClass::PointerTypeClass => {
- let raw_child_ty = raw_ty.target().unwrap();
- let ptr_class = PointerClass {
- width: Some(raw_ty_bit_width as u16),
- child_type: from_bn_type_internal(
- view,
- visited_refs,
- &raw_child_ty.contents,
- raw_child_ty.confidence,
- ),
- // TODO: Handle addressing.
- addressing: PointerAddressing::Absolute,
- };
- TypeClass::Pointer(ptr_class)
- }
- BNTypeClass::ArrayTypeClass => {
- let length = raw_ty.count();
- let raw_member_ty = raw_ty.element_type().unwrap();
- let array_class = ArrayClass {
- length: Some(length),
- member_type: from_bn_type_internal(
- view,
- visited_refs,
- &raw_member_ty.contents,
- raw_member_ty.confidence,
- ),
- modifiers: ArrayModifiers::empty(),
- };
- TypeClass::Array(array_class)
- }
- BNTypeClass::FunctionTypeClass => {
- let in_members = raw_ty
- .parameters()
- .unwrap()
- .into_iter()
- .map(|raw_member| {
- // TODO: Location...
- let _location = Location::Register(RegisterLocation);
- FunctionMember {
- name: Some(raw_member.name),
- ty: from_bn_type_internal(
- view,
- visited_refs,
- &raw_member.ty.contents,
- raw_member.ty.confidence,
- ),
- // TODO: Just omit location for now?
- // TODO: Location should be optional...
- locations: vec![],
- }
- })
- .collect();
-
- let mut out_members = Vec::new();
- if let Some(return_ty) = raw_ty.return_value() {
- out_members.push(FunctionMember {
- name: None,
- ty: from_bn_type_internal(
- view,
- visited_refs,
- &return_ty.contents,
- return_ty.confidence,
- ),
- locations: vec![],
- });
- }
-
- let calling_convention = raw_ty
- .calling_convention()
- .map(|bn_cc| from_bn_calling_convention(bn_cc.contents));
-
- let func_class = FunctionClass {
- calling_convention,
- in_members,
- out_members,
- };
- TypeClass::Function(func_class)
- }
- BNTypeClass::VarArgsTypeClass => TypeClass::Void,
- BNTypeClass::ValueTypeClass => {
- // What the is this.
- TypeClass::Void
- }
- BNTypeClass::NamedTypeReferenceClass => {
- let raw_ntr = raw_ty.get_named_type_reference().unwrap();
- let ref_id = TypeRefID::from(raw_ntr.as_ref());
- let mut ref_class = ReferrerClass::new(None, Some(raw_ntr.name().to_string()));
- if visited_refs.insert(ref_id) {
- // This ntr is NOT self-referential, meaning we can deduce a type GUID.
- if let Some(computed_ty) = cached_type_reference(view, visited_refs, &raw_ntr) {
- // NOTE: The GUID here must always equal the same for any given type for this to work effectively.
- ref_class.guid = Some(computed_ty.guid);
- }
- visited_refs.remove(&ref_id);
- }
- TypeClass::Referrer(ref_class)
- }
- BNTypeClass::WideCharTypeClass => {
- let char_class = CharacterClass {
- width: Some(raw_ty_bit_width as u16),
- };
- TypeClass::Character(char_class)
- }
- };
-
- let name = raw_ty.registered_name().map(|n| n.name().to_string());
-
- Type {
- name,
- class: Box::new(type_class),
- confidence,
- // TODO: Fill these out...
- modifiers: vec![],
- alignment: Default::default(),
- // TODO: Filling this out is... weird.
- // TODO: we _do_ want this for networked types (this is the only way we can update type is if we fill this out)
- ancestors: vec![],
+pub fn bn_comment_to_comment(func: &BNFunction, bn_comment: BNComment) -> FunctionComment {
+ let offset = (bn_comment.addr as i64) - (func.start() as i64);
+ FunctionComment {
+ offset,
+ text: bn_comment.comment,
}
}
-pub fn from_bn_calling_convention(raw_cc: BNRef<BNCallingConvention>) -> CallingConvention {
- // NOTE: Currently calling convention just stores the name.
- CallingConvention::new(raw_cc.name().as_str())
-}
-
-pub fn to_bn_calling_convention<A: BNArchitecture>(
- arch: &A,
- calling_convention: &CallingConvention,
-) -> BNRef<BNCallingConvention> {
- for cc in &arch.calling_conventions() {
- if cc.name().as_str() == calling_convention.name {
- return cc.clone();
- }
+pub fn comment_to_bn_comment(func: &BNFunction, comment: FunctionComment) -> BNComment {
+ BNComment {
+ addr: comment
+ .offset
+ .checked_add_unsigned(func.start())
+ .unwrap_or_default() as u64,
+ comment: comment.text,
}
- arch.get_default_calling_convention().unwrap()
}
-pub fn to_bn_type<A: BNArchitecture>(arch: &A, ty: &Type) -> BNRef<BNType> {
- let bits_to_bytes = |val: u64| (val / 8);
- let addr_size = arch.address_size() as u64;
- match ty.class.as_ref() {
- TypeClass::Void => BNType::void(),
- TypeClass::Boolean(_) => BNType::bool(),
- TypeClass::Integer(c) => {
- let width = c.width.map(|w| bits_to_bytes(w as _)).unwrap_or(4);
- BNType::int(width as usize, c.signed)
- }
- TypeClass::Character(c) => match c.width {
- Some(w) => BNType::wide_char(bits_to_bytes(w as _) as usize),
- None => BNType::char(),
- },
- TypeClass::Float(c) => {
- let width = c.width.map(|w| bits_to_bytes(w as _)).unwrap_or(4);
- BNType::float(width as usize)
+pub fn platform_to_target(platform: &Platform) -> Target {
+ let arch_name = platform.arch().name();
+ let platform_name = platform.name();
+ // We do not want to populate the platform if we are actually only the architecture.
+ if arch_name == platform_name {
+ Target {
+ architecture: Some(arch_name),
+ platform: None,
}
- TypeClass::Pointer(ref c) => {
- let child_type = to_bn_type(arch, &c.child_type);
- let ptr_width = c.width.map(|w| bits_to_bytes(w as _)).unwrap_or(addr_size);
- // TODO: Child type confidence
- let constant = ty.is_const();
- let volatile = ty.is_volatile();
- // TODO: If the pointer is to a null terminated array of chars, make it a pointer to char
- // TODO: Addressing mode
- BNType::pointer_of_width(&child_type, ptr_width as usize, constant, volatile, None)
- }
- TypeClass::Array(c) => {
- let member_type = to_bn_type(arch, &c.member_type);
- // TODO: How to handle DST array (length is None)
- BNType::array(&member_type, c.length.unwrap_or(0))
- }
- TypeClass::Structure(c) => {
- let mut builder = BNStructureBuilder::new();
- // TODO: Structure type class?
- // TODO: Alignment
- // TODO: Other modifiers?
- let mut base_structs: Vec<BNBaseStructure> = Vec::new();
- for member in &c.members {
- let member_type = BNConf::new(to_bn_type(arch, &member.ty), u8::MAX);
- let member_name = member.name.to_owned().unwrap_or("field_OFFSET".into());
- let member_offset = bits_to_bytes(member.offset);
- let member_access = if member
- .modifiers
- .contains(StructureMemberModifiers::Internal)
- {
- BNMemberAccess::PrivateAccess
- } else {
- BNMemberAccess::PublicAccess
- };
- // TODO: Member scope
- let member_scope = BNMemberScope::NoScope;
- if member
- .modifiers
- .contains(StructureMemberModifiers::Flattened)
- {
- // Add member as a base structure to inherit its fields.
- match member.ty.class.as_ref() {
- TypeClass::Referrer(c) => {
- // We only support base structures with a referrer right now.
- let base_struct_ntr_name =
- c.name.to_owned().unwrap_or("base_UNKNOWN".into());
- let base_struct_ntr = match c.guid {
- Some(guid) => BNNamedTypeReference::new_with_id(
- NamedTypeReferenceClass::UnknownNamedTypeClass,
- &guid.to_string(),
- base_struct_ntr_name,
- ),
- None => BNNamedTypeReference::new(
- NamedTypeReferenceClass::UnknownNamedTypeClass,
- base_struct_ntr_name,
- ),
- };
- base_structs.push(BNBaseStructure::new(
- base_struct_ntr,
- member_offset,
- member.ty.size().unwrap_or(0),
- ))
- }
- _ => {
- log::error!(
- "Adding base {:?} with invalid ty: {:?}",
- ty.name,
- member.ty
- );
- }
- }
- } else {
- builder.insert_member(
- BNStructureMember::new(
- member_type,
- member_name,
- member_offset,
- member_access,
- member_scope,
- ),
- false,
- );
- }
- }
- builder.base_structures(&base_structs);
- BNType::structure(&builder.finalize())
- }
- TypeClass::Enumeration(c) => {
- let mut builder = BNEnumerationBuilder::new();
- for member in &c.members {
- // TODO: Add default name?
- let member_name = member.name.to_owned().unwrap_or("enum_VAL".into());
- let member_value = member.constant;
- builder.insert(&member_name, member_value);
- }
- // TODO: Warn if enumeration has no size.
- let width = bits_to_bytes(c.member_type.size().unwrap()) as usize;
- let signed = matches!(*c.member_type.class, TypeClass::Integer(c) if c.signed);
- // TODO: Passing width like this is weird.
- BNType::enumeration(&builder.finalize(), width.try_into().unwrap(), signed)
- }
- TypeClass::Union(c) => {
- let mut builder = BNStructureBuilder::new();
- builder.structure_type(BNStructureType::UnionStructureType);
- for member in &c.members {
- let member_type = BNConf::new(to_bn_type(arch, &member.ty), u8::MAX);
- let member_name = member.name.to_owned();
- // TODO: Member access
- let member_access = BNMemberAccess::PublicAccess;
- // TODO: Member scope
- let member_scope = BNMemberScope::NoScope;
- let structure_member = BNStructureMember::new(
- member_type,
- member_name,
- 0, // Union members all exist at 0 right?
- member_access,
- member_scope,
- );
- builder.insert_member(structure_member, false);
- }
- BNType::structure(&builder.finalize())
- }
- TypeClass::Function(c) => {
- let return_type = if !c.out_members.is_empty() {
- // TODO: WTF
- to_bn_type(arch, &c.out_members[0].ty)
- } else {
- BNType::void()
- };
- let params: Vec<_> = c
- .in_members
- .iter()
- .map(|member| {
- let member_type = to_bn_type(arch, &member.ty);
- let name = member.name.clone();
- // TODO: Location AND fix default param name
- BNFunctionParameter::new(member_type, name.unwrap_or("param_IDK".into()), None)
- })
- .collect();
- // TODO: Variable arguments
- let variable_args = false;
- // If we have a calling convention we run the extended function type creation.
- match c.calling_convention.as_ref() {
- Some(cc) => {
- let calling_convention = to_bn_calling_convention(arch, cc);
- BNType::function_with_opts(
- &return_type,
- &params,
- variable_args,
- BNConf::new(calling_convention, u8::MAX),
- BNConf::new(0, 0),
- )
- }
- None => BNType::function(&return_type, params, variable_args),
- }
- }
- TypeClass::Referrer(c) => {
- let ntr = match c.guid {
- Some(guid) => {
- let guid_str = guid.to_string();
- let ntr_name = c.name.to_owned().unwrap_or(guid_str.clone());
- NamedTypeReference::new_with_id(
- NamedTypeReferenceClass::UnknownNamedTypeClass,
- &guid_str,
- ntr_name,
- )
- }
- None => match c.name.as_ref() {
- Some(ntr_name) => NamedTypeReference::new(
- NamedTypeReferenceClass::UnknownNamedTypeClass,
- ntr_name,
- ),
- None => {
- log::error!("Referrer with no reference! {:?}", c);
- NamedTypeReference::new(
- NamedTypeReferenceClass::UnknownNamedTypeClass,
- "AHHHHHH",
- )
- }
- },
- };
- BNType::named_type(&ntr)
+ } else {
+ Target {
+ architecture: Some(arch_name),
+ platform: Some(platform_name),
}
}
}
-#[cfg(test)]
-mod tests {
- use super::*;
- use binaryninja::binary_view::BinaryViewExt;
- use binaryninja::headless::Session;
- use std::path::PathBuf;
- use std::sync::OnceLock;
- use warp::r#type::guid::TypeGUID;
-
- static INIT: OnceLock<Session> = OnceLock::new();
-
- fn get_session<'a>() -> &'a Session {
- INIT.get_or_init(|| Session::new().expect("Failed to initialize session"))
- }
-
- #[test]
- fn type_conversion() {
- let session = get_session();
- let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap();
- for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") {
- let entry = entry.expect("Failed to read directory entry");
- let path = entry.path();
- if path.is_file() {
- if let Some(bv) = session.load(path.to_str().unwrap()) {
- let types_len = bv.types().len();
- let converted_types: Vec<_> = bv
- .types()
- .iter()
- .map(|qualified_name_and_type| {
- let ty = from_bn_type(&bv, &qualified_name_and_type.ty, u8::MAX);
- (TypeGUID::from(&ty), ty)
- })
- .collect();
- assert_eq!(types_len, converted_types.len());
- }
- }
- }
- }
-
- #[ignore]
- #[test]
- fn check_for_leaks() {
- let session = get_session();
- let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap();
- for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") {
- let entry = entry.expect("Failed to read directory entry");
- let path = entry.path();
- if path.is_file() {
- if let Some(inital_bv) = session.load(path.to_str().unwrap()) {
- let types_len = inital_bv.types().len();
- let converted_types: Vec<_> = inital_bv
- .types()
- .iter()
- .map(|t| {
- let ty = from_bn_type(&inital_bv, &t.ty, u8::MAX);
- (TypeGUID::from(&ty), ty)
- })
- .collect();
- assert_eq!(types_len, converted_types.len());
- // Hold on to a reference to the core to prevent view getting dropped in worker thread.
- let _core_ref = inital_bv
- .functions()
- .iter()
- .next()
- .map(|f| f.unresolved_stack_adjustment_graph());
- // Drop the file and view.
- inital_bv.file().close();
- std::mem::drop(inital_bv);
- let initial_memory_info = binaryninja::memory_info();
- if let Some(second_bv) = session.load(path.to_str().unwrap()) {
- let types_len = second_bv.types().len();
- let converted_types: Vec<_> = second_bv
- .types()
- .iter()
- .map(|t| {
- let ty = from_bn_type(&second_bv, &t.ty, u8::MAX);
- (TypeGUID::from(&ty), ty)
- })
- .collect();
- assert_eq!(types_len, converted_types.len());
- // Hold on to a reference to the core to prevent view getting dropped in worker thread.
- let _core_ref = second_bv
- .functions()
- .iter()
- .next()
- .map(|f| f.unresolved_stack_adjustment_graph());
- // Drop the file and view.
- second_bv.file().close();
- std::mem::drop(second_bv);
- let final_memory_info = binaryninja::memory_info();
- for info in initial_memory_info {
- let initial_count = info.1;
- if let Some(&final_count) = final_memory_info.get(&info.0) {
- assert!(
- final_count <= initial_count,
- "{}: final objects {} vs initial objects {}",
- info.0,
- final_count,
- initial_count
- );
- }
- }
- }
- }
- }
- }
+pub fn target_to_platform(target: Target) -> Option<Ref<Platform>> {
+ // First try using the platform, then try using the arch.
+ match Platform::by_name(&target.platform.unwrap()) {
+ None => Platform::by_name(&target.architecture.unwrap()).map(|platform| platform),
+ Some(platform) => Some(platform),
}
}
diff --git a/plugins/warp/src/convert/symbol.rs b/plugins/warp/src/convert/symbol.rs
new file mode 100644
index 00000000..0107cff5
--- /dev/null
+++ b/plugins/warp/src/convert/symbol.rs
@@ -0,0 +1,91 @@
+use binaryninja::binary_view::{BinaryView, BinaryViewExt};
+use binaryninja::rc::Ref as BNRef;
+use binaryninja::symbol::Symbol as BNSymbol;
+use binaryninja::symbol::SymbolType as BNSymbolType;
+use warp::symbol::{Symbol, SymbolClass, SymbolModifiers};
+
+pub fn from_bn_symbol(raw_symbol: &BNSymbol) -> Symbol {
+ // TODO: Use this?
+ let _is_export = raw_symbol.external();
+ let raw_symbol_name = raw_symbol.raw_name();
+ let symbol_name = raw_symbol_name.to_string_lossy();
+ match raw_symbol.sym_type() {
+ BNSymbolType::ImportAddress => {
+ Symbol::new(
+ symbol_name,
+ SymbolClass::Function,
+ // TODO: External = symbolic i guess
+ SymbolModifiers::External,
+ )
+ }
+ BNSymbolType::Data => {
+ Symbol::new(
+ symbol_name,
+ // TODO: Data?
+ SymbolClass::Data,
+ SymbolModifiers::default(),
+ )
+ }
+ BNSymbolType::Symbolic => {
+ Symbol::new(
+ symbol_name,
+ SymbolClass::Function,
+ // TODO: External = symbolic i guess
+ SymbolModifiers::External,
+ )
+ }
+ BNSymbolType::LocalLabel => {
+ // TODO: This is a placeholder for another symbol.
+ Symbol::new(symbol_name, SymbolClass::Data, SymbolModifiers::External)
+ }
+ BNSymbolType::External => Symbol::new(
+ symbol_name,
+ // TODO: External data?
+ SymbolClass::Function,
+ SymbolModifiers::External,
+ ),
+ BNSymbolType::ImportedData => {
+ Symbol::new(symbol_name, SymbolClass::Data, SymbolModifiers::External)
+ }
+ BNSymbolType::LibraryFunction | BNSymbolType::Function => Symbol::new(
+ symbol_name,
+ SymbolClass::Function,
+ SymbolModifiers::default(),
+ ),
+ BNSymbolType::ImportedFunction => Symbol::new(
+ symbol_name,
+ SymbolClass::Function,
+ // TODO: Exported?
+ SymbolModifiers::External,
+ ),
+ }
+}
+
+pub fn to_bn_symbol_at_address(view: &BinaryView, symbol: &Symbol, addr: u64) -> BNRef<BNSymbol> {
+ let is_external = symbol.modifiers.contains(SymbolModifiers::External);
+ let _is_exported = symbol.modifiers.contains(SymbolModifiers::Exported);
+ let symbol_type = match symbol.class {
+ SymbolClass::Function if is_external => BNSymbolType::ImportedFunction,
+ // TODO: We should instead make it a Function, however due to the nature of the imports we are setting them to library for now.
+ SymbolClass::Function => BNSymbolType::LibraryFunction,
+ SymbolClass::Data if is_external => BNSymbolType::ImportedData,
+ SymbolClass::Data => BNSymbolType::Data,
+ _ => BNSymbolType::Data,
+ };
+ let raw_name = symbol.name.as_str();
+ let mut symbol_builder = BNSymbol::builder(symbol_type, &symbol.name, addr);
+ // Demangle symbol name (short is with simplifications).
+ if let Some(arch) = view.default_arch() {
+ if let Some((full_name, _)) =
+ binaryninja::demangle::demangle_generic(&arch, raw_name, Some(view), false)
+ {
+ symbol_builder = symbol_builder.full_name(full_name);
+ }
+ if let Some((short_name, _)) =
+ binaryninja::demangle::demangle_generic(&arch, raw_name, Some(view), false)
+ {
+ symbol_builder = symbol_builder.short_name(short_name);
+ }
+ }
+ symbol_builder.create()
+}
diff --git a/plugins/warp/src/convert/types.rs b/plugins/warp/src/convert/types.rs
new file mode 100644
index 00000000..1dfe5fa9
--- /dev/null
+++ b/plugins/warp/src/convert/types.rs
@@ -0,0 +1,516 @@
+use crate::cache::{cached_type_reference, TypeRefID};
+use binaryninja::architecture::Architecture as BNArchitecture;
+use binaryninja::architecture::ArchitectureExt;
+use binaryninja::binary_view::BinaryView;
+use binaryninja::calling_convention::CoreCallingConvention as BNCallingConvention;
+use binaryninja::confidence::Conf as BNConf;
+use binaryninja::confidence::MAX_CONFIDENCE;
+use binaryninja::rc::Ref as BNRef;
+use binaryninja::types::BaseStructure as BNBaseStructure;
+use binaryninja::types::EnumerationBuilder as BNEnumerationBuilder;
+use binaryninja::types::FunctionParameter as BNFunctionParameter;
+use binaryninja::types::MemberAccess as BNMemberAccess;
+use binaryninja::types::MemberScope as BNMemberScope;
+use binaryninja::types::NamedTypeReference as BNNamedTypeReference;
+use binaryninja::types::StructureBuilder as BNStructureBuilder;
+use binaryninja::types::StructureMember as BNStructureMember;
+use binaryninja::types::StructureType as BNStructureType;
+use binaryninja::types::Type as BNType;
+use binaryninja::types::TypeClass as BNTypeClass;
+use binaryninja::types::{NamedTypeReference, NamedTypeReferenceClass};
+use std::collections::HashSet;
+use warp::r#type::class::array::ArrayModifiers;
+use warp::r#type::class::pointer::PointerAddressing;
+use warp::r#type::class::structure::StructureMemberModifiers;
+use warp::r#type::class::{
+ ArrayClass, BooleanClass, CallingConvention, CharacterClass, EnumerationClass,
+ EnumerationMember, FloatClass, FunctionClass, FunctionMember, IntegerClass, PointerClass,
+ ReferrerClass, StructureClass, StructureMember, TypeClass,
+};
+use warp::r#type::{Type, TypeModifiers};
+
+pub fn from_bn_type(view: &BinaryView, raw_ty: &BNType, confidence: u8) -> Type {
+ from_bn_type_internal(view, &mut HashSet::new(), raw_ty, confidence)
+}
+
+pub fn from_bn_type_internal(
+ view: &BinaryView,
+ visited_refs: &mut HashSet<TypeRefID>,
+ raw_ty: &BNType,
+ confidence: u8,
+) -> Type {
+ let bytes_to_bits = |val| val * 8;
+ let raw_ty_bit_width = bytes_to_bits(raw_ty.width());
+ let type_class = match raw_ty.type_class() {
+ BNTypeClass::VoidTypeClass => TypeClass::Void,
+ BNTypeClass::BoolTypeClass => {
+ let bool_class = BooleanClass { width: None };
+ TypeClass::Boolean(bool_class)
+ }
+ BNTypeClass::IntegerTypeClass => {
+ let signed = raw_ty.is_signed().contents;
+ let width = Some(raw_ty_bit_width as u16);
+ if signed && width == Some(8) {
+ // NOTE: if its an i8, its a char.
+ let char_class = CharacterClass { width: None };
+ TypeClass::Character(char_class)
+ } else {
+ let int_class = IntegerClass { width, signed };
+ TypeClass::Integer(int_class)
+ }
+ }
+ BNTypeClass::FloatTypeClass => {
+ let float_class = FloatClass {
+ width: Some(raw_ty_bit_width as u16),
+ };
+ TypeClass::Float(float_class)
+ }
+ // TODO: Union?????
+ BNTypeClass::StructureTypeClass => {
+ let raw_struct = raw_ty.get_structure().unwrap();
+
+ let mut members = raw_struct
+ .members()
+ .into_iter()
+ .map(|raw_member| {
+ let bit_offset = bytes_to_bits(raw_member.offset);
+ let mut modifiers = StructureMemberModifiers::empty();
+ // If this member is not public mark it as internal.
+ modifiers.set(
+ StructureMemberModifiers::Internal,
+ !matches!(raw_member.access, BNMemberAccess::PublicAccess),
+ );
+ StructureMember {
+ name: Some(raw_member.name),
+ offset: bit_offset,
+ ty: Box::new(from_bn_type_internal(
+ view,
+ visited_refs,
+ &raw_member.ty.contents,
+ raw_member.ty.confidence,
+ )),
+ modifiers,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ // Add base structures as flattened members
+ let base_to_member_iter = raw_struct.base_structures().into_iter().map(|base_struct| {
+ let bit_offset = bytes_to_bits(base_struct.offset);
+ let mut modifiers = StructureMemberModifiers::empty();
+ modifiers.set(StructureMemberModifiers::Flattened, true);
+ let base_struct_ty = from_bn_type_internal(
+ view,
+ visited_refs,
+ &BNType::named_type(&base_struct.ty),
+ MAX_CONFIDENCE,
+ );
+ StructureMember {
+ name: base_struct_ty.name.to_owned(),
+ offset: bit_offset,
+ ty: Box::new(base_struct_ty),
+ modifiers,
+ }
+ });
+ members.extend(base_to_member_iter);
+
+ // TODO: Check if union
+ let struct_class = StructureClass::new(members);
+ TypeClass::Structure(struct_class)
+ }
+ BNTypeClass::EnumerationTypeClass => {
+ let raw_enum = raw_ty.get_enumeration().unwrap();
+
+ let enum_ty_signed = raw_ty.is_signed().contents;
+ let enum_ty = Type::builder::<String, _>()
+ .class(TypeClass::Integer(IntegerClass {
+ width: Some(raw_ty_bit_width as u16),
+ signed: enum_ty_signed,
+ }))
+ .build();
+
+ let members = raw_enum
+ .members()
+ .into_iter()
+ .map(|raw_member| EnumerationMember {
+ name: Some(raw_member.name),
+ constant: raw_member.value,
+ })
+ .collect();
+
+ let enum_class = EnumerationClass::new(enum_ty, members);
+ TypeClass::Enumeration(enum_class)
+ }
+ BNTypeClass::PointerTypeClass => {
+ let raw_child_ty = raw_ty.target().unwrap();
+ let ptr_class = PointerClass {
+ width: Some(raw_ty_bit_width as u16),
+ child_type: Box::new(from_bn_type_internal(
+ view,
+ visited_refs,
+ &raw_child_ty.contents,
+ raw_child_ty.confidence,
+ )),
+ // TODO: Handle addressing.
+ addressing: PointerAddressing::Absolute,
+ };
+ TypeClass::Pointer(ptr_class)
+ }
+ BNTypeClass::ArrayTypeClass => {
+ let length = raw_ty.count();
+ let raw_member_ty = raw_ty.element_type().unwrap();
+ let array_class = ArrayClass {
+ length: Some(length),
+ member_type: Box::new(from_bn_type_internal(
+ view,
+ visited_refs,
+ &raw_member_ty.contents,
+ raw_member_ty.confidence,
+ )),
+ modifiers: ArrayModifiers::empty(),
+ };
+ TypeClass::Array(array_class)
+ }
+ BNTypeClass::FunctionTypeClass => {
+ let in_members = raw_ty
+ .parameters()
+ .unwrap()
+ .into_iter()
+ .map(|raw_member| {
+ // TODO: Location...
+ // let _location = Location::Register(RegisterLocation);
+ FunctionMember {
+ name: Some(raw_member.name),
+ ty: Box::new(from_bn_type_internal(
+ view,
+ visited_refs,
+ &raw_member.ty.contents,
+ raw_member.ty.confidence,
+ )),
+ // TODO: Just omit location for now?
+ // TODO: Location should be optional...
+ location: None,
+ }
+ })
+ .collect();
+
+ let mut out_members = Vec::new();
+ if let Some(return_ty) = raw_ty.return_value() {
+ out_members.push(FunctionMember {
+ name: None,
+ ty: Box::new(from_bn_type_internal(
+ view,
+ visited_refs,
+ &return_ty.contents,
+ return_ty.confidence,
+ )),
+ location: None,
+ });
+ }
+
+ let calling_convention = raw_ty
+ .calling_convention()
+ .map(|bn_cc| from_bn_calling_convention(bn_cc.contents));
+
+ let func_class = FunctionClass {
+ calling_convention,
+ in_members,
+ out_members,
+ };
+ TypeClass::Function(func_class)
+ }
+ BNTypeClass::VarArgsTypeClass => TypeClass::Void,
+ BNTypeClass::ValueTypeClass => {
+ // What the is this.
+ TypeClass::Void
+ }
+ BNTypeClass::NamedTypeReferenceClass => {
+ let raw_ntr = raw_ty.get_named_type_reference().unwrap();
+ let ref_id = TypeRefID::from(raw_ntr.as_ref());
+ let mut ref_class = ReferrerClass::new(None, Some(raw_ntr.name().to_string()));
+ if visited_refs.insert(ref_id) {
+ // This ntr is NOT self-referential, meaning we can deduce a type GUID.
+ if let Some(computed_ty) = cached_type_reference(view, visited_refs, &raw_ntr) {
+ // NOTE: The GUID here must always equal the same for any given type for this to work effectively.
+ ref_class.guid = Some(computed_ty.guid);
+ }
+ visited_refs.remove(&ref_id);
+ }
+ TypeClass::Referrer(ref_class)
+ }
+ BNTypeClass::WideCharTypeClass => {
+ let char_class = CharacterClass {
+ width: Some(raw_ty_bit_width as u16),
+ };
+ TypeClass::Character(char_class)
+ }
+ };
+
+ let name = raw_ty.registered_name().map(|n| n.name().to_string());
+
+ Type {
+ name,
+ class: type_class,
+ confidence,
+ // TODO: Fill these out...
+ modifiers: TypeModifiers::empty(),
+ metadata: vec![],
+ alignment: Default::default(),
+ // TODO: Filling this out is... weird.
+ // TODO: we _do_ want this for networked types (this is the only way we can update type is if we fill this out)
+ ancestors: vec![],
+ }
+}
+
+pub fn from_bn_calling_convention(raw_cc: BNRef<BNCallingConvention>) -> CallingConvention {
+ // NOTE: Currently calling convention just stores the name.
+ CallingConvention::new(raw_cc.name().as_str())
+}
+
+pub fn to_bn_calling_convention<A: BNArchitecture>(
+ arch: &A,
+ calling_convention: &CallingConvention,
+) -> BNRef<BNCallingConvention> {
+ for cc in &arch.calling_conventions() {
+ if cc.name().as_str() == calling_convention.name {
+ return cc.clone();
+ }
+ }
+ arch.get_default_calling_convention().unwrap()
+}
+
+pub fn to_bn_type<A: BNArchitecture>(arch: &A, ty: &Type) -> BNRef<BNType> {
+ let bits_to_bytes = |val: u64| (val / 8);
+ let addr_size = arch.address_size() as u64;
+ match &ty.class {
+ TypeClass::Void => BNType::void(),
+ TypeClass::Boolean(_) => BNType::bool(),
+ TypeClass::Integer(c) => {
+ let width = c.width.map(|w| bits_to_bytes(w as _)).unwrap_or(4);
+ BNType::int(width as usize, c.signed)
+ }
+ TypeClass::Character(c) => match c.width {
+ Some(w) => BNType::wide_char(bits_to_bytes(w as _) as usize),
+ None => BNType::char(),
+ },
+ TypeClass::Float(c) => {
+ let width = c.width.map(|w| bits_to_bytes(w as _)).unwrap_or(4);
+ BNType::float(width as usize)
+ }
+ TypeClass::Pointer(ref c) => {
+ let child_type = to_bn_type(arch, &c.child_type);
+ let ptr_width = c.width.map(|w| bits_to_bytes(w as _)).unwrap_or(addr_size);
+ // TODO: Child type confidence
+ let constant = ty.is_const();
+ let volatile = ty.is_volatile();
+ // TODO: If the pointer is to a null terminated array of chars, make it a pointer to char
+ // TODO: Addressing mode
+ BNType::pointer_of_width(&child_type, ptr_width as usize, constant, volatile, None)
+ }
+ TypeClass::Array(c) => {
+ let member_type = to_bn_type(arch, &c.member_type);
+ // TODO: How to handle DST array (length is None)
+ BNType::array(&member_type, c.length.unwrap_or(0))
+ }
+ TypeClass::Structure(c) => {
+ let mut builder = BNStructureBuilder::new();
+ // TODO: Structure type class?
+ // TODO: Alignment
+ // TODO: Other modifiers?
+ let mut base_structs: Vec<BNBaseStructure> = Vec::new();
+ for member in &c.members {
+ let member_type = BNConf::new(to_bn_type(arch, &member.ty), u8::MAX);
+ let member_name = member.name.to_owned().unwrap_or("field_OFFSET".into());
+ let member_offset = bits_to_bytes(member.offset);
+ let member_access = if member
+ .modifiers
+ .contains(StructureMemberModifiers::Internal)
+ {
+ BNMemberAccess::PrivateAccess
+ } else {
+ BNMemberAccess::PublicAccess
+ };
+ // TODO: Member scope
+ let member_scope = BNMemberScope::NoScope;
+ if member
+ .modifiers
+ .contains(StructureMemberModifiers::Flattened)
+ {
+ // Add member as a base structure to inherit its fields.
+ match &member.ty.class {
+ TypeClass::Referrer(c) => {
+ // We only support base structures with a referrer right now.
+ let base_struct_ntr_name =
+ c.name.to_owned().unwrap_or("base_UNKNOWN".into());
+ let base_struct_ntr = match c.guid {
+ Some(guid) => BNNamedTypeReference::new_with_id(
+ NamedTypeReferenceClass::UnknownNamedTypeClass,
+ &guid.to_string(),
+ base_struct_ntr_name,
+ ),
+ None => BNNamedTypeReference::new(
+ NamedTypeReferenceClass::UnknownNamedTypeClass,
+ base_struct_ntr_name,
+ ),
+ };
+ base_structs.push(BNBaseStructure::new(
+ base_struct_ntr,
+ member_offset,
+ member.ty.size().unwrap_or(0),
+ ))
+ }
+ _ => {
+ log::error!(
+ "Adding base {:?} with invalid ty: {:?}",
+ ty.name,
+ member.ty
+ );
+ }
+ }
+ } else {
+ builder.insert_member(
+ BNStructureMember::new(
+ member_type,
+ member_name,
+ member_offset,
+ member_access,
+ member_scope,
+ ),
+ false,
+ );
+ }
+ }
+ builder.base_structures(&base_structs);
+ BNType::structure(&builder.finalize())
+ }
+ TypeClass::Enumeration(c) => {
+ let mut builder = BNEnumerationBuilder::new();
+ for member in &c.members {
+ // TODO: Add default name?
+ let member_name = member.name.to_owned().unwrap_or("enum_VAL".into());
+ let member_value = member.constant;
+ builder.insert(&member_name, member_value);
+ }
+ // TODO: Warn if enumeration has no size.
+ let width = bits_to_bytes(c.member_type.size().unwrap()) as usize;
+ let signed = matches!(c.member_type.class, TypeClass::Integer(c) if c.signed);
+ // TODO: Passing width like this is weird.
+ BNType::enumeration(&builder.finalize(), width.try_into().unwrap(), signed)
+ }
+ TypeClass::Union(c) => {
+ let mut builder = BNStructureBuilder::new();
+ builder.structure_type(BNStructureType::UnionStructureType);
+ for member in &c.members {
+ let member_type = BNConf::new(to_bn_type(arch, &member.ty), u8::MAX);
+ let member_name = member.name.to_owned();
+ // TODO: Member access
+ let member_access = BNMemberAccess::PublicAccess;
+ // TODO: Member scope
+ let member_scope = BNMemberScope::NoScope;
+ let structure_member = BNStructureMember::new(
+ member_type,
+ member_name,
+ 0, // Union members all exist at 0 right?
+ member_access,
+ member_scope,
+ );
+ builder.insert_member(structure_member, false);
+ }
+ BNType::structure(&builder.finalize())
+ }
+ TypeClass::Function(c) => {
+ let return_type = if !c.out_members.is_empty() {
+ // TODO: WTF
+ to_bn_type(arch, &c.out_members[0].ty)
+ } else {
+ BNType::void()
+ };
+ let params: Vec<_> = c
+ .in_members
+ .iter()
+ .map(|member| {
+ let member_type = to_bn_type(arch, &member.ty);
+ let name = member.name.clone();
+ // TODO: Location AND fix default param name
+ BNFunctionParameter::new(member_type, name.unwrap_or("param_IDK".into()), None)
+ })
+ .collect();
+ // TODO: Variable arguments
+ let variable_args = false;
+ // If we have a calling convention we run the extended function type creation.
+ match c.calling_convention.as_ref() {
+ Some(cc) => {
+ let calling_convention = to_bn_calling_convention(arch, cc);
+ BNType::function_with_opts(
+ &return_type,
+ &params,
+ variable_args,
+ BNConf::new(calling_convention, u8::MAX),
+ BNConf::new(0, 0),
+ )
+ }
+ None => BNType::function(&return_type, params, variable_args),
+ }
+ }
+ TypeClass::Referrer(c) => {
+ let ntr = match c.guid {
+ Some(guid) => {
+ let guid_str = guid.to_string();
+ let ntr_name = c.name.to_owned().unwrap_or(guid_str.clone());
+ NamedTypeReference::new_with_id(
+ NamedTypeReferenceClass::TypedefNamedTypeClass,
+ &guid_str,
+ ntr_name,
+ )
+ }
+ None => match c.name.as_ref() {
+ Some(ntr_name) => NamedTypeReference::new(
+ NamedTypeReferenceClass::UnknownNamedTypeClass,
+ ntr_name,
+ ),
+ None => {
+ log::error!("Referrer with no reference! {:?}", c);
+ NamedTypeReference::new(
+ NamedTypeReferenceClass::UnknownNamedTypeClass,
+ "AHHHHHH",
+ )
+ }
+ },
+ };
+ BNType::named_type(&ntr)
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use binaryninja::binary_view::BinaryViewExt;
+ use binaryninja::headless::Session;
+ use std::path::PathBuf;
+ use warp::r#type::guid::TypeGUID;
+
+ #[test]
+ fn type_conversion() {
+ let session = Session::new().expect("Failed to initialize session");
+ let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap();
+ for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") {
+ let entry = entry.expect("Failed to read directory entry");
+ let path = entry.path();
+ if path.is_file() {
+ if let Some(bv) = session.load(path) {
+ let types_len = bv.types().len();
+ let converted_types: Vec<_> = bv
+ .types()
+ .iter()
+ .map(|qualified_name_and_type| {
+ let ty = from_bn_type(&bv, &qualified_name_and_type.ty, u8::MAX);
+ (TypeGUID::from(&ty), ty)
+ })
+ .collect();
+ assert_eq!(types_len, converted_types.len());
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/warp/src/lib.rs b/plugins/warp/src/lib.rs
index 6d147bdc..aab26932 100644
--- a/plugins/warp/src/lib.rs
+++ b/plugins/warp/src/lib.rs
@@ -1,7 +1,5 @@
-use crate::cache::{
- cached_adjacency_constraints, cached_call_site_constraints, cached_function_guid,
-};
-use crate::convert::{from_bn_symbol, from_bn_type};
+use crate::cache::{cached_constraints, cached_function_guid};
+use crate::convert::{bn_comment_to_comment, from_bn_symbol, from_bn_type};
use binaryninja::architecture::{
Architecture, ImplicitRegisterExtend, Register as BNRegister, RegisterInfo,
};
@@ -9,25 +7,55 @@ use binaryninja::basic_block::BasicBlock as BNBasicBlock;
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
use binaryninja::confidence::MAX_CONFIDENCE;
use binaryninja::function::{Function as BNFunction, NativeBlock};
-use binaryninja::low_level_il::expression::{ExpressionHandler, LowLevelILExpressionKind};
+use binaryninja::low_level_il::expression::{
+ ExpressionHandler, LowLevelILExpression, LowLevelILExpressionKind, ValueExpr,
+};
use binaryninja::low_level_il::function::{FunctionMutability, LowLevelILFunction, NonSSA};
use binaryninja::low_level_il::instruction::{
InstructionHandler, LowLevelILInstruction, LowLevelILInstructionKind,
};
use binaryninja::low_level_il::{LowLevelILRegisterKind, VisitorAction};
-use binaryninja::rc::Ref as BNRef;
+use binaryninja::rc::{Ref as BNRef, Ref};
use std::ops::Range;
use std::path::PathBuf;
use warp::signature::basic_block::BasicBlockGUID;
-use warp::signature::function::constraints::FunctionConstraints;
use warp::signature::function::{Function, FunctionGUID};
+use binaryninja::tags::TagType;
+use binaryninja::variable::RegisterValueType;
+/// Re-export the warp crate that is used, this is useful for consumers of this crate.
+pub use warp;
+
pub mod cache;
+pub mod container;
pub mod convert;
-mod matcher;
+pub mod matcher;
+pub mod processor;
+pub mod report;
+
/// Only used when compiled for cdylib target.
mod plugin;
+// TODO: Make this 4kb
+/// If the address is within this range before or after a relocatable region, we will assume the address to be relocatable.
+const ADDRESS_RELOCATION_THRESHOLD: u64 = 0x10000;
+
+const TAG_ICON: &str = "🌐";
+const TAG_NAME: &str = "WARP";
+
+fn get_warp_tag_type(view: &BinaryView) -> Ref<TagType> {
+ view.tag_type_by_name(TAG_NAME)
+ .unwrap_or_else(|| view.create_tag_type(TAG_NAME, TAG_ICON))
+}
+
+const INCLUDE_TAG_ICON: &str = "🚀";
+const INCLUDE_TAG_NAME: &str = "WARP: Selected Function";
+
+fn get_warp_include_tag_type(view: &BinaryView) -> Ref<TagType> {
+ view.tag_type_by_name(INCLUDE_TAG_NAME)
+ .unwrap_or_else(|| view.create_tag_type(INCLUDE_TAG_NAME, INCLUDE_TAG_ICON))
+}
+
pub fn core_signature_dir() -> PathBuf {
// Get core signatures for the given platform
let install_dir = binaryninja::install_directory();
@@ -45,22 +73,34 @@ pub fn user_signature_dir() -> PathBuf {
pub fn build_function<M: FunctionMutability>(
func: &BNFunction,
- llil: &LowLevelILFunction<M, NonSSA>,
+ lifted_il: &LowLevelILFunction<M, NonSSA>,
) -> Function {
- let bn_fn_ty = func.function_type();
+ let comments = func
+ .comments()
+ .iter()
+ .map(|c| bn_comment_to_comment(func, c))
+ .collect();
Function {
- guid: cached_function_guid(func, llil),
+ guid: cached_function_guid(func, lifted_il),
symbol: from_bn_symbol(&func.symbol()),
- ty: from_bn_type(&func.view(), &bn_fn_ty, MAX_CONFIDENCE),
- constraints: FunctionConstraints {
- // NOTE: Adding adjacent only works if analysis is complete.
- // NOTE: We do not filter out adjacent functions here.
- adjacent: cached_adjacency_constraints(func, |_| true),
- call_sites: cached_call_site_constraints(func),
- // TODO: Add caller sites (when adjacent and call sites are minimal)
- // NOTE: Adding caller sites only works if analysis is complete.
- caller_sites: Default::default(),
+ // Currently we only store the type if its a user type.
+ // TODO: In the future we might want to make this configurable.
+ ty: match func.has_user_type() || func.has_explicitly_defined_type() {
+ true => Some(from_bn_type(
+ &func.view(),
+ &func.function_type(),
+ MAX_CONFIDENCE,
+ )),
+ false => None,
},
+ // NOTE: Adding adjacent only works if analysis is complete.
+ // NOTE: We do not filter out adjacent functions here.
+ constraints: cached_constraints(func, |_| true),
+ comments,
+ // TODO: Gather relevant variables (only user?).
+ // TODO: Will need MLIL SSA for this to locate def sites.
+ // TODO: Add this info in a second pass?
+ variables: vec![],
}
}
@@ -71,20 +111,21 @@ pub fn sorted_basic_blocks(func: &BNFunction) -> Vec<BNRef<BNBasicBlock<NativeBl
.iter()
.map(|bb| bb.clone())
.collect::<Vec<_>>();
+ // NOTE: start_index is actually the address with [`NativeBlock`].
basic_blocks.sort_by_key(|f| f.start_index());
basic_blocks
}
pub fn function_guid<M: FunctionMutability>(
func: &BNFunction,
- llil: &LowLevelILFunction<M, NonSSA>,
+ lifted_il: &LowLevelILFunction<M, NonSSA>,
) -> FunctionGUID {
// TODO: We might want to make this configurable, or otherwise _not_ retrieve from the view here.
let relocatable_regions = relocatable_regions(&func.view());
let basic_blocks = sorted_basic_blocks(func);
let basic_block_guids = basic_blocks
.iter()
- .map(|bb| basic_block_guid(&relocatable_regions, bb, llil))
+ .map(|bb| basic_block_guid(&relocatable_regions, bb, lifted_il))
.collect::<Vec<_>>();
FunctionGUID::from_basic_blocks(&basic_block_guids)
}
@@ -92,30 +133,60 @@ pub fn function_guid<M: FunctionMutability>(
pub fn basic_block_guid<M: FunctionMutability>(
relocatable_regions: &[Range<u64>],
basic_block: &BNBasicBlock<NativeBlock>,
- llil: &LowLevelILFunction<M, NonSSA>,
+ lifted_il: &LowLevelILFunction<M, NonSSA>,
) -> BasicBlockGUID {
let func = basic_block.function();
+ // TODO: We really should never consult another IL, no guarantee that it exists.
+ let low_level_il = func.low_level_il();
let view = func.view();
let arch = func.arch();
let max_instr_len = arch.max_instr_len();
+ // NOTE: Whenever you make a change here, prefer being "additive", that is, make a smaller change that
+ // only increases the masked contents, instead of making a larger change that could *remove* masked
+ // contents. The reason is that we assume any change that is purely additive to increase the ability
+ // to match previously "unmatchable" functions, whereas the latter would take away. This is not always
+ // the case, but it is generally a good rule to follow.
let basic_block_range = basic_block.start_index()..basic_block.end_index();
let mut basic_block_bytes = Vec::with_capacity(basic_block_range.count());
for instr_addr in basic_block.into_iter() {
let mut instr_bytes = view.read_vec(instr_addr, max_instr_len);
if let Some(instr_info) = arch.instruction_info(&instr_bytes, instr_addr) {
instr_bytes.truncate(instr_info.length);
- if let Some(instr_llil) = llil.instruction_at(instr_addr) {
- // If instruction is blacklisted don't include the bytes.
- if !is_blacklisted_instruction(&instr_llil) {
- if is_variant_instruction(relocatable_regions, &instr_llil) {
- // Found a variant instruction, mask off entire instruction.
+
+ // Find variant and blacklisted instructions using lifted il.
+ if let Some(lifted_il_instr) = lifted_il.instruction_at(instr_addr) {
+ // If instruction is blacklisted, don't include the bytes.
+ if is_blacklisted_instruction(&lifted_il_instr) {
+ continue;
+ }
+
+ if is_variant_instruction(relocatable_regions, &lifted_il_instr) {
+ // Found a variant instruction, mask off the entire instruction.
+ instr_bytes.fill(0);
+ }
+ }
+
+ // TODO: We cannot access the values of expression in lifted IL, we have to go and consult low level IL.
+ // TODO: But because of some extremely annoying simplifications that are happening at LLIL, namely
+ // TODO: Folding of expressions into other instructions, we cannot use only LLIL. Therefor
+ // TODO: We only put the checks that require the expr value here.
+ // TODO: This still has the issue of, some (if (rax + 44) => 28) expression being masked,
+ // TODO: But the only way to remove that is to not consult LLIL at all and have the values
+ // TODO: Available at lifted IL, I have not found a good way to do this without making
+ // TODO: A "mapped llil" or having some simple data flow, the simple data flow is the most attractive
+ // TODO: "solution", but it would require
+ if let Ok(llil) = &low_level_il {
+ if let Some(low_level_instr) = llil.instruction_at(instr_addr) {
+ if is_computed_variant_instruction(relocatable_regions, &low_level_instr) {
+ // Found a computed variant instruction, mask off the entire instruction.
instr_bytes.fill(0);
}
- // Add the instructions bytes to the basic blocks bytes
- basic_block_bytes.extend(instr_bytes);
}
}
+
+ // Add the instruction bytes to the basic blocks bytes
+ basic_block_bytes.extend(instr_bytes);
}
}
@@ -127,8 +198,8 @@ pub fn basic_block_guid<M: FunctionMutability>(
/// Blacklisted instructions will make an otherwise identical function GUID fail to match.
///
/// Example: NOPs and useless moves are blacklisted to allow for hot-patchable functions.
-pub fn is_blacklisted_instruction<A: Architecture, M: FunctionMutability>(
- instr: &LowLevelILInstruction<A, M, NonSSA<RegularNonSSA>>,
+pub fn is_blacklisted_instruction<M: FunctionMutability>(
+ instr: &LowLevelILInstruction<M, NonSSA>,
) -> bool {
match instr.kind() {
LowLevelILInstructionKind::Nop(_) => true,
@@ -138,13 +209,13 @@ pub fn is_blacklisted_instruction<A: Architecture, M: FunctionMutability>(
if op.dest_reg() == source_op.source_reg() =>
{
match op.dest_reg() {
- LowLevelILRegister::ArchReg(r) => {
- // If this register has no implicit extend then we can safely assume it's a NOP.
+ LowLevelILRegisterKind::Arch(r) => {
+ // If this register has no implicit extend, we can safely assume it's a NOP.
// Ex. on x86_64 we don't want to remove `mov edi, edi` as it will zero the upper 32 bits.
// Ex. on x86 we do want to remove `mov edi, edi` as it will not have a side effect like above.
matches!(r.info().implicit_extend(), ImplicitRegisterExtend::NoExtend)
}
- LowLevelILRegister::Temp(_) => false,
+ LowLevelILRegisterKind::Temp(_) => false,
}
}
_ => false,
@@ -154,24 +225,22 @@ pub fn is_blacklisted_instruction<A: Architecture, M: FunctionMutability>(
}
}
-pub fn is_variant_instruction<A: Architecture, M: FunctionMutability>(
+pub fn is_variant_instruction<M: FunctionMutability>(
relocatable_regions: &[Range<u64>],
- instr: &LowLevelILInstruction<A, M, NonSSA<RegularNonSSA>>,
+ instr: &LowLevelILInstruction<M, NonSSA>,
) -> bool {
- let is_variant_expr = |expr: &LowLevelILExpressionKind<A, M, NonSSA<RegularNonSSA>>| {
- match expr {
+ let is_variant_expr = |expr: &LowLevelILExpression<M, NonSSA, ValueExpr>| {
+ match expr.kind() {
LowLevelILExpressionKind::ConstPtr(op)
if is_address_relocatable(relocatable_regions, op.value()) =>
{
// Constant Pointer must be in a section for it to be relocatable.
- // NOTE: We cannot utilize segments here as there will be a zero based segment.
true
}
LowLevelILExpressionKind::Const(op)
if is_address_relocatable(relocatable_regions, op.value()) =>
{
// Constant value must be in a section for it to be relocatable.
- // NOTE: We cannot utilize segments here as there will be a zero based segment.
true
}
LowLevelILExpressionKind::ExternPtr(_) => true,
@@ -181,7 +250,7 @@ pub fn is_variant_instruction<A: Architecture, M: FunctionMutability>(
// Visit instruction expressions looking for variant expression, [VisitorAction::Halt] means variant.
instr.visit_tree(&mut |expr| {
- if is_variant_expr(&expr.kind()) {
+ if is_variant_expr(expr) {
// Found a variant expression.
VisitorAction::Halt
} else {
@@ -191,18 +260,87 @@ pub fn is_variant_instruction<A: Architecture, M: FunctionMutability>(
}) == VisitorAction::Halt
}
-/// If the address is inside any of the given ranges we will assume the address to be relocatable.
+/// NOTE: This will only work at LLIL, **NOT** lifted IL. You must do this in a second pass.
+///
+/// This was previously done inside `is_variant_instruction` but had to be moved to access expr value.
+pub fn is_computed_variant_instruction<M: FunctionMutability>(
+ relocatable_regions: &[Range<u64>],
+ instr: &LowLevelILInstruction<M, NonSSA>,
+) -> bool {
+ let is_expr_constant = |expr: &LowLevelILExpression<M, NonSSA, ValueExpr>| match expr.kind() {
+ LowLevelILExpressionKind::Const(_) => true,
+ _ => false,
+ };
+
+ let is_variant_observed_expr = |expr: &LowLevelILExpression<M, NonSSA, ValueExpr>| {
+ match expr.kind() {
+ // TODO: Skip problematic expressions like IF?
+ LowLevelILExpressionKind::Add(op) | LowLevelILExpressionKind::Sub(op) => {
+ // For now, we limit to only expressions that contain some constant; this keeps add expressions
+ // with two registers with known values from being marked variant.
+ let constant_expressed =
+ is_expr_constant(&op.left()) || is_expr_constant(&op.right());
+ // NOTE: Lifted IL does not have the value ever, we must consult Low Level IL.
+ // If the expression value is known, we check to see if it's a relocatable address.
+ let expr_value = expr.value();
+ match expr_value.state {
+ RegisterValueType::EntryValue
+ | RegisterValueType::ConstantValue
+ | RegisterValueType::ConstantPointerValue
+ | RegisterValueType::ExternalPointerValue
+ | RegisterValueType::StackFrameOffset
+ | RegisterValueType::ReturnAddressValue
+ | RegisterValueType::ImportedAddressValue
+ if constant_expressed
+ && is_address_relocatable(
+ relocatable_regions,
+ expr_value.value as u64,
+ ) =>
+ {
+ // Concrete arithmetic operation with a relocatable result.
+ true
+ }
+ _ => false,
+ }
+ }
+ _ => false,
+ }
+ };
+
+ // Visit instruction expressions looking for an observed variant expression, [VisitorAction::Halt] means variant.
+ instr.visit_tree(&mut |expr| {
+ if is_variant_observed_expr(expr) {
+ // Found a variant expression.
+ VisitorAction::Halt
+ } else {
+ // Keep looking for an observed variant expression.
+ VisitorAction::Descend
+ }
+ }) == VisitorAction::Halt
+}
+
+/// If the address is inside any of the given ranges, we will assume the address to be relocatable.
pub fn is_address_relocatable(relocatable_regions: &[Range<u64>], address: u64) -> bool {
relocatable_regions
.iter()
- .any(|range| range.contains(&address))
+ .any(|range| {
+ // Check if the address is within the range itself
+ (range.contains(&address))
+ // Check if the address is within the threshold **AFTER** the range
+ // NOTE: The address must at least be larger than the threshold itself, for lower image-based binaries.
+ || (address > range.end && address > ADDRESS_RELOCATION_THRESHOLD && address <= range.end + ADDRESS_RELOCATION_THRESHOLD)
+ // Check if the address is within the threshold **BEFORE** the range
+ // NOTE: The address must at least be larger than the threshold itself, for lower image-based binaries.
+ || (address < range.start && address > ADDRESS_RELOCATION_THRESHOLD && address >= range.start.saturating_sub(ADDRESS_RELOCATION_THRESHOLD))
+ })
}
// TODO: This might need to be configurable, in that case we better remove this function.
/// Get the relocatable regions of the view.
///
-/// Currently, this is all the sections, however this might be refined later.
+/// Currently, this is all the sections, however, this might be refined later.
pub fn relocatable_regions(view: &BinaryView) -> Vec<Range<u64>> {
+ // NOTE: We cannot use segments here as there will be a zero-based segment.
view.sections()
.iter()
.map(|s| Range {
@@ -211,41 +349,3 @@ pub fn relocatable_regions(view: &BinaryView) -> Vec<Range<u64>> {
})
.collect()
}
-
-#[cfg(test)]
-mod tests {
- use crate::cache::cached_function_guid;
- use binaryninja::binary_view::BinaryViewExt;
- use binaryninja::headless::Session;
- use std::path::PathBuf;
- use std::sync::OnceLock;
-
- static INIT: OnceLock<Session> = OnceLock::new();
-
- fn get_session<'a>() -> &'a Session {
- // TODO: This is not shared between other test modules, should still be fine (mutex in core now).
- INIT.get_or_init(|| Session::new().expect("Failed to initialize session"))
- }
-
- #[test]
- fn insta_signatures() {
- let session = get_session();
- let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap();
- for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") {
- let entry = entry.expect("Failed to read directory entry");
- let path = entry.path();
- if path.is_file() {
- let view = session.load(&path).expect("Failed to load view");
- let mut functions = view
- .functions()
- .iter()
- .map(|f| cached_function_guid(&f, &f.low_level_il().unwrap()))
- .collect::<Vec<_>>();
- functions.sort_by_key(|guid| guid.guid);
- let snapshot_name =
- format!("snapshot_{}", path.file_stem().unwrap().to_string_lossy());
- insta::assert_debug_snapshot!(snapshot_name, functions);
- }
- }
- }
-}
diff --git a/plugins/warp/src/matcher.rs b/plugins/warp/src/matcher.rs
index 0ce258ad..c6ab7f9f 100644
--- a/plugins/warp/src/matcher.rs
+++ b/plugins/warp/src/matcher.rs
@@ -1,363 +1,282 @@
+use crate::cache::cached_constraints;
+use crate::container::{Container, SourceId};
+use crate::convert::to_bn_type;
use binaryninja::architecture::Architecture as BNArchitecture;
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
use binaryninja::function::Function as BNFunction;
-use binaryninja::platform::Platform;
-use binaryninja::rc::Guard;
-use binaryninja::rc::Ref as BNRef;
-use dashmap::DashMap;
+use binaryninja::settings::{QueryOptions, Settings as BNSettings};
use serde_json::json;
use std::cmp::Ordering;
-use std::collections::{HashMap, HashSet};
-use std::hash::{DefaultHasher, Hash, Hasher};
-use std::path::PathBuf;
-use std::sync::OnceLock;
-use walkdir::{DirEntry, WalkDir};
+use std::collections::HashSet;
+use std::hash::Hash;
use warp::r#type::class::TypeClass;
-use warp::r#type::guid::TypeGUID;
use warp::r#type::Type;
-use warp::signature::function::{Function, FunctionGUID};
-use warp::signature::Data;
-
-use crate::cache::{
- cached_adjacency_constraints, cached_call_site_constraints, cached_function_match,
- try_cached_function_guid,
-};
-use crate::convert::to_bn_type;
-use crate::plugin::on_matched_function;
-use crate::{core_signature_dir, user_signature_dir};
-
-pub static PLAT_MATCHER_CACHE: OnceLock<DashMap<PlatformID, Matcher>> = OnceLock::new();
-
-pub fn cached_function_matcher(function: &BNFunction) {
- let platform = function.platform();
- let platform_id = PlatformID::from(platform.as_ref());
- let matcher_cache = PLAT_MATCHER_CACHE.get_or_init(Default::default);
- match matcher_cache.get(&platform_id) {
- Some(matcher) => matcher.match_function(function),
- None => {
- let matcher = Matcher::from_platform(platform);
- matcher.match_function(function);
- matcher_cache.insert(platform_id, matcher);
- }
- }
-}
-
-// TODO: Maybe just clear individual platforms? This works well enough either way.
-pub fn invalidate_function_matcher_cache() {
- let matcher_cache = PLAT_MATCHER_CACHE.get_or_init(Default::default);
- matcher_cache.clear();
-}
+use warp::signature::function::Function;
-#[derive(Debug, Default, Clone)]
+/// A matcher represents a specific configuration for identify functions using WARP. A matcher
+/// does not store/own any WARP information directly, instead the matcher is given a [`Container`]
+/// that holds all of that information.
+///
+/// The separation of the WARP information from the [`Matcher`] allows a greater degree of control and
+/// provides a clean interface for further logic to be built on top of. A matcher instance, unlike
+/// a typical [`Container`] implementation, is cheap to create.
+#[derive(Debug, Clone, Copy)]
pub struct Matcher {
- // TODO: Storing the settings here means that they are effectively global.
- // TODO: If we want scoped or view settings they must be moved out.
pub settings: MatcherSettings,
- pub functions: DashMap<FunctionGUID, Vec<Function>>,
- pub types: DashMap<TypeGUID, Type>,
- pub named_types: DashMap<String, Type>,
}
impl Matcher {
- /// Create a matcher from the platforms signature subdirectory.
- pub fn from_platform(platform: BNRef<Platform>) -> Self {
- let platform_name = platform.name().to_string();
+ pub fn new(settings: MatcherSettings) -> Self {
+ Matcher { settings }
+ }
- // Get core and user signatures.
- // TODO: Separate each file into own bucket for filtering?
- let plat_core_sig_dir = core_signature_dir().join(&platform_name);
- let mut data = get_data_from_dir(&plat_core_sig_dir);
- let plat_user_sig_dir = user_signature_dir().join(&platform_name);
- let user_data = get_data_from_dir(&plat_user_sig_dir);
+ pub fn match_function_from_constraints<'a>(
+ &self,
+ function: &BNFunction,
+ matched_functions: &'a [Function],
+ ) -> Option<&'a Function> {
+ let function_len = function.highest_address() - function.lowest_address();
+ let is_function_trivial = { function_len < self.settings.trivial_function_len };
+ let is_function_allowed = {
+ function_len >= self.settings.minimum_function_len
+ && function_len < self.settings.maximum_function_len.unwrap_or(u64::MAX)
+ };
- data.extend(user_data);
- let merged_data = Data::merge(data.values().cloned().collect::<Vec<_>>());
- log::debug!("Loaded signatures: {:?}", data.keys());
- Matcher::from_data(merged_data)
- }
+ // Function isn't allowed, or no matches so stop early.
+ if !is_function_allowed || matched_functions.is_empty() {
+ return None;
+ }
- pub fn from_data(data: Data) -> Self {
- let functions = data.functions.into_iter().fold(
- DashMap::new(),
- |map: DashMap<FunctionGUID, Vec<_>>, func| {
- map.entry(func.guid).or_default().push(func);
- map
- },
- );
- let types = data
- .types
- .iter()
- .map(|ty| (ty.guid, ty.ty.clone()))
- .collect();
- let named_types = data
- .types
- .into_iter()
- .filter_map(|ty| ty.ty.name.to_owned().map(|name| (name, ty.ty)))
- .collect();
+ // If we have a single possible match than that must be our function.
+ // We must also not be a trivial function, as those will likely be artifacts of an incomplete dataset
+ if matched_functions.len() == 1 && !is_function_trivial {
+ return matched_functions.first();
+ }
+ // Filter out adjacent functions which are trivial, this helps avoid false positives.
+ // NOTE: If the user sets `trivial_function_adjacent_allowed` to true we will always match.
+ // TODO: Expand on this more later. We might want to match on adjacent functions smaller than this.
+ let adjacent_function_filter = |adj_func: &BNFunction| {
+ let adj_func_len = adj_func.highest_address() - adj_func.lowest_address();
+ adj_func_len >= self.settings.trivial_function_len
+ || self.settings.trivial_function_adjacent_allowed
+ };
- Self {
- // NOTE: Settings will be retrieved from global state every time this is called.
- settings: MatcherSettings::global(),
- functions,
- types,
- named_types,
+ // TODO: When the highest count has two matches we return None. Need to alert the user.
+ // "common" being the intersection between the observed and matched.
+ let constraints = cached_constraints(function, adjacent_function_filter);
+ let mut highest_count = 0;
+ let mut matched_func = None;
+ for matched in matched_functions {
+ let common_count = constraints.intersection(&matched.constraints).count();
+ match common_count.cmp(&highest_count) {
+ Ordering::Equal => matched_func = None,
+ Ordering::Greater => {
+ highest_count = common_count;
+ matched_func = Some(matched);
+ }
+ Ordering::Less => {}
+ }
}
- }
- pub fn extend_with_matcher(&mut self, matcher: Matcher) {
- self.functions.extend(matcher.functions);
- self.types.extend(matcher.types);
- self.named_types.extend(matcher.named_types);
+ // If we have a match below the minimum threshold, ignore.
+ match highest_count.cmp(&self.settings.minimum_matched_constraints) {
+ Ordering::Equal => matched_func,
+ Ordering::Greater => matched_func,
+ Ordering::Less => None,
+ }
}
- pub fn add_type_to_view<A: BNArchitecture>(&self, view: &BinaryView, arch: &A, ty: &Type) {
+ // TODO: I would really like for WARP types to be added in a seperate type container, so that we don't
+ // TODO: just add them as system or user types.
+ pub fn add_type_to_view<A: BNArchitecture>(
+ &self,
+ container: &dyn Container,
+ source: &SourceId,
+ view: &BinaryView,
+ arch: &A,
+ ty: &Type,
+ ) where
+ Self: Sized,
+ {
fn inner_add_type_to_view<A: BNArchitecture>(
- matcher: &Matcher,
+ container: &dyn Container,
+ source: &SourceId,
view: &BinaryView,
arch: &A,
visited_refs: &mut HashSet<String>,
ty: &Type,
) {
- let ty_id_str = TypeGUID::from(ty).to_string();
- if view.type_by_id(&ty_id_str).is_some() {
- // Type already added.
- return;
- }
// Type not already added to the view.
// Verify all nested types are added before adding type.
- match ty.class.as_ref() {
- TypeClass::Pointer(c) => {
- inner_add_type_to_view(matcher, view, arch, visited_refs, &c.child_type)
- }
- TypeClass::Array(c) => {
- inner_add_type_to_view(matcher, view, arch, visited_refs, &c.member_type)
- }
+ match &ty.class {
+ TypeClass::Pointer(c) => inner_add_type_to_view(
+ container,
+ source,
+ view,
+ arch,
+ visited_refs,
+ &c.child_type,
+ ),
+ TypeClass::Array(c) => inner_add_type_to_view(
+ container,
+ source,
+ view,
+ arch,
+ visited_refs,
+ &c.member_type,
+ ),
TypeClass::Structure(c) => {
for member in &c.members {
- inner_add_type_to_view(matcher, view, arch, visited_refs, &member.ty)
+ inner_add_type_to_view(
+ container,
+ source,
+ view,
+ arch,
+ visited_refs,
+ &member.ty,
+ )
}
}
- TypeClass::Enumeration(c) => {
- inner_add_type_to_view(matcher, view, arch, visited_refs, &c.member_type)
- }
+ TypeClass::Enumeration(c) => inner_add_type_to_view(
+ container,
+ source,
+ view,
+ arch,
+ visited_refs,
+ &c.member_type,
+ ),
TypeClass::Union(c) => {
for member in &c.members {
- inner_add_type_to_view(matcher, view, arch, visited_refs, &member.ty)
+ inner_add_type_to_view(
+ container,
+ source,
+ view,
+ arch,
+ visited_refs,
+ &member.ty,
+ )
}
}
TypeClass::Function(c) => {
for out_member in &c.out_members {
- inner_add_type_to_view(matcher, view, arch, visited_refs, &out_member.ty)
+ inner_add_type_to_view(
+ container,
+ source,
+ view,
+ arch,
+ visited_refs,
+ &out_member.ty,
+ )
}
for in_member in &c.in_members {
- inner_add_type_to_view(matcher, view, arch, visited_refs, &in_member.ty)
+ inner_add_type_to_view(
+ container,
+ source,
+ view,
+ arch,
+ visited_refs,
+ &in_member.ty,
+ )
}
}
TypeClass::Referrer(c) => {
// Check to see if the referrer has been added to the view.
- let mut resolved = false;
+ let mut resolved_ty = None;
if let Some(ref_guid) = c.guid {
// NOTE: We do not need to check for cyclic reference here because
// NOTE: GUID references are unable to be referenced by themselves.
if view.type_by_id(&ref_guid.to_string()).is_none() {
// Add the referrer to the view if it is in the Matcher types
- if let Some(ref_ty) = matcher.types.get(&ref_guid) {
- inner_add_type_to_view(matcher, view, arch, visited_refs, &ref_ty);
- resolved = true;
+ if let Ok(Some(ref_ty)) = container.type_with_guid(source, &ref_guid) {
+ inner_add_type_to_view(
+ container,
+ source,
+ view,
+ arch,
+ visited_refs,
+ &ref_ty,
+ );
+ resolved_ty = Some(ref_ty);
}
}
}
if let Some(ref_name) = &c.name {
// Only try and resolve by name if not already visiting.
- if !resolved
+ if resolved_ty.is_none()
&& visited_refs.insert(ref_name.to_string())
&& view.type_by_name(ref_name).is_none()
{
// Add the ref to the view if it is in the Matcher types
- if let Some(ref_ty) = matcher.named_types.get(ref_name) {
- inner_add_type_to_view(matcher, view, arch, visited_refs, &ref_ty);
+ let type_guids = container
+ .type_guids_with_name(source, ref_name)
+ .unwrap_or_default();
+ // TODO: What happens if we have more than one?
+ if type_guids.len() == 1 {
+ // TODO: What happens if we cant get the guid?
+ if let Ok(Some(ref_ty)) =
+ container.type_with_guid(source, &type_guids[0])
+ {
+ inner_add_type_to_view(
+ container,
+ source,
+ view,
+ arch,
+ visited_refs,
+ &ref_ty,
+ );
+ resolved_ty = Some(ref_ty);
+ }
}
// No longer visiting type.
visited_refs.remove(ref_name);
}
}
- // All nested types _should_ be added now, we can add this type.
- // TODO: Do we want to make unnamed types visible? I think we should, but some people might be opposed.
- let ty_name = ty.name.to_owned().unwrap_or_else(|| ty_id_str.clone());
- view.define_auto_type_with_id(ty_name, &ty_id_str, &to_bn_type(arch, ty));
- }
- _ => {}
- }
- }
- inner_add_type_to_view(self, view, arch, &mut HashSet::new(), ty)
- }
-
- pub fn match_function(&self, function: &BNFunction) {
- // Call this the first time you matched on the function.
- let resolve_new_types = |matched: &Function| {
- // We also want to resolve the types here.
- if let TypeClass::Function(c) = matched.ty.class.as_ref() {
- // Recursively go through the function type and resolve referrers
- let view = function.view();
- let arch = function.arch();
- for out_member in &c.out_members {
- self.add_type_to_view(&view, &arch, &out_member.ty);
- }
- for in_member in &c.in_members {
- self.add_type_to_view(&view, &arch, &in_member.ty);
- }
- }
- };
-
- if let Some(matched_function) = cached_function_match(function, || {
- // We have yet to match on this function.
- let function_len = function.highest_address() - function.lowest_address();
- let is_function_trivial = { function_len < self.settings.trivial_function_len };
- let is_function_allowed = {
- function_len > self.settings.minimum_function_len
- && function_len < self.settings.maximum_function_len.unwrap_or(u64::MAX)
- };
- let warp_func_guid = try_cached_function_guid(function)?;
- match self.functions.get(&warp_func_guid) {
- _ if !is_function_allowed => None,
- Some(matched) if matched.len() == 1 && !is_function_trivial => {
- resolve_new_types(&matched[0]);
- Some(matched[0].to_owned())
- }
- Some(matched) => {
- let matched_on = self.match_function_from_constraints(function, &matched)?;
- resolve_new_types(matched_on);
- Some(matched_on.to_owned())
- }
- None => None,
- }
- }) {
- on_matched_function(function, &matched_function);
- }
- }
-
- pub fn match_function_from_constraints<'a>(
- &self,
- function: &BNFunction,
- matched_functions: &'a [Function],
- ) -> Option<&'a Function> {
- // Filter out adjacent functions which are trivial, this helps avoid false positives.
- // NOTE: If the user sets `trivial_function_adjacent_allowed` to true we will always match.
- // TODO: Expand on this more later. We might want to match on adjacent functions smaller than this.
- let adjacent_function_filter = |adj_func: &BNFunction| {
- let adj_func_len = adj_func.highest_address() - adj_func.lowest_address();
- adj_func_len > self.settings.trivial_function_len
- || self.settings.trivial_function_adjacent_allowed
- };
-
- let call_sites = cached_call_site_constraints(function);
- let adjacent = cached_adjacency_constraints(function, adjacent_function_filter);
-
- // "common" being the intersection between the observed and matched.
- fn find_highest_common_count<'a, F, T>(
- observed_items: &HashSet<T>,
- matched_functions: &'a [Function],
- extract_items: F,
- ) -> (usize, Option<&'a Function>)
- where
- F: Fn(&Function) -> HashSet<T>,
- T: Hash + Eq,
- {
- let mut highest_count = 0;
- let mut matched_func = None;
- for matched in matched_functions {
- let matched_items = extract_items(matched);
- let common_count = observed_items.intersection(&matched_items).count();
- match common_count.cmp(&highest_count) {
- Ordering::Equal => matched_func = None,
- Ordering::Greater => {
- highest_count = common_count;
- matched_func = Some(matched);
+ // Adds the ref'd type to the view.
+ match (c.guid, &c.name, resolved_ty) {
+ (Some(guid), Some(name), Some(ref_ty)) => {
+ view.define_auto_type_with_id(
+ name,
+ &guid.to_string(),
+ &to_bn_type(arch, &ref_ty),
+ );
+ }
+ (Some(_guid), Some(_name), None) => {
+ // TODO: Got name and guid but no type? Do we add a bare NTR?
+ }
+ (Some(_guid), None, _) => {
+ // TODO: How would we reference this type without a name???
+ }
+ (None, Some(_name), _) => {
+ // TODO: Cyclic type reference if no guid, so... dont define?
+ }
+ (None, None, _) => {
+ // TODO: What?!?!?
+ }
}
- Ordering::Less => {}
}
+ TypeClass::Void
+ | TypeClass::Boolean(_)
+ | TypeClass::Integer(_)
+ | TypeClass::Character(_)
+ | TypeClass::Float(_) => {}
}
- (highest_count, matched_func)
- }
-
- let call_site_guids: HashSet<_> = call_sites.iter().filter_map(|c| c.guid).collect();
- let call_site_symbol_names: HashSet<_> = call_sites
- .into_iter()
- .filter_map(|c| c.symbol.map(|s| s.name))
- .collect();
- let adjacent_guids: HashSet<_> = adjacent.iter().filter_map(|c| c.guid).collect();
- let adjacent_symbol_names: HashSet<_> = adjacent
- .into_iter()
- .filter_map(|c| c.symbol.map(|s| s.name))
- .collect();
-
- // Ordered from the lowest confidence to the highest confidence constraint.
- let checked_constraints = [
- find_highest_common_count(&adjacent_symbol_names, matched_functions, |matched| {
- matched
- .constraints
- .adjacent
- .iter()
- .filter_map(|c| c.symbol.to_owned().map(|s| s.name))
- .collect()
- }),
- find_highest_common_count(&adjacent_guids, matched_functions, |matched| {
- matched
- .constraints
- .adjacent
- .iter()
- .filter_map(|c| c.guid)
- .collect()
- }),
- find_highest_common_count(&call_site_symbol_names, matched_functions, |matched| {
- matched
- .constraints
- .call_sites
- .iter()
- .filter_map(|c| c.symbol.to_owned().map(|s| s.name))
- .collect()
- }),
- find_highest_common_count(&call_site_guids, matched_functions, |matched| {
- matched
- .constraints
- .call_sites
- .iter()
- .filter_map(|c| c.guid)
- .collect()
- }),
- ];
- // If there is a tie, the last one wins, which should be call_site guid.
- checked_constraints
- .into_iter()
- .max_by_key(|&(count, _)| count)
- .filter(|&(count, _)| count >= self.settings.minimum_matched_constraints)
- .and_then(|(_, func)| func)
+ // TODO: Some refs likely need to ommitted because they are just that, refs to another type.
+ // let guid = TypeGUID::from(ty);
+ // let name = ty.name.clone().unwrap_or(guid.to_string());
+ // view.define_auto_type_with_id(name, &guid.to_string(), &to_bn_type(arch, ty));
+ }
+ inner_add_type_to_view(container, source, view, arch, &mut HashSet::new(), ty)
}
}
-fn get_data_from_dir(dir: &PathBuf) -> HashMap<PathBuf, Data> {
- let data_from_entry = |entry: DirEntry| {
- let path = entry.path();
- let contents = std::fs::read(path).ok()?;
- Data::from_bytes(&contents)
- };
-
- WalkDir::new(dir)
- .into_iter()
- .filter_map(|e| e.ok())
- .filter(|e| e.file_type().is_file())
- .filter_map(|e| Some((e.clone().into_path(), data_from_entry(e)?)))
- .collect()
-}
-
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MatcherSettings {
/// Any function under this length will be required to constrain.
///
- /// This is set to [MatcherSettings::DEFAULT_TRIVIAL_FUNCTION_LEN] by default.
+ /// This is set to [MatcherSettings::TRIVIAL_FUNCTION_LEN_DEFAULT] by default.
pub trivial_function_len: u64,
/// Any function under this length will not match.
///
@@ -367,13 +286,15 @@ pub struct MatcherSettings {
///
/// This is set to [MatcherSettings::MAXIMUM_FUNCTION_LEN_DEFAULT] by default.
pub maximum_function_len: Option<u64>,
- /// For a successful constrained function match the number of matches must be above this.
+ /// For a successful constrained function match, the number of matches must be above this.
///
- /// This is set to [MatcherSettings::DEFAULT_TRIVIAL_FUNCTION_LEN] by default.
+ /// This is set to [MatcherSettings::MINIMUM_MATCHED_CONSTRAINTS_DEFAULT] by default.
pub minimum_matched_constraints: usize,
- /// For a successful constrained function match the number of matches must be above this.
+ /// When function constraints are checked, if this is enabled, functions can match based off trivial adjacent functions.
+ ///
+ /// Any function under `trivial_function_len` will be considered trivial.
///
- /// This is set to [MatcherSettings::DEFAULT_TRIVIAL_FUNCTION_LEN] by default.
+ /// This is set to [MatcherSettings::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_DEFAULT] by default.
pub trivial_function_adjacent_allowed: bool,
}
@@ -395,17 +316,15 @@ impl MatcherSettings {
///
/// Call this once when you initialize so that the settings exist.
///
- /// NOTE: If you are using this as a library then just modify the MatcherSettings directly
+ /// NOTE: If you are using this as a library, then modify the [`MatcherSettings`] directly
/// in the matcher instance, that way you don't need to round-trip through Binary Ninja.
- pub fn register() {
- let bn_settings = binaryninja::settings::Settings::new();
-
+ pub fn register(bn_settings: &mut BNSettings) {
let trivial_function_len_props = json!({
"title" : "Trivial Function Length",
"type" : "number",
"default" : Self::TRIVIAL_FUNCTION_LEN_DEFAULT,
"description" : "Functions below this length in bytes will be required to match on constraints.",
- "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ "ignore" : []
});
bn_settings.register_setting_json(
Self::TRIVIAL_FUNCTION_LEN_SETTING,
@@ -417,7 +336,7 @@ impl MatcherSettings {
"type" : "number",
"default" : Self::MINIMUM_FUNCTION_LEN_DEFAULT,
"description" : "Functions below this length will not be matched.",
- "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ "ignore" : []
});
bn_settings.register_setting_json(
Self::MINIMUM_FUNCTION_LEN_SETTING,
@@ -429,7 +348,7 @@ impl MatcherSettings {
"type" : "number",
"default" : Self::MAXIMUM_FUNCTION_LEN_DEFAULT,
"description" : "Functions above this length will not be matched. A value of 0 will disable this check.",
- "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ "ignore" : []
});
bn_settings.register_setting_json(
Self::MAXIMUM_FUNCTION_LEN_SETTING,
@@ -441,7 +360,7 @@ impl MatcherSettings {
"type" : "number",
"default" : Self::MINIMUM_MATCHED_CONSTRAINTS_DEFAULT,
"description" : "When function constraints are checked the amount of constraints matched must be at-least this.",
- "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ "ignore" : []
});
bn_settings.register_setting_json(
Self::MINIMUM_MATCHED_CONSTRAINTS_SETTING,
@@ -453,7 +372,7 @@ impl MatcherSettings {
"type" : "boolean",
"default" : Self::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_DEFAULT,
"description" : "When function constraints are checked if this is enabled functions can match based off trivial adjacent functions.",
- "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ "ignore" : []
});
bn_settings.register_setting_json(
Self::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_SETTING,
@@ -461,26 +380,32 @@ impl MatcherSettings {
);
}
- pub fn global() -> Self {
+ /// Retrieve matcher settings from [`BNSettings`].
+ pub fn from_settings(bn_settings: &BNSettings, query_opts: &mut QueryOptions) -> Self {
let mut settings = MatcherSettings::default();
- let bn_settings = binaryninja::settings::Settings::new();
if bn_settings.contains(Self::TRIVIAL_FUNCTION_LEN_SETTING) {
settings.trivial_function_len =
- bn_settings.get_integer(Self::TRIVIAL_FUNCTION_LEN_SETTING);
+ bn_settings.get_integer_with_opts(Self::TRIVIAL_FUNCTION_LEN_SETTING, query_opts);
}
if bn_settings.contains(Self::MINIMUM_FUNCTION_LEN_SETTING) {
settings.minimum_function_len =
- bn_settings.get_integer(Self::MINIMUM_FUNCTION_LEN_SETTING);
+ bn_settings.get_integer_with_opts(Self::MINIMUM_FUNCTION_LEN_SETTING, query_opts);
}
if bn_settings.contains(Self::MAXIMUM_FUNCTION_LEN_SETTING) {
- match bn_settings.get_integer(Self::MAXIMUM_FUNCTION_LEN_SETTING) {
+ match bn_settings.get_integer_with_opts(Self::MAXIMUM_FUNCTION_LEN_SETTING, query_opts)
+ {
0 => settings.maximum_function_len = None,
len => settings.maximum_function_len = Some(len),
}
}
if bn_settings.contains(Self::MINIMUM_MATCHED_CONSTRAINTS_SETTING) {
- settings.minimum_matched_constraints =
- bn_settings.get_integer(Self::MINIMUM_MATCHED_CONSTRAINTS_SETTING) as usize;
+ settings.minimum_matched_constraints = bn_settings
+ .get_integer_with_opts(Self::MINIMUM_MATCHED_CONSTRAINTS_SETTING, query_opts)
+ as usize;
+ }
+ if bn_settings.contains(Self::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_SETTING) {
+ settings.trivial_function_adjacent_allowed = bn_settings
+ .get_bool_with_opts(Self::TRIVIAL_FUNCTION_ADJACENT_ALLOWED_SETTING, query_opts);
}
settings
}
@@ -498,27 +423,3 @@ impl Default for MatcherSettings {
}
}
}
-
-/// A unique platform ID, used for caching.
-#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
-pub struct PlatformID(u64);
-
-impl From<&Platform> for PlatformID {
- fn from(value: &Platform) -> Self {
- let mut hasher = DefaultHasher::new();
- hasher.write(value.name().as_bytes());
- Self(hasher.finish())
- }
-}
-
-impl From<BNRef<Platform>> for PlatformID {
- fn from(value: BNRef<Platform>) -> Self {
- Self::from(value.as_ref())
- }
-}
-
-impl From<Guard<'_, Platform>> for PlatformID {
- fn from(value: Guard<'_, Platform>) -> Self {
- Self::from(value.as_ref())
- }
-}
diff --git a/plugins/warp/src/plugin.rs b/plugins/warp/src/plugin.rs
index 4adab293..f8c4c0c5 100644
--- a/plugins/warp/src/plugin.rs
+++ b/plugins/warp/src/plugin.rs
@@ -1,186 +1,40 @@
use crate::cache::register_cache_destructor;
+use std::time::Instant;
+use crate::cache::container::add_cached_container;
+use crate::container::disk::DiskContainer;
use crate::matcher::MatcherSettings;
use crate::plugin::render_layer::HighlightRenderLayer;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::{Command, FunctionCommand};
-use binaryninja::function::{Function, FunctionUpdateType};
+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,
+};
use binaryninja::logger::Logger;
-use binaryninja::rc::Ref;
-use binaryninja::tags::TagType;
-use binaryninja::ObjectDestructor;
+use binaryninja::settings::Settings;
use log::LevelFilter;
-use warp::signature::function::constraints::FunctionConstraint;
-use warp::signature::function::Function as WarpFunction;
-mod add;
-mod copy;
mod create;
-mod find;
+mod debug;
+mod ffi;
+mod file;
+mod function;
mod load;
+mod project;
mod render_layer;
-mod types;
+mod settings;
mod workflow;
-// TODO: This icon is a little much
-const TAG_ICON: &str = "🌏";
-const TAG_NAME: &str = "WARP";
-
-fn get_warp_tag_type(view: &BinaryView) -> Ref<TagType> {
- view.tag_type_by_name(TAG_NAME)
- .unwrap_or_else(|| view.create_tag_type(TAG_NAME, TAG_ICON))
-}
-
-// What happens to the function when it is matched.
-// TODO: add user: bool
-// TODO: Rename to markup_function or something.
-pub fn on_matched_function(function: &Function, matched: &WarpFunction) {
- let view = function.view();
- // TODO: Using user symbols here is problematic
- // TODO: For one they queue up a bunch of main thread actions
- // TODO: Secondly by queueing up those main thread actions if you attempt to save the file
- // TODO: Before the undo actions are done completing
- view.define_user_symbol(&to_bn_symbol_at_address(
- &view,
- &matched.symbol,
- function.symbol().address(),
- ));
- function.set_user_type(&to_bn_type(&function.arch(), &matched.ty));
- // TODO: Add metadata. (both binja metadata and warp metadata)
- function.add_tag(
- &get_warp_tag_type(&view),
- &matched.guid.to_string(),
- None,
- true,
- None,
- );
- // Seems to be the only way to get the analysis update to work correctly.
- function.mark_updates_required(FunctionUpdateType::FullAutoFunctionUpdate);
-}
-
-struct DebugFunction;
-
-impl FunctionCommand for DebugFunction {
- fn action(&self, _view: &BinaryView, func: &Function) {
- if let Ok(llil) = func.low_level_il() {
- log::info!("{:#?}", build_function(func, &llil));
- }
- }
-
- fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
- true
- }
-}
-
-struct DebugMatcher;
-
-impl FunctionCommand for DebugMatcher {
- fn action(&self, _view: &BinaryView, function: &Function) {
- let Ok(llil) = function.low_level_il() else {
- log::error!("No LLIL for function 0x{:x}", function.start());
- return;
- };
- let platform = function.platform();
- // Build the matcher every time this is called to make sure we aren't in a bad state.
- let matcher = Matcher::from_platform(platform);
- let func = build_function(function, &llil);
- // TODO: Clean this up.
- if let Some(possible_matches) = matcher.functions.get(&func.guid) {
- let print_constraint = |prefix: &str, constraint: &FunctionConstraint| {
- log::info!(
- " {} {} ({})",
- prefix,
- constraint
- .to_owned()
- .symbol
- .map(|s| s.name)
- .unwrap_or("*".to_string()),
- constraint
- .guid
- .map(|g| g.to_string())
- .unwrap_or("*".to_string())
- );
- };
- for possible_match in possible_matches.value() {
- log::info!("{} ({})", possible_match.symbol.name, possible_match.guid);
- for constraint in &possible_match.constraints.call_sites {
- print_constraint("CS", constraint);
- }
- for constraint in &possible_match.constraints.call_sites {
- print_constraint("ADJ", constraint);
- }
- }
- } else {
- log::error!(
- "No possible matches found for the function 0x{:x}",
- function.start()
- );
- };
- }
-
- fn valid(&self, _view: &BinaryView, _function: &Function) -> bool {
- true
- }
-}
-
-struct DebugCache;
-
-impl Command for DebugCache {
- fn action(&self, view: &BinaryView) {
- let view_id = ViewID::from(view);
- let function_cache = FUNCTION_CACHE.get_or_init(Default::default);
- if let Some(cache) = function_cache.get(&view_id) {
- log::info!("View functions: {}", cache.cache.len());
- }
-
- let matched_function_cache = MATCHED_FUNCTION_CACHE.get_or_init(Default::default);
- if let Some(cache) = matched_function_cache.get(&view_id) {
- log::info!("View matched functions: {}", cache.cache.len());
- }
-
- let function_guid_cache = GUID_CACHE.get_or_init(Default::default);
- if let Some(cache) = function_guid_cache.get(&view_id) {
- log::info!("View function guids: {}", cache.cache.len());
- }
-
- let plat_cache = PLAT_MATCHER_CACHE.get_or_init(Default::default);
- if let Some(plat) = view.default_platform() {
- let platform_id = PlatformID::from(plat);
- if let Some(cache) = plat_cache.get(&platform_id) {
- log::info!("Platform functions: {}", cache.functions.len());
- log::info!("Platform types: {}", cache.types.len());
- log::info!("Platform settings: {:?}", cache.settings);
- }
- }
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
-
-struct DebugInvalidateCache;
-
-impl Command for DebugInvalidateCache {
- fn action(&self, view: &BinaryView) {
- invalidate_function_matcher_cache();
- let destructor = cache::CacheDestructor {};
- destructor.destruct_view(view);
- log::info!("Invalidated all WARP caches...");
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
-
#[no_mangle]
#[allow(non_snake_case)]
pub extern "C" fn CorePluginInit() -> bool {
Logger::new("WARP").with_level(LevelFilter::Debug).init();
- // Register our matcher settings.
- MatcherSettings::register();
+ // Register our matcher and plugin settings globally.
+ let mut global_bn_settings = Settings::new();
+ MatcherSettings::register(&mut global_bn_settings);
+ PluginSettings::register(&mut global_bn_settings);
// Make sure caches are flushed when the views get destructed.
register_cache_destructor();
@@ -190,70 +44,93 @@ pub extern "C" fn CorePluginInit() -> bool {
workflow::insert_workflow();
- binaryninja::command::register_command(
+ let plugin_settings = PluginSettings::from_settings(&global_bn_settings);
+ // We want to load all the bundled directories into the container cache.
+ let background_task = BackgroundTask::new("Loading WARP files...", false);
+ let start = Instant::now();
+ if plugin_settings.load_bundled_files {
+ let core_disk_container = DiskContainer::new_from_dir(core_signature_dir());
+ log::debug!("{:#?}", core_disk_container);
+ add_cached_container(core_disk_container);
+ }
+ if plugin_settings.load_user_files {
+ let user_disk_container = DiskContainer::new_from_dir(user_signature_dir());
+ log::debug!("{:#?}", user_disk_container);
+ add_cached_container(user_disk_container);
+ }
+ log::info!("Loading bundled files took {:?}", start.elapsed());
+ background_task.finish();
+
+ register_command(
"WARP\\Run Matcher",
"Run the matcher manually",
workflow::RunMatcher {},
);
- binaryninja::command::register_command(
+ register_command(
"WARP\\Debug\\Cache",
"Debug cache sizes... because...",
- DebugCache {},
+ debug::DebugCache {},
);
- binaryninja::command::register_command(
+ register_command(
"WARP\\Debug\\Invalidate Caches",
"Invalidate all WARP caches",
- DebugInvalidateCache {},
+ debug::DebugInvalidateCache {},
);
- binaryninja::command::register_command_for_function(
+ register_command_for_function(
"WARP\\Debug\\Function Signature",
"Print the entire signature for the function",
- DebugFunction {},
- );
-
- binaryninja::command::register_command_for_function(
- "WARP\\Debug\\Function Matcher",
- "Print all possible matches for the function",
- DebugMatcher {},
+ debug::DebugFunction {},
);
- binaryninja::command::register_command(
- "WARP\\Debug\\Apply Signature File Types",
- "Load all types from a signature file and ignore functions",
- types::LoadTypes {},
- );
-
- binaryninja::command::register_command(
- "WARP\\Load Signature File",
+ register_command(
+ "WARP\\Load File",
"Load file into the matcher, this does NOT kick off matcher analysis",
load::LoadSignatureFile {},
);
- binaryninja::command::register_command_for_function(
- "WARP\\Copy Function GUID",
+ register_command_for_function(
+ "WARP\\Function\\Include",
+ "Add current function to the list of functions to add to the signature file",
+ function::IncludeFunction {},
+ );
+
+ register_command_for_function(
+ "WARP\\Function\\Copy GUID",
"Copy the computed GUID for the function",
- copy::CopyFunctionGUID {},
+ function::CopyFunctionGUID {},
);
- binaryninja::command::register_command(
- "WARP\\Find Function From GUID",
+ register_command(
+ "WARP\\Function\\Find GUID",
"Locate the function in the view using a GUID",
- find::FindFunctionFromGUID {},
+ function::FindFunctionFromGUID {},
+ );
+
+ register_command(
+ "WARP\\Create\\From Current View",
+ "Creates a signature file containing all selected functions",
+ create::CreateFromCurrentView {},
+ );
+
+ register_command(
+ "WARP\\Create\\From File(s)",
+ "Creates a signature file containing all selected functions",
+ create::CreateFromFiles {},
);
- binaryninja::command::register_command(
- "WARP\\Generate Signature File",
- "Generates a signature file containing all binary view functions",
- create::CreateSignatureFile {},
+ register_command(
+ "WARP\\Show Report",
+ "Creates a report for the selected file, displaying info on functions and types",
+ file::ShowFileReport {},
);
- binaryninja::command::register_command_for_function(
- "WARP\\Add Function Signature to File",
- "Stores the signature for the function in the signature file",
- add::AddFunctionSignature {},
+ 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/add.rs b/plugins/warp/src/plugin/add.rs
deleted file mode 100644
index 2f76c8b5..00000000
--- a/plugins/warp/src/plugin/add.rs
+++ /dev/null
@@ -1,72 +0,0 @@
-use crate::cache::{cached_function, cached_type_references};
-use crate::matcher::invalidate_function_matcher_cache;
-use crate::user_signature_dir;
-use binaryninja::binary_view::BinaryView;
-use binaryninja::command::FunctionCommand;
-use binaryninja::function::Function;
-use std::thread;
-
-pub struct AddFunctionSignature;
-
-impl FunctionCommand for AddFunctionSignature {
- fn action(&self, view: &BinaryView, func: &Function) {
- let func_plat_name = func.platform().name().to_string();
- let signature_dir = user_signature_dir().join(func_plat_name);
- let view = view.to_owned();
- let func = func.to_owned();
- thread::spawn(move || {
- let Ok(llil) = func.low_level_il() else {
- log::error!("Could not get low level IL for function.");
- return;
- };
-
- // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API.
- // If we did need to save signature files to a project than this would need to change.
- let Some(save_file) = rfd::FileDialog::new()
- .add_filter("Signature Files", &["sbin"])
- .set_file_name("user.sbin")
- .set_directory(signature_dir)
- .save_file()
- else {
- return;
- };
-
- let mut data = warp::signature::Data::default();
- if let Ok(file_bytes) = std::fs::read(&save_file) {
- // If the file we are adding the function to already has data we should preserve it!
- log::info!("Signature file already exists, preserving data...");
- let Some(file_data) = warp::signature::Data::from_bytes(&file_bytes) else {
- log::error!("Could not get data from signature file: {:?}", save_file);
- return;
- };
- data = file_data;
- };
-
- // Now add our function to the data.
- data.functions.push(cached_function(&func, &llil));
-
- if let Some(ref_ty_cache) = cached_type_references(&view) {
- let referenced_types = ref_ty_cache
- .cache
- .iter()
- .filter_map(|t| t.to_owned())
- .collect::<Vec<_>>();
-
- data.types.extend(referenced_types);
- }
-
- match std::fs::write(&save_file, data.to_bytes()) {
- Ok(_) => {
- log::info!("Signature file saved successfully.");
- // Force rebuild platform matcher.
- invalidate_function_matcher_cache();
- }
- Err(e) => log::error!("Failed to write data to signature file: {:?}", e),
- }
- });
- }
-
- fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/copy.rs b/plugins/warp/src/plugin/copy.rs
deleted file mode 100644
index b9b985fc..00000000
--- a/plugins/warp/src/plugin/copy.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use binaryninja::binary_view::BinaryView;
-use binaryninja::command::FunctionCommand;
-use binaryninja::function::Function;
-
-use crate::cache::cached_function_guid;
-
-pub struct CopyFunctionGUID;
-
-impl FunctionCommand for CopyFunctionGUID {
- fn action(&self, _view: &BinaryView, func: &Function) {
- let Ok(llil) = func.low_level_il() else {
- log::error!("Could not get low level il for copied function");
- return;
- };
- let guid = cached_function_guid(func, &llil);
- log::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
- }
-}
diff --git a/plugins/warp/src/plugin/create.rs b/plugins/warp/src/plugin/create.rs
index 1dd83e6e..9ebcba2d 100644
--- a/plugins/warp/src/plugin/create.rs
+++ b/plugins/warp/src/plugin/create.rs
@@ -1,94 +1,232 @@
-use crate::cache::{cached_function, cached_type_references};
-use crate::matcher::invalidate_function_matcher_cache;
-use crate::user_signature_dir;
+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;
-use binaryninja::function::Function;
-use binaryninja::rc::Guard;
-use rayon::prelude::*;
-use std::sync::atomic::AtomicUsize;
-use std::sync::atomic::Ordering::Relaxed;
+use binaryninja::interaction::form::{Form, FormInputField};
+use binaryninja::interaction::{MessageBoxButtonResult, MessageBoxButtonSet, MessageBoxIcon};
+use binaryninja::rc::Ref;
+use std::path::PathBuf;
use std::thread;
-use std::time::Instant;
+use warp::chunk::Chunk;
+use warp::WarpFile;
-pub struct CreateSignatureFile;
+pub struct SaveFileField;
-// TODO: Prompt the user to add the newly created signature file to the signature blacklist (so that it doesn't keep getting applied)
+impl SaveFileField {
+ pub fn field(view: &BinaryView) -> FormInputField {
+ let default_name = view
+ .file()
+ .filename()
+ .split('/')
+ .last()
+ .unwrap_or("file")
+ .to_string();
+ 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_name: Some(default_name),
+ default: Some(default_file_path.to_string_lossy().to_string()),
+ value: None,
+ }
+ }
-impl Command for CreateSignatureFile {
- fn action(&self, view: &BinaryView) {
- let is_function_named = |f: &Guard<Function>| {
- !f.symbol().short_name().to_string_lossy().contains("sub_") || f.has_user_annotations()
- };
- let mut signature_dir = user_signature_dir();
- if let Some(default_plat) = view.default_platform() {
- // If there is a default platform, put the signature in there.
- // TODO: We should instead use the platform of the function.
- signature_dir.push(default_plat.name().to_string());
+ 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,
}
- let view = view.to_owned();
- thread::spawn(move || {
- let total_functions = view.functions().len();
- let done_functions = AtomicUsize::default();
- let background_task = binaryninja::background_task::BackgroundTask::new(
- &format!("Generating signatures... ({}/{})", 0, total_functions),
- true,
- );
+ }
+
+ 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;
- let start = Instant::now();
+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");
- let mut data = warp::signature::Data::default();
- data.functions.par_extend(
- view.functions()
- .par_iter()
- .inspect(|_| {
- done_functions.fetch_add(1, Relaxed);
- background_task.set_progress_text(&format!(
- "Generating signatures... ({}/{})",
- done_functions.load(Relaxed),
- total_functions
- ))
- })
- .filter(is_function_named)
- .filter(|f| !f.analysis_skipped())
- .filter_map(|func| {
- let llil = func.low_level_il().ok()?;
- Some(cached_function(&func, &llil))
- }),
+ 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,
);
- if let Some(ref_ty_cache) = cached_type_references(&view) {
- let referenced_types = ref_ty_cache
- .cache
- .iter()
- .filter_map(|t| t.to_owned())
- .collect::<Vec<_>>();
+ 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 => {
+ log::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);
- data.types.extend(referenced_types);
+ 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(
+ "Error",
+ &format!("Failed to create signature file: {}", err),
+ MessageBoxButtonSet::OKButtonSet,
+ MessageBoxIcon::ErrorIcon,
+ );
+ log::error!("Failed to create signature file: {}", err);
+ return None;
+ }
- log::info!("Signature generation took {:?}", start.elapsed());
- background_task.finish();
+ let mut file = file.unwrap();
+ // Add back the existing chunks if the user selected to keep them.
+ file.chunks.extend(existing_chunks);
+ // TODO: Make merging optional?
+ file.chunks = Chunk::merge(&file.chunks, compression_type.into());
+
+ if std::fs::write(&file_path, file.to_bytes()).is_err() {
+ log::error!("Failed to write data to signature file!");
+ }
- // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API.
- // If we did need to save signature files to a project than this would need to change.
- let Some(save_file) = rfd::FileDialog::new()
- .add_filter("Signature Files", &["sbin"])
- .set_file_name(format!("{}.sbin", view.file().filename()))
- .set_directory(signature_dir)
- .save_file()
- else {
- return;
- };
+ // 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);
+ }
- match std::fs::write(&save_file, data.to_bytes()) {
- Ok(_) => {
- log::info!("Signature file saved successfully.");
- // Force rebuild platform matcher.
- invalidate_function_matcher_cache();
+ 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());
}
- Err(e) => log::error!("Failed to write data to signature file: {:?}", e),
}
+ }
+
+ 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 Command for CreateFromFiles {
+ fn action(&self, view: &BinaryView) {
+ let view = view.to_owned();
+ thread::spawn(move || {
+ CreateFromCurrentView::execute(view, true);
});
}
diff --git a/plugins/warp/src/plugin/debug.rs b/plugins/warp/src/plugin/debug.rs
new file mode 100644
index 00000000..fc4e5817
--- /dev/null
+++ b/plugins/warp/src/plugin/debug.rs
@@ -0,0 +1,48 @@
+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::ObjectDestructor;
+
+pub struct DebugFunction;
+
+impl FunctionCommand for DebugFunction {
+ fn action(&self, _view: &BinaryView, func: &Function) {
+ if let Ok(lifted_il) = func.lifted_il() {
+ log::info!("{:#?}", build_function(func, &lifted_il));
+ }
+ }
+
+ 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| {
+ log::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);
+ log::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
new file mode 100644
index 00000000..d78ca3c5
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi.rs
@@ -0,0 +1,205 @@
+mod container;
+mod function;
+
+use binaryninjacore_sys::{
+ BNBasicBlock, BNBinaryView, BNFunction, BNLowLevelILFunction, BNPlatform,
+};
+use std::ffi::c_char;
+use std::sync::{Arc, RwLock};
+use uuid::Uuid;
+
+use binaryninja::basic_block::{BasicBlock, BasicBlockType};
+use binaryninja::function::{Function, NativeBlock};
+
+use crate::cache::cached_function_guid;
+use crate::container::{Container, SourceId};
+use crate::convert::platform_to_target;
+use crate::plugin::workflow::run_matcher;
+use crate::{
+ basic_block_guid, is_blacklisted_instruction, is_computed_variant_instruction,
+ is_variant_instruction, relocatable_regions,
+};
+use binaryninja::binary_view::BinaryView;
+use binaryninja::low_level_il::function::{LowLevelILFunction, Mutable, NonSSA};
+use binaryninja::low_level_il::instruction::LowLevelInstructionIndex;
+use binaryninja::platform::Platform;
+use binaryninja::string::BnString;
+use warp::r#type::guid::TypeGUID;
+use warp::signature::basic_block::BasicBlockGUID;
+use warp::signature::constraint::{Constraint, ConstraintGUID, UNRELATED_OFFSET};
+use warp::signature::function::FunctionGUID;
+
+/// [`SourceId`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI.
+pub type BNWARPSource = SourceId;
+
+/// [`BasicBlockGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI.
+pub type BNWARPBasicBlockGUID = BasicBlockGUID;
+
+/// [`ConstraintGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI.
+pub type BNWARPConstraintGUID = ConstraintGUID;
+
+/// [`FunctionGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI.
+pub type BNWARPFunctionGUID = FunctionGUID;
+
+/// [`TypeGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI.
+pub type BNWARPTypeGUID = TypeGUID;
+
+pub type BNWARPTarget = warp::target::Target;
+pub type BNWARPFunction = warp::signature::function::Function;
+pub type BNWARPContainer = RwLock<Box<dyn Container>>;
+
+// TODO: Some sort of callback for loading functions
+// TODO: Be able to run matcher for a specific file
+// TODO: Generate signatures for a file, return what?
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+pub struct BNWARPConstraint {
+ guid: BNWARPConstraintGUID,
+ offset: i64,
+}
+
+impl From<BNWARPConstraint> for Constraint {
+ fn from(constraint: BNWARPConstraint) -> Self {
+ Constraint {
+ guid: constraint.guid,
+ offset: match constraint.offset {
+ UNRELATED_OFFSET => None,
+ _ => Some(constraint.offset),
+ },
+ }
+ }
+}
+
+impl From<Constraint> for BNWARPConstraint {
+ fn from(constraint: Constraint) -> Self {
+ BNWARPConstraint {
+ guid: constraint.guid,
+ offset: constraint.offset.unwrap_or(UNRELATED_OFFSET),
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPUUIDGetString(uuid: *const Uuid) -> *mut c_char {
+ let uuid_str = (*uuid).to_string();
+ // NOTE: Leak the uuid string to be freed by BNFreeString
+ BnString::into_raw(uuid_str.into())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPUUIDEqual(a: *const Uuid, b: *const Uuid) -> bool {
+ (*a) == (*b)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPRunMatcher(view: *mut BNBinaryView) {
+ let view = BinaryView::from_raw(view);
+ run_matcher(&view)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetBasicBlockGUID(
+ basic_block: *mut BNBasicBlock,
+ result: *mut BNWARPBasicBlockGUID,
+) -> bool {
+ let basic_block = unsafe { BasicBlock::from_raw(basic_block, NativeBlock::new()) };
+ if basic_block.block_type() != BasicBlockType::Native {
+ return false;
+ }
+ let function = basic_block.function();
+ match function.lifted_il() {
+ Ok(lifted_il) => {
+ let relocatable_regions = relocatable_regions(&function.view());
+ *result = basic_block_guid(&relocatable_regions, &basic_block, &lifted_il);
+ true
+ }
+ Err(_) => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetAnalysisFunctionGUID(
+ analysis_function: *mut BNFunction,
+ result: *mut BNWARPFunctionGUID,
+) -> bool {
+ let function = unsafe { Function::from_raw(analysis_function) };
+ match function.lifted_il() {
+ Ok(lifted_il) => {
+ *result = cached_function_guid(&function, &lifted_il);
+ true
+ }
+ Err(_) => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPIsLiftedInstructionVariant(
+ analysis_function: *mut BNLowLevelILFunction,
+ index: LowLevelInstructionIndex,
+) -> bool {
+ let lifted_il: LowLevelILFunction<Mutable, NonSSA> =
+ unsafe { LowLevelILFunction::from_raw(analysis_function) };
+ match lifted_il.instruction_from_index(index) {
+ Some(instr) => {
+ let relocatable_regions = relocatable_regions(&lifted_il.function().view());
+ is_variant_instruction(&relocatable_regions, &instr)
+ }
+ None => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPIsLowLevelInstructionComputedVariant(
+ analysis_function: *mut BNLowLevelILFunction,
+ index: LowLevelInstructionIndex,
+) -> bool {
+ let llil: LowLevelILFunction<Mutable, NonSSA> =
+ unsafe { LowLevelILFunction::from_raw(analysis_function) };
+ match llil.instruction_from_index(index) {
+ Some(instr) => {
+ let relocatable_regions = relocatable_regions(&llil.function().view());
+ is_computed_variant_instruction(&relocatable_regions, &instr)
+ }
+ None => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPIsLiftedInstructionBlacklisted(
+ analysis_function: *mut BNLowLevelILFunction,
+ index: LowLevelInstructionIndex,
+) -> bool {
+ let lifted_il: LowLevelILFunction<Mutable, NonSSA> =
+ unsafe { LowLevelILFunction::from_raw(analysis_function) };
+ match lifted_il.instruction_from_index(index) {
+ Some(instr) => is_blacklisted_instruction(&instr),
+ None => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeUUIDList(uuids: *mut Uuid, count: usize) {
+ let sources_ptr = std::ptr::slice_from_raw_parts_mut(uuids, count);
+ let _ = unsafe { Box::from_raw(sources_ptr) };
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetTarget(platform: *mut BNPlatform) -> *mut BNWARPTarget {
+ let platform = Platform::from_raw(platform);
+ Arc::into_raw(Arc::new(platform_to_target(&platform))) as *mut BNWARPTarget
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewTargetReference(target: *mut BNWARPTarget) -> *mut BNWARPTarget {
+ Arc::increment_strong_count(target);
+ target
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeTargetReference(target: *mut BNWARPTarget) {
+ if target.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(target);
+}
diff --git a/plugins/warp/src/plugin/ffi/container.rs b/plugins/warp/src/plugin/ffi/container.rs
new file mode 100644
index 00000000..21ad4dc0
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi/container.rs
@@ -0,0 +1,412 @@
+use crate::cache::container::cached_containers;
+use crate::container::SourcePath;
+use crate::convert::{from_bn_type, to_bn_type};
+use crate::plugin::ffi::{
+ BNWARPContainer, BNWARPFunction, BNWARPFunctionGUID, BNWARPSource, BNWARPTarget, 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::ffi::{c_char, CStr};
+use std::mem::ManuallyDrop;
+use std::sync::Arc;
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetContainers(count: *mut usize) -> *mut *mut BNWARPContainer {
+ // NOTE: Leak the arc pointers to be freed by BNWARPFreeContainerList
+ let boxed_raw_containers: Box<[_]> =
+ cached_containers().into_iter().map(Arc::into_raw).collect();
+ *count = boxed_raw_containers.len();
+ let leaked_raw_containers = Box::into_raw(boxed_raw_containers);
+ leaked_raw_containers as *mut *mut BNWARPContainer
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetName(container: *mut BNWARPContainer) -> *const c_char {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null();
+ };
+ let name = container.to_string();
+ // NOTE: Leak the container name to be freed by BNFreeString
+ BnString::into_raw(name.into())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetSources(
+ container: *mut BNWARPContainer,
+ count: *mut usize,
+) -> *mut BNWARPSource {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.write() else {
+ return std::ptr::null_mut();
+ };
+
+ // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ let boxed_sources: Box<[_]> = container.sources().unwrap_or_default().into_boxed_slice();
+ *count = boxed_sources.len();
+ Box::into_raw(boxed_sources) as *mut BNWARPSource
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerAddSource(
+ container: *mut BNWARPContainer,
+ source_path: *const c_char,
+ result: *mut BNWARPSource,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let source_path_cstr = unsafe { CStr::from_ptr(source_path) };
+ let source_path_str = source_path_cstr.to_str().unwrap();
+ let source_path = SourcePath::new_with_str(source_path_str);
+
+ match container.add_source(source_path) {
+ Ok(source) => {
+ // NOTE: Leak the source to be freed by BNFreeString
+ *result = source;
+ true
+ }
+ Err(_) => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerCommitSource(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let source = unsafe { *source };
+
+ container
+ .commit_source(&source)
+ .is_ok_and(|committed| committed)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerIsSourceUncommitted(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return false;
+ };
+
+ let source = unsafe { *source };
+
+ container
+ .is_source_uncommitted(&source)
+ .is_ok_and(|uncommitted| uncommitted)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerIsSourceWritable(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return false;
+ };
+
+ let source = unsafe { *source };
+
+ container
+ .is_source_writable(&source)
+ .is_ok_and(|writable| writable)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetSourcePath(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+) -> *const c_char {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null();
+ };
+
+ let source = unsafe { *source };
+
+ match container.source_path(&source) {
+ Ok(path) => {
+ let path = path.to_string();
+ // NOTE: Leak the source path to be freed by BNFreeString
+ BnString::into_raw(path.into())
+ }
+ Err(_) => std::ptr::null(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerAddFunctions(
+ container: *mut BNWARPContainer,
+ target: *mut BNWARPTarget,
+ source: *const BNWARPSource,
+ functions: *mut *mut BNWARPFunction,
+ count: usize,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) };
+
+ let source = unsafe { *source };
+
+ let functions_ptr = std::slice::from_raw_parts(functions, count);
+ // TODO: We have to clone the objects here to make the type checker happy.
+ // TODO: See about avoiding this later.
+ let functions: Vec<_> = functions_ptr
+ .iter()
+ .map(|&f| unsafe { ManuallyDrop::new(Arc::from_raw(f)).as_ref().clone() })
+ .collect();
+ container
+ .add_functions(&target, &source, &functions)
+ .is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerAddTypes(
+ view: *mut BNBinaryView,
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+ types: *mut *mut BNType,
+ 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;
+ };
+
+ let source = unsafe { *source };
+
+ let types_ptr = std::slice::from_raw_parts(types, count);
+ let types: Vec<_> = types_ptr
+ .iter()
+ .map(|&t| Type::from_raw(t))
+ .map(|ty| from_bn_type(&view, &ty, 255))
+ .collect();
+ container.add_types(&source, &types).is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerRemoveFunctions(
+ container: *mut BNWARPContainer,
+ target: *mut BNWARPTarget,
+ source: *const BNWARPSource,
+ functions: *mut *mut BNWARPFunction,
+ count: usize,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) };
+
+ let source = unsafe { *source };
+
+ let functions_ptr = std::slice::from_raw_parts(functions, count);
+ // TODO: We have to clone the objects here to make the type checker happy.
+ // TODO: See about avoiding this later.
+ let functions: Vec<_> = functions_ptr
+ .iter()
+ .map(|&f| unsafe { ManuallyDrop::new(Arc::from_raw(f)).as_ref().clone() })
+ .collect();
+ container
+ .remove_functions(&target, &source, &functions)
+ .is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerRemoveTypes(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+ guids: *mut BNWARPTypeGUID,
+ count: usize,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let source = unsafe { *source };
+
+ let guids = std::slice::from_raw_parts(guids, count);
+ container.remove_types(&source, &guids).is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetSourcesWithFunctionGUID(
+ container: *mut BNWARPContainer,
+ target: *mut BNWARPTarget,
+ guid: *const BNWARPFunctionGUID,
+ count: *mut usize,
+) -> *mut BNWARPSource {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null_mut();
+ };
+
+ let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) };
+
+ let guid = unsafe { *guid };
+
+ // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ let boxed_sources: Box<[_]> = container
+ .sources_with_function_guid(&target, &guid)
+ .unwrap_or_default()
+ .into_boxed_slice();
+ *count = boxed_sources.len();
+ Box::into_raw(boxed_sources) as *mut BNWARPSource
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetSourcesWithTypeGUID(
+ container: *mut BNWARPContainer,
+ guid: *const BNWARPTypeGUID,
+ count: *mut usize,
+) -> *mut BNWARPSource {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null_mut();
+ };
+
+ let guid = unsafe { *guid };
+
+ // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ let boxed_sources: Box<[_]> = container
+ .sources_with_type_guid(&guid)
+ .unwrap_or_default()
+ .into_boxed_slice();
+ *count = boxed_sources.len();
+ Box::into_raw(boxed_sources) as *mut BNWARPSource
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetFunctionsWithGUID(
+ container: *mut BNWARPContainer,
+ target: *mut BNWARPTarget,
+ source: *const BNWARPSource,
+ guid: *const BNWARPFunctionGUID,
+ count: *mut usize,
+) -> *mut *mut BNWARPFunction {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null_mut();
+ };
+
+ let source = unsafe { *source };
+
+ let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) };
+
+ let guid = unsafe { *guid };
+
+ // NOTE: Leak the functions to be freed by BNWARPFreeFunctionList
+ let raw_boxed_functions: Box<[_]> = container
+ .functions_with_guid(&target, &source, &guid)
+ .unwrap_or_default()
+ .into_iter()
+ .map(Arc::new)
+ .map(Arc::into_raw)
+ .collect();
+ *count = raw_boxed_functions.len();
+ 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 {
+ 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 };
+
+ let Some(ty) = container.type_with_guid(&source, &guid).unwrap_or_default() else {
+ return std::ptr::null_mut();
+ };
+ let function_type = to_bn_type(&arch, &ty);
+ // NOTE: The type ref has been pre-incremented for the caller.
+ unsafe { Ref::into_raw(function_type) }.handle
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetTypeGUIDsWithName(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+ name: *const c_char,
+ count: *mut usize,
+) -> *mut BNWARPTypeGUID {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null_mut();
+ };
+
+ let source = unsafe { *source };
+
+ let name_cstr = unsafe { CStr::from_ptr(name) };
+ let name = name_cstr.to_str().unwrap();
+
+ // NOTE: Leak the guids to be freed by BNWARPFreeTypeGUIDList
+ let boxed_guids = container
+ .type_guids_with_name(&source, name)
+ .unwrap_or_default()
+ .into_boxed_slice();
+ *count = boxed_guids.len();
+ Box::into_raw(boxed_guids) as *mut BNWARPTypeGUID
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewContainerReference(
+ container: *mut BNWARPContainer,
+) -> *mut BNWARPContainer {
+ Arc::increment_strong_count(container);
+ container
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeContainerReference(container: *mut BNWARPContainer) {
+ if container.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(container);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeContainerList(
+ containers: *mut *mut BNWARPContainer,
+ count: usize,
+) {
+ let containers_ptr = std::ptr::slice_from_raw_parts_mut(containers, count);
+ let containers = unsafe { Box::from_raw(containers_ptr) };
+ for container in containers {
+ BNWARPFreeContainerReference(container);
+ }
+}
diff --git a/plugins/warp/src/plugin/ffi/function.rs b/plugins/warp/src/plugin/ffi/function.rs
new file mode 100644
index 00000000..3db8f307
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi/function.rs
@@ -0,0 +1,228 @@
+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 binaryninja::function::Function;
+use binaryninja::rc::Ref;
+use binaryninja::string::BnString;
+use binaryninjacore_sys::{BNFunction, BNSymbol, BNType};
+use std::ffi::c_char;
+use std::mem::ManuallyDrop;
+use std::sync::Arc;
+use warp::signature::comment::FunctionComment;
+
+#[repr(C)]
+pub struct BNWarpFunctionComment {
+ pub text: *mut c_char,
+ pub offset: i64,
+}
+
+impl BNWarpFunctionComment {
+ /// Leaks the text string to be freed with BNWARPFreeFunctionComment
+ pub fn from_owned(value: &FunctionComment) -> Self {
+ let text = BnString::into_raw(BnString::new(&value.text));
+ Self {
+ text,
+ offset: value.offset,
+ }
+ }
+
+ pub unsafe fn free_raw(value: &Self) {
+ unsafe { BnString::free_raw(value.text) }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetFunction(
+ analysis_function: *mut BNFunction,
+) -> *mut BNWARPFunction {
+ let function = Function::from_raw(analysis_function);
+ let Ok(lifted_il) = function.lifted_il() else {
+ return std::ptr::null_mut();
+ };
+ let function = build_function(&function, &lifted_il);
+ Arc::into_raw(Arc::new(function)) as *mut BNWARPFunction
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetMatchedFunction(
+ analysis_function: *mut BNFunction,
+) -> *mut BNWARPFunction {
+ let function = Function::from_raw(analysis_function);
+ match try_cached_function_match(&function) {
+ Some(matched_function) => {
+ let arc_matched_function = Arc::new(matched_function);
+ // NOTE: Freed by BNWARPFreeFunctionReference
+ Arc::into_raw(arc_matched_function) as *mut BNWARPFunction
+ }
+ None => std::ptr::null_mut(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionApply(
+ function: *mut BNWARPFunction,
+ analysis_function: *mut BNFunction,
+) {
+ let analysis_function = Function::from_raw(analysis_function);
+ match function.is_null() {
+ false => {
+ // Set the matched function to `function` and return previous.
+ let matched_function = ManuallyDrop::new(Arc::from_raw(function));
+ insert_cached_function_match(
+ &analysis_function,
+ Some(matched_function.as_ref().clone()),
+ )
+ }
+ true => {
+ // We are removing the previous match and returning it.
+ insert_cached_function_match(&analysis_function, None)
+ }
+ };
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetGUID(
+ function: *mut BNWARPFunction,
+) -> BNWARPFunctionGUID {
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ function.guid
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetSymbol(
+ function: *mut BNWARPFunction,
+ analysis_function: *mut BNFunction,
+) -> *mut BNSymbol {
+ let analysis_function = Function::from_raw(analysis_function);
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ let view = analysis_function.view();
+ let address = analysis_function.symbol().address();
+ let function_symbol = to_bn_symbol_at_address(&view, &function.symbol, address);
+ // NOTE: The symbol ref has been pre-incremented for the caller.
+ Ref::into_raw(function_symbol).handle
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetSymbolName(function: *mut BNWARPFunction) -> *mut c_char {
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ let bn_name = BnString::new(&function.symbol.name);
+ // NOTE: The symbol name string to be freed by BNFreeString
+ BnString::into_raw(bn_name)
+}
+
+#[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);
+ // 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(&arch, func_ty);
+ // NOTE: The type ref has been pre-incremented for the caller.
+ unsafe { Ref::into_raw(function_type) }.handle
+ }
+ None => std::ptr::null_mut(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetConstraints(
+ function: *mut BNWARPFunction,
+ count: *mut usize,
+) -> *mut BNWARPConstraint {
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ let raw_constraints: Box<[BNWARPConstraint]> = function
+ .constraints
+ .clone()
+ .into_iter()
+ .map(Into::into)
+ .collect();
+ *count = raw_constraints.len();
+ let raw_constraints_ptr = Box::into_raw(raw_constraints);
+ raw_constraints_ptr as *mut BNWARPConstraint
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetComments(
+ function: *mut BNWARPFunction,
+ count: *mut usize,
+) -> *mut BNWarpFunctionComment {
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ let raw_comments: Box<[_]> = function
+ .comments
+ .iter()
+ .map(BNWarpFunctionComment::from_owned)
+ .collect();
+ *count = raw_comments.len();
+ let raw_comments_ptr = Box::into_raw(raw_comments);
+ raw_comments_ptr as *mut BNWarpFunctionComment
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionsEqual(
+ function_a: *mut BNWARPFunction,
+ function_b: *mut BNWARPFunction,
+) -> bool {
+ // We do not own function so we should not drop.
+ let function_a = ManuallyDrop::new(Arc::from_raw(function_a));
+ // We do not own function so we should not drop.
+ let function_b = ManuallyDrop::new(Arc::from_raw(function_b));
+ function_a.eq(&function_b)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewFunctionReference(
+ function: *mut BNWARPFunction,
+) -> *mut BNWARPFunction {
+ Arc::increment_strong_count(function);
+ function
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeFunctionReference(function: *mut BNWARPFunction) {
+ if function.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(function);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeFunctionList(functions: *mut *mut BNWARPFunction, count: usize) {
+ let functions_ptr = std::ptr::slice_from_raw_parts_mut(functions, count);
+ let functions = Box::from_raw(functions_ptr);
+ for function in functions {
+ // NOTE: The functions themselves should also be arc.
+ BNWARPFreeFunctionReference(function);
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeFunctionCommentList(
+ comments: *mut BNWarpFunctionComment,
+ count: usize,
+) {
+ let comments_ptr = std::ptr::slice_from_raw_parts_mut(comments, count);
+ let comments = Box::from_raw(comments_ptr);
+ for comment in &comments {
+ BNWarpFunctionComment::free_raw(comment)
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeConstraintList(
+ constraints: *mut BNWARPConstraint,
+ count: usize,
+) {
+ let constraints_ptr = std::ptr::slice_from_raw_parts_mut(constraints, count);
+ let _constraints = unsafe { Box::from_raw(constraints_ptr) };
+}
diff --git a/plugins/warp/src/plugin/file.rs b/plugins/warp/src/plugin/file.rs
new file mode 100644
index 00000000..ec61bd78
--- /dev/null
+++ b/plugins/warp/src/plugin/file.rs
@@ -0,0 +1,41 @@
+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 {
+ log::error!("Failed to read file: {:?}", path);
+ return;
+ };
+
+ let Some(file) = warp::WarpFile::from_bytes(&bytes) else {
+ log::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/find.rs b/plugins/warp/src/plugin/find.rs
deleted file mode 100644
index accc015d..00000000
--- a/plugins/warp/src/plugin/find.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use crate::cache::try_cached_function_guid;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::Command;
-use binaryninja::function::Function as BNFunction;
-use binaryninja::rc::Guard as BNGuard;
-use rayon::prelude::*;
-use std::thread;
-use warp::signature::function::FunctionGUID;
-
-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 {
- log::error!("Failed to parse function guid... {}", guid_str);
- return;
- };
-
- log::info!("Searching functions for GUID... {}", searched_guid);
- let funcs = view.functions();
- thread::spawn(move || {
- let background_task = binaryninja::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 = funcs
- .par_iter()
- .filter(|func| {
- try_cached_function_guid(func).is_some_and(|guid| guid == searched_guid)
- })
- .collect::<Vec<BNGuard<BNFunction>>>();
-
- if matched.is_empty() {
- log::info!("No matches found for GUID... {}", searched_guid);
- } else {
- for func in matched {
- log::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/function.rs b/plugins/warp/src/plugin/function.rs
new file mode 100644
index 00000000..130e8582
--- /dev/null
+++ b/plugins/warp/src/plugin/function.rs
@@ -0,0 +1,133 @@
+use crate::cache::{cached_function_guid, try_cached_function_guid};
+use crate::{get_warp_include_tag_type, INCLUDE_TAG_NAME};
+use binaryninja::background_task::BackgroundTask;
+use binaryninja::binary_view::{BinaryView, BinaryViewExt};
+use binaryninja::command::{Command, FunctionCommand};
+use binaryninja::function::Function;
+use binaryninja::rc::Guard;
+use rayon::iter::ParallelIterator;
+use std::thread;
+use warp::signature::function::FunctionGUID;
+
+pub struct IncludeFunction;
+
+impl FunctionCommand for IncludeFunction {
+ fn action(&self, view: &BinaryView, func: &Function) {
+ let sym_name = func.symbol().short_name();
+ let sym_name_str = sym_name.to_string_lossy();
+ let should_add_tag = func.function_tags(None, Some(INCLUDE_TAG_NAME)).is_empty();
+ let insert_tag_type = get_warp_include_tag_type(view);
+ match should_add_tag {
+ true => {
+ log::info!(
+ "Including selected function '{}' at 0x{:x}",
+ sym_name_str,
+ func.start()
+ );
+ func.add_tag(&insert_tag_type, "", None, false, None);
+ }
+ false => {
+ log::info!(
+ "Removing included function '{}' at 0x{:x}",
+ sym_name_str,
+ func.start()
+ );
+ func.remove_tags_of_type(&insert_tag_type, None, false, None);
+ }
+ }
+ }
+
+ fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
+ // TODO: Only allow if the function is named?
+ true
+ }
+}
+
+pub struct CopyFunctionGUID;
+
+impl FunctionCommand for CopyFunctionGUID {
+ fn action(&self, _view: &BinaryView, func: &Function) {
+ let Ok(lifted_il) = func.lifted_il() else {
+ log::error!("Could not get lifted il for copied function");
+ return;
+ };
+ let guid = cached_function_guid(func, &lifted_il);
+ log::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 {
+ log::error!("Failed to parse function guid... {}", guid_str);
+ return;
+ };
+
+ log::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() {
+ log::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()
+ {
+ log::error!(
+ "Failed to navigate to found function 0x{:0x} in view {}",
+ func.start(),
+ current_view
+ );
+ }
+ }
+ log::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/load.rs b/plugins/warp/src/plugin/load.rs
index c86a16cb..c0319ea5 100644
--- a/plugins/warp/src/plugin/load.rs
+++ b/plugins/warp/src/plugin/load.rs
@@ -1,54 +1,162 @@
-use crate::matcher::{Matcher, PlatformID, PLAT_MATCHER_CACHE};
+use crate::cache::container::add_cached_container;
+use crate::container::disk::{DiskContainer, DiskContainerSource};
+use crate::container::{ContainerError, SourcePath};
+use crate::convert::platform_to_target;
+use crate::plugin::workflow::run_matcher;
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
use binaryninja::command::Command;
+use binaryninja::interaction::{
+ show_message_box, Form, FormInputField, MessageBoxButtonResult, MessageBoxButtonSet,
+ MessageBoxIcon,
+};
+use binaryninja::rc::Ref;
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::thread;
+use warp::WarpFile;
+
+pub struct LoadFileField;
+
+impl LoadFileField {
+ pub fn field() -> FormInputField {
+ FormInputField::OpenFileName {
+ prompt: "File Path".to_string(),
+ // TODO: This is called extension but is really a filter.
+ extension: Some("*.warp".to_string()),
+ default: None,
+ 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 RunMatcherField;
+
+impl RunMatcherField {
+ pub fn field() -> FormInputField {
+ FormInputField::Choice {
+ prompt: "Rerun Initial Matcher".to_string(),
+ choices: vec!["No".to_string(), "Yes".to_string()],
+ default: Some(1),
+ value: 0,
+ }
+ }
+
+ pub fn from_form(form: &Form) -> Option<bool> {
+ let field = form.get_field_with_name("Rerun Initial Matcher")?;
+ let field_value = field.try_value_index()?;
+ match field_value {
+ 1 => Some(true),
+ _ => Some(false),
+ }
+ }
+}
+
pub struct LoadSignatureFile;
-impl Command for LoadSignatureFile {
- fn action(&self, view: &BinaryView) {
- let Some(platform) = view.default_platform() else {
- log::error!("Default platform must be set to load signature!");
- return;
- };
+impl LoadSignatureFile {
+ pub fn read_file(
+ view: &BinaryView,
+ path: SourcePath,
+ ) -> Result<WarpFile<'static>, ContainerError> {
+ let contents = std::fs::read(&path).map_err(|e| ContainerError::FailedIO(e.kind()))?;
+ let mut file = WarpFile::from_owned_bytes(contents).ok_or(
+ ContainerError::CorruptedData("file data failed to validate"),
+ )?;
- // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API.
- // If we did need to load signature files from a project than this would need to change.
- let Some(file) = rfd::FileDialog::new()
- .add_filter("Signature Files", &["sbin"])
- .set_file_name(format!("{}.sbin", view.file().filename()))
- .pick_file()
- else {
- return;
- };
+ let view_target = view
+ .default_platform()
+ .map(|p| platform_to_target(&p))
+ .unwrap_or_default();
+ let file_has_target = file
+ .chunks
+ .iter()
+ .find(|c| c.header.target == view_target)
+ .is_some();
+
+ if !file_has_target {
+ // File does not contain a view target, alert user if they would like to override the file chunks to the view target.
+ let text = format!(
+ "Attempting to load WARP file with no target `{:?}`, continue loading anyways?",
+ &view_target
+ );
+ let res = show_message_box(
+ "Override file target?",
+ &text,
+ MessageBoxButtonSet::YesNoButtonSet,
+ MessageBoxIcon::WarningIcon,
+ );
+ if res != MessageBoxButtonResult::YesButton {
+ return Err(ContainerError::CorruptedData(
+ "User does not want to load file",
+ ));
+ }
+
+ // Take all the chunks and convert them to the target, so we load them.
+ // If we do not do this, the user will be surprised when they get no new matches.
+ for chunk in &mut file.chunks {
+ chunk.header.target = view_target.clone();
+ }
+ }
- let Ok(data) = std::fs::read(&file) else {
- log::error!("Could not read signature file: {:?}", file);
+ Ok(file)
+ }
+
+ pub fn execute(view: Ref<BinaryView>) {
+ let mut form = Form::new("Load Signature File");
+ form.add_field(LoadFileField::field());
+ // let fd_field = FileDataKindField::default();
+ // form.add_field(fd_field.to_field());
+ form.add_field(RunMatcherField::field());
+ if !form.prompt() {
return;
- };
+ }
- let Some(data) = warp::signature::Data::from_bytes(&data) else {
- log::error!("Could not get data from signature file: {:?}", file);
+ let Some(file_path) = LoadFileField::from_form(&form) else {
return;
};
+ // TODO: Decide what to pull using the file data kind.
+ // let _file_data_kind = FileDataKindField::from_form(&form).unwrap_or_default();
+ let rerun_matcher = RunMatcherField::from_form(&form).unwrap_or(false);
+
+ let source_file_path = SourcePath::new(file_path.clone());
- let new_matcher = Matcher::from_data(data);
- log::info!(
- "Loading signature file with {} functions and {} types...",
- new_matcher.functions.len(),
- new_matcher.types.len()
- );
- let platform_id = PlatformID::from(platform.as_ref());
- let matcher_cache = PLAT_MATCHER_CACHE.get_or_init(Default::default);
- match matcher_cache.get_mut(&platform_id) {
- Some(mut matcher) => matcher.extend_with_matcher(new_matcher),
- None => {
- // We still must uphold `from_platform` in case we are running this before the matcher workflow
- // is kicked off. Other-wise we only will have the `new_matcher` data.
- let mut matcher = Matcher::from_platform(platform);
- matcher.extend_with_matcher(new_matcher);
- matcher_cache.insert(platform_id, matcher);
+ let file = match LoadSignatureFile::read_file(&view, source_file_path.clone()) {
+ Ok(file) => file,
+ Err(e) => {
+ log::error!("Failed to read signature file: {}", e);
+ return;
}
+ };
+
+ let container_source = DiskContainerSource::new(source_file_path.clone(), file);
+ log::info!("Loading container source: '{}'", container_source.path);
+ let mut map = HashMap::new();
+ map.insert(source_file_path.to_source_id(), container_source);
+ let container = DiskContainer::new("Loaded signatures".to_string(), map);
+ // TODO: See notes in the matcher about doing this, we really need to load it into an existing container.
+ add_cached_container(container);
+
+ if rerun_matcher {
+ thread::spawn(move || {
+ run_matcher(&view);
+ });
}
}
+}
+
+impl Command for LoadSignatureFile {
+ fn action(&self, view: &BinaryView) {
+ let view = view.to_owned();
+ thread::spawn(move || {
+ LoadSignatureFile::execute(view);
+ });
+ }
fn valid(&self, _view: &BinaryView) -> bool {
true
diff --git a/plugins/warp/src/plugin/project.rs b/plugins/warp/src/plugin/project.rs
new file mode 100644
index 00000000..ea5a5600
--- /dev/null
+++ b/plugins/warp/src/plugin/project.rs
@@ -0,0 +1,132 @@
+use binaryninja::background_task::BackgroundTask;
+use binaryninja::command::ProjectCommand;
+use binaryninja::interaction::{Form, FormInputField};
+use binaryninja::project::Project;
+use binaryninja::rc::Ref;
+use regex::Regex;
+use std::thread;
+use std::time::Instant;
+
+use crate::processor::{
+ new_processing_state_background_thread, FileDataKindField, FileFilterField, WarpFileProcessor,
+};
+use crate::report::{ReportGenerator, ReportKindField};
+
+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());
+ // TODO: Threads (we run the analysis in the background)
+ 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<Regex> {
+ 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 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 mut processor = WarpFileProcessor::new().with_file_data(file_data_kind);
+
+ // 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());
+
+ if let Some(filter) = form.file_filter() {
+ processor = processor.with_file_filter(filter);
+ }
+
+ let start = Instant::now();
+ match processor.process_project(&project) {
+ Ok(warp_file) => {
+ // Print the processor string into the description of the file, so we know how it was generated.
+ let processor_str = format!("{:#?}", &processor);
+
+ // TODO: File name needs to be configurable.
+ if project
+ .create_file(
+ &warp_file.to_bytes(),
+ None,
+ "generated.warp",
+ &processor_str,
+ )
+ .is_err()
+ {
+ log::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.{}", ext);
+ if project
+ .create_file(&generated.into_bytes(), None, &file_name, "Warp file")
+ .is_err()
+ {
+ log::error!("Failed to create project file!");
+ }
+ }
+ }
+ Err(e) => {
+ log::error!("Failed to process project: {}", e);
+ }
+ }
+ log::info!("Processing project files took: {:?}", start.elapsed());
+
+ // 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/plugin/render_layer.rs b/plugins/warp/src/plugin/render_layer.rs
index cdac71f2..d7984303 100644
--- a/plugins/warp/src/plugin/render_layer.rs
+++ b/plugins/warp/src/plugin/render_layer.rs
@@ -1,56 +1,94 @@
-use crate::{is_blacklisted_instruction, is_variant_instruction, relocatable_regions};
+use crate::{
+ is_blacklisted_instruction, is_computed_variant_instruction, is_variant_instruction,
+ relocatable_regions,
+};
use binaryninja::basic_block::BasicBlock;
use binaryninja::disassembly::DisassemblyTextLine;
+use binaryninja::flowgraph::FlowGraph;
use binaryninja::function::{HighlightColor, HighlightStandardColor, NativeBlock};
-use binaryninja::low_level_il::instruction::LowLevelInstructionIndex;
+use binaryninja::low_level_il::LowLevelILRegularFunction;
use binaryninja::render_layer::{register_render_layer, RenderLayer};
-pub struct HighlightRenderLayer {}
+// TODO: Add a render layer to show basic block GUID's?
+// TODO: Add a render layer to show constraints for current function?
+
+pub struct HighlightRenderLayer {
+ blacklist: HighlightColor,
+ variant: HighlightColor,
+ computed_variant: HighlightColor,
+}
impl HighlightRenderLayer {
pub fn register() {
register_render_layer(
"WARP Highlight Layer",
- HighlightRenderLayer {},
+ // TODO: Make the highlight colors configurable.
+ HighlightRenderLayer {
+ blacklist: HighlightColor::StandardHighlightColor {
+ color: HighlightStandardColor::BlackHighlightColor,
+ alpha: 155,
+ },
+ variant: HighlightColor::StandardHighlightColor {
+ color: HighlightStandardColor::RedHighlightColor,
+ alpha: 155,
+ },
+ computed_variant: HighlightColor::StandardHighlightColor {
+ color: HighlightStandardColor::OrangeHighlightColor,
+ alpha: 155,
+ },
+ },
Default::default(),
);
}
+
+ /// Highlights the lines that are variant or blacklisted.
+ pub fn highlight_lines(
+ &self,
+ lifted_il: &LowLevelILRegularFunction,
+ llil: &LowLevelILRegularFunction,
+ lines: &mut [DisassemblyTextLine],
+ ) {
+ let relocatable_regions = relocatable_regions(&lifted_il.function().view());
+ for line in lines {
+ // We use address here instead of index since it's more reliable for other IL's.
+ if let Some(lifted_il_instr) = lifted_il.instruction_at(line.address) {
+ if is_blacklisted_instruction(&lifted_il_instr) {
+ line.highlight = self.blacklist;
+ } else if is_variant_instruction(&relocatable_regions, &lifted_il_instr) {
+ line.highlight = self.variant;
+ }
+ }
+
+ if let Some(llil_instr) = llil.instruction_at(line.address) {
+ if is_computed_variant_instruction(&relocatable_regions, &llil_instr) {
+ line.highlight = self.computed_variant;
+ }
+ }
+ }
+ }
}
impl RenderLayer for HighlightRenderLayer {
- fn apply_to_llil_block(
+ fn apply_to_flow_graph(&self, graph: &mut FlowGraph) {
+ if let (Some(lifted_il), Some(llil)) = (graph.lifted_il(), graph.low_level_il()) {
+ for node in &graph.nodes() {
+ let mut new_lines = node.lines().to_vec();
+ self.highlight_lines(&lifted_il, &llil, &mut new_lines);
+ node.set_lines(new_lines);
+ }
+ }
+ }
+
+ fn apply_to_block(
&self,
block: &BasicBlock<NativeBlock>,
mut lines: Vec<DisassemblyTextLine>,
) -> Vec<DisassemblyTextLine> {
- // Highlight any LLIL instruction that will be masked by WARP.
+ // Highlight any instruction that WARP will mask.
let function = block.function();
- // TODO: We might need to make relocatable regions configurable.
- let relocatable_regions = relocatable_regions(&function.view());
- let Ok(llil) = function.low_level_il() else {
- // Don't even think this is possible but _shrug_.
- return lines;
- };
-
- for line in &mut lines {
- let llil_instr_idx = LowLevelInstructionIndex(line.instruction_index);
- if let Some(llil_instr) = llil.instruction_from_index(llil_instr_idx) {
- if is_blacklisted_instruction(&llil_instr) {
- // We have a blacklisted instruction, highlight it as orange!
- line.highlight = HighlightColor::StandardHighlightColor {
- color: HighlightStandardColor::OrangeHighlightColor,
- alpha: 155,
- };
- } else if is_variant_instruction(&relocatable_regions, &llil_instr) {
- // We have a variant instruction, highlight it as red!
- line.highlight = HighlightColor::StandardHighlightColor {
- color: HighlightStandardColor::RedHighlightColor,
- alpha: 155,
- };
- }
- }
+ if let (Ok(lifted_il), Ok(llil)) = (function.lifted_il(), function.low_level_il()) {
+ self.highlight_lines(&lifted_il, &llil, &mut lines);
}
-
lines
}
}
diff --git a/plugins/warp/src/plugin/settings.rs b/plugins/warp/src/plugin/settings.rs
new file mode 100644
index 00000000..6cd2e0cc
--- /dev/null
+++ b/plugins/warp/src/plugin/settings.rs
@@ -0,0 +1,129 @@
+use binaryninja::settings::Settings as BNSettings;
+use serde_json::json;
+use std::string::ToString;
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct PluginSettings {
+ /// Whether to load bundled WARP files on startup. Turn this off if you want to manually load them.
+ ///
+ /// This is set to [PluginSettings::LOAD_BUNDLED_FILES_DEFAULT] by default.
+ pub load_bundled_files: bool,
+ /// Whether to load user WARP files on startup. Turn this off if you want to manually load them.
+ ///
+ /// This is set to [PluginSettings::LOAD_USER_FILES_DEFAULT] by default.
+ pub load_user_files: bool,
+ /// The WARP server to use.
+ ///
+ /// This is set to [PluginSettings::SERVER_URL_DEFAULT] by default.
+ pub server_url: String,
+ /// The API key to use for the selected WARP server, if not specified, you will be unable to push data and may be rate-limited.
+ ///
+ /// This is set to [PluginSettings::SERVER_API_KEY_DEFAULT] by default.
+ pub server_api_key: String,
+ /// Whether to allow networked WARP requests. Turning this off will not disable local WARP functionality.
+ ///
+ /// This is set to [PluginSettings::ENABLE_SERVER_DEFAULT] by default.
+ pub enable_server: bool,
+}
+
+impl PluginSettings {
+ pub const LOAD_BUNDLED_FILES_DEFAULT: bool = true;
+ pub const LOAD_BUNDLED_FILES_SETTING: &'static str = "analysis.warp.loadBundledFiles";
+ pub const LOAD_USER_FILES_DEFAULT: bool = true;
+ pub const LOAD_USER_FILES_SETTING: &'static str = "analysis.warp.loadUserFiles";
+ pub const SERVER_URL_DEFAULT: &'static str = "https://warp.binary.ninja";
+ pub const SERVER_URL_SETTING: &'static str = "analysis.warp.serverUrl";
+ pub const SERVER_API_KEY_DEFAULT: &'static str = "";
+ pub const SERVER_API_KEY_SETTING: &'static str = "analysis.warp.serverApiKey";
+ pub const ENABLE_SERVER_DEFAULT: bool = true;
+ pub const ENABLE_SERVER_SETTING: &'static str = "network.enableWARP";
+
+ pub fn register(bn_settings: &mut BNSettings) {
+ let load_bundled_files_prop = json!({
+ "title" : "Load Bundled Files",
+ "type" : "boolean",
+ "default" : Self::LOAD_BUNDLED_FILES_DEFAULT,
+ "description" : "Whether to load bundled WARP files on startup. Turn this off if you want to manually load them.",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ });
+ bn_settings.register_setting_json(
+ Self::LOAD_BUNDLED_FILES_SETTING,
+ &load_bundled_files_prop.to_string(),
+ );
+ let load_user_files_prop = json!({
+ "title" : "Load User Files",
+ "type" : "boolean",
+ "default" : Self::LOAD_USER_FILES_DEFAULT,
+ "description" : "Whether to load user WARP files on startup. Turn this off if you want to manually load them.",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ });
+ bn_settings.register_setting_json(
+ Self::LOAD_USER_FILES_SETTING,
+ &load_user_files_prop.to_string(),
+ );
+ let server_url_prop = json!({
+ "title" : "Server URL",
+ "type" : "string",
+ "default" : Self::SERVER_URL_DEFAULT,
+ "description" : "The WARP server to use.",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ });
+ bn_settings.register_setting_json(Self::SERVER_URL_SETTING, &server_url_prop.to_string());
+ let server_api_key_prop = json!({
+ "title" : "Server API Key",
+ "type" : "string",
+ "default" : Self::SERVER_API_KEY_DEFAULT,
+ "description" : "The API key to use for the selected WARP server, if not specified you will be unable to push data, and may be rate limited.",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"],
+ "hidden": true
+ });
+ bn_settings.register_setting_json(
+ Self::SERVER_API_KEY_SETTING,
+ &server_api_key_prop.to_string(),
+ );
+ let server_enabled_prop = json!({
+ "title" : "Enable WARP",
+ "type" : "boolean",
+ "default" : Self::ENABLE_SERVER_DEFAULT,
+ "description" : "Whether or not to allow networked WARP requests. Turning this off will not disable local WARP functionality.",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ });
+ bn_settings.register_setting_json(
+ Self::ENABLE_SERVER_SETTING,
+ &server_enabled_prop.to_string(),
+ );
+ }
+
+ /// Retrieve plugin settings from [`BNSettings`].
+ pub fn from_settings(bn_settings: &BNSettings) -> Self {
+ let mut settings = PluginSettings::default();
+ if bn_settings.contains(Self::LOAD_BUNDLED_FILES_SETTING) {
+ settings.load_bundled_files = bn_settings.get_bool(Self::LOAD_BUNDLED_FILES_SETTING);
+ }
+ if bn_settings.contains(Self::LOAD_USER_FILES_SETTING) {
+ settings.load_user_files = bn_settings.get_bool(Self::LOAD_USER_FILES_SETTING);
+ }
+ if bn_settings.contains(Self::SERVER_URL_SETTING) {
+ settings.server_url = bn_settings.get_string(Self::SERVER_URL_SETTING);
+ }
+ if bn_settings.contains(Self::SERVER_API_KEY_SETTING) {
+ settings.server_url = bn_settings.get_string(Self::SERVER_API_KEY_SETTING);
+ }
+ if bn_settings.contains(Self::ENABLE_SERVER_SETTING) {
+ settings.enable_server = bn_settings.get_bool(Self::ENABLE_SERVER_SETTING);
+ }
+ settings
+ }
+}
+
+impl Default for PluginSettings {
+ fn default() -> Self {
+ Self {
+ load_bundled_files: PluginSettings::LOAD_BUNDLED_FILES_DEFAULT,
+ load_user_files: PluginSettings::LOAD_USER_FILES_DEFAULT,
+ server_url: PluginSettings::SERVER_URL_DEFAULT.to_string(),
+ server_api_key: PluginSettings::SERVER_API_KEY_DEFAULT.to_string(),
+ enable_server: PluginSettings::ENABLE_SERVER_DEFAULT,
+ }
+ }
+}
diff --git a/plugins/warp/src/plugin/types.rs b/plugins/warp/src/plugin/types.rs
deleted file mode 100644
index 41e03cd3..00000000
--- a/plugins/warp/src/plugin/types.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use crate::convert::to_bn_type;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::Command;
-use std::time::Instant;
-
-pub struct LoadTypes;
-
-impl Command for LoadTypes {
- fn action(&self, view: &BinaryView) {
- // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API.
- // If we did need to load signature files from a project than this would need to change.
- let Some(file) = rfd::FileDialog::new()
- .add_filter("Signature Files", &["sbin"])
- .set_file_name(format!("{}.sbin", view.file().filename()))
- .pick_file()
- else {
- return;
- };
-
- let Ok(data) = std::fs::read(&file) else {
- log::error!("Could not read signature file: {:?}", file);
- return;
- };
-
- let Some(data) = warp::signature::Data::from_bytes(&data) else {
- log::error!("Could not get data from signature file: {:?}", file);
- return;
- };
-
- let Some(arch) = view.default_arch() else {
- log::error!("Could not get default architecture");
- return;
- };
-
- let view = view.to_owned();
- std::thread::spawn(move || {
- let background_task = binaryninja::background_task::BackgroundTask::new(
- &format!("Applying {} types...", data.types.len()),
- true,
- );
-
- let start = Instant::now();
- for comp_ty in data.types {
- let ty_id = comp_ty.guid.to_string();
- let ty_name = comp_ty.ty.name.to_owned().unwrap_or_else(|| ty_id.clone());
- view.define_auto_type_with_id(ty_name, &ty_id, &to_bn_type(&arch, &comp_ty.ty));
- }
-
- log::info!("Type application took {:?}", start.elapsed());
- background_task.finish();
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/workflow.rs b/plugins/warp/src/plugin/workflow.rs
index f5763116..487fccfe 100644
--- a/plugins/warp/src/plugin/workflow.rs
+++ b/plugins/warp/src/plugin/workflow.rs
@@ -1,16 +1,40 @@
-use crate::cache::cached_function_guid;
-use crate::matcher::cached_function_matcher;
+use crate::cache::container::for_cached_containers;
+use crate::cache::{
+ cached_function_guid, insert_cached_function_match, try_cached_function_guid,
+ try_cached_function_match,
+};
+use crate::convert::{
+ comment_to_bn_comment, platform_to_target, to_bn_symbol_at_address, to_bn_type,
+};
+use crate::matcher::{Matcher, MatcherSettings};
+use crate::{get_warp_tag_type, relocatable_regions};
use binaryninja::background_task::BackgroundTask;
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
use binaryninja::command::Command;
+use binaryninja::settings::{QueryOptions, Settings};
use binaryninja::workflow::{Activity, AnalysisContext, Workflow};
+use itertools::Itertools;
+use std::collections::HashMap;
use std::time::Instant;
+use warp::signature::function::{Function, FunctionGUID};
+use warp::target::Target;
+
+pub const APPLY_ACTIVITY_NAME: &str = "analysis.warp.apply";
+const APPLY_ACTIVITY_CONFIG: &str = r#"{
+ "name": "analysis.warp.apply",
+ "title" : "WARP Apply Matched",
+ "description": "This analysis step applies WARP info to matched functions...",
+ "eligibility": {
+ "auto": {},
+ "runOnce": false
+ }
+}"#;
pub const MATCHER_ACTIVITY_NAME: &str = "analysis.warp.matcher";
const MATCHER_ACTIVITY_CONFIG: &str = r#"{
"name": "analysis.warp.matcher",
"title" : "WARP Matcher",
- "description": "This analysis step applies WARP info to matched functions...",
+ "description": "This analysis step attempts to find matching WARP functions after the initial analysis is complete...",
"eligibility": {
"auto": {},
"runOnce": true
@@ -24,7 +48,7 @@ const GUID_ACTIVITY_CONFIG: &str = r#"{
"description": "This analysis step generates the GUID for all analyzed functions...",
"eligibility": {
"auto": {},
- "runOnce": true
+ "runOnce": false
}
}"#;
@@ -33,19 +57,8 @@ pub struct RunMatcher;
impl Command for RunMatcher {
fn action(&self, view: &BinaryView) {
let view = view.to_owned();
- // TODO: Check to see if the GUID cache is empty and ask the user if they want to regenerate the guids.
std::thread::spawn(move || {
- let undo_id = view.file().begin_undo_actions(true);
- let background_task = BackgroundTask::new("Matching on functions...", false);
- let start = Instant::now();
- view.functions()
- .iter()
- .for_each(|function| cached_function_matcher(&function));
- log::info!("Function matching took {:?}", start.elapsed());
- background_task.finish();
- view.file().commit_undo_actions(&undo_id);
- // Now we want to trigger re-analysis.
- view.update_analysis();
+ run_matcher(&view);
});
}
@@ -54,44 +67,157 @@ impl Command for RunMatcher {
}
}
+pub fn run_matcher(view: &BinaryView) {
+ // Alert the user if we have no actual regions (one comes from the synthetic section).
+ let regions = relocatable_regions(view);
+ if regions.len() <= 1 {
+ log::warn!(
+ "No relocatable regions found, for best results please define sections for the binary!"
+ );
+ }
+
+ // Then we want to actually find matching functions.
+ let background_task = BackgroundTask::new("Matching on WARP functions...", true);
+ let start = Instant::now();
+
+ // Build matcher
+ let view_settings = Settings::new();
+ let mut query_opts = QueryOptions::new_with_view(view);
+ let matcher_settings = MatcherSettings::from_settings(&view_settings, &mut query_opts);
+ let matcher = Matcher::new(matcher_settings);
+
+ // TODO: Par iter this? Using dashmap
+ let functions_by_target_and_guid: HashMap<(FunctionGUID, Target), Vec<_>> = view
+ .functions()
+ .iter()
+ .filter_map(|f| {
+ let guid = try_cached_function_guid(&f)?;
+ let target = platform_to_target(&f.platform());
+ Some(((guid, target), f.to_owned()))
+ })
+ .into_group_map();
+
+ // TODO: Par iter this? Using dashmap
+ let guids_by_target: HashMap<Target, Vec<FunctionGUID>> = functions_by_target_and_guid
+ .keys()
+ .map(|(guid, target)| (target.clone(), *guid))
+ .into_group_map();
+
+ // TODO: Target gets cloned a lot.
+ // TODO: Containers might both match on the same function. What should we do?
+ for_cached_containers(|container| {
+ if background_task.is_cancelled() {
+ return;
+ }
+
+ for (target, guids) in &guids_by_target {
+ let function_guid_with_sources = container
+ .sources_with_function_guids(target, guids)
+ .unwrap_or_default();
+
+ for (guid, sources) in &function_guid_with_sources {
+ let matched_functions: Vec<Function> = sources
+ .iter()
+ .flat_map(|source| {
+ container
+ .functions_with_guid(target, source, guid)
+ .unwrap_or_default()
+ })
+ .collect();
+
+ let functions = functions_by_target_and_guid
+ .get(&(*guid, target.clone()))
+ .expect("Function guid not found");
+
+ for function in functions {
+ // Match on all the possible functions
+ if let Some(matched_function) =
+ matcher.match_function_from_constraints(function, &matched_functions)
+ {
+ // We were able to find a match, add it to the match cache and then mark the function
+ // as requiring updates; this is so that we know about it in the applier activity.
+ insert_cached_function_match(function, Some(matched_function.clone()));
+ }
+ }
+ }
+ }
+ });
+
+ if background_task.is_cancelled() {
+ log::info!("Matcher was cancelled by user, you may run it again by running the 'Run Matcher' command.");
+ }
+
+ log::info!("Function matching took {:?}", start.elapsed());
+ background_task.finish();
+
+ // Now we want to trigger re-analysis.
+ view.update_analysis();
+}
+
pub fn insert_workflow() {
+ // "Hey look, it's a plier" ~ Josh 2025
+ let apply_activity = |ctx: &AnalysisContext| {
+ let view = ctx.view();
+ let function = ctx.function();
+ if let Some(matched_function) = try_cached_function_match(&function) {
+ view.define_auto_symbol(&to_bn_symbol_at_address(
+ &view,
+ &matched_function.symbol,
+ function.symbol().address(),
+ ));
+ if let Some(func_ty) = &matched_function.ty {
+ function.set_auto_type(&to_bn_type(&function.arch(), func_ty));
+ }
+ // TODO: How to clear the comments? They are just persisted.
+ // TODO: Also they generate an undo action, i hate implicit undo actions so much.
+ for comment in matched_function.comments {
+ let bn_comment = comment_to_bn_comment(&function, comment);
+ function.set_comment_at(bn_comment.addr, &bn_comment.comment);
+ }
+ function.add_tag(
+ &get_warp_tag_type(&view),
+ &matched_function.guid.to_string(),
+ None,
+ false,
+ None,
+ );
+ }
+ };
+
let matcher_activity = |ctx: &AnalysisContext| {
let view = ctx.view();
- let undo_id = view.file().begin_undo_actions(true);
- let background_task = BackgroundTask::new("Matching on functions...", false);
- let start = Instant::now();
- view.functions()
- .iter()
- .for_each(|function| cached_function_matcher(&function));
- log::info!("Function matching took {:?}", start.elapsed());
- background_task.finish();
- view.file().commit_undo_actions(&undo_id);
- // Now we want to trigger re-analysis.
- view.update_analysis();
+ run_matcher(&view);
};
let guid_activity = |ctx: &AnalysisContext| {
let function = ctx.function();
- // TODO: Returning RegularNonSSA means we cant modify the il (the lifting code was written just for lifted il, that needs to be fixed)
- if let Some(llil) = unsafe { ctx.llil_function() } {
- cached_function_guid(&function, &llil);
+ if let Some(lifted_il) = unsafe { ctx.lifted_il_function() } {
+ cached_function_guid(&function, &lifted_il);
}
};
let old_function_meta_workflow = Workflow::instance("core.function.metaAnalysis");
let function_meta_workflow = old_function_meta_workflow.clone_to("core.function.metaAnalysis");
let guid_activity = Activity::new_with_action(GUID_ACTIVITY_CONFIG, guid_activity);
+ let apply_activity = Activity::new_with_action(APPLY_ACTIVITY_CONFIG, apply_activity);
function_meta_workflow
.register_activity(&guid_activity)
.unwrap();
+ // Because we are going to impact analysis with application we must make sure the function update is triggered to continue to update analysis.
+ // TODO: need to ask why i cant do core.function.update like in the rtti plugin.
+ function_meta_workflow
+ .register_activity_with_subactivities::<Vec<String>>(&apply_activity, vec![])
+ .unwrap();
function_meta_workflow.insert("core.function.runFunctionRecognizers", [GUID_ACTIVITY_NAME]);
+ function_meta_workflow.insert("core.function.generateMediumLevelIL", [APPLY_ACTIVITY_NAME]);
function_meta_workflow.register().unwrap();
let old_module_meta_workflow = Workflow::instance("core.module.metaAnalysis");
let module_meta_workflow = old_module_meta_workflow.clone_to("core.module.metaAnalysis");
let matcher_activity = Activity::new_with_action(MATCHER_ACTIVITY_CONFIG, matcher_activity);
+ // Matcher activity must have core.module.update as subactivity otherwise analysis will sometimes never retrigger.
module_meta_workflow
- .register_activity(&matcher_activity)
+ .register_activity_with_subactivities(&matcher_activity, vec!["core.module.update"])
.unwrap();
module_meta_workflow.insert(
"core.module.deleteUnusedAutoFunctions",
diff --git a/plugins/warp/src/processor.rs b/plugins/warp/src/processor.rs
new file mode 100644
index 00000000..4b973753
--- /dev/null
+++ b/plugins/warp/src/processor.rs
@@ -0,0 +1,829 @@
+use std::collections::{HashMap, HashSet};
+use std::fs::File;
+use std::path::{Path, PathBuf};
+use std::sync::atomic::Ordering::Relaxed;
+use std::sync::atomic::{AtomicBool, AtomicUsize};
+use std::sync::Arc;
+use std::time::{Duration, Instant};
+
+use ar::Archive;
+use dashmap::DashMap;
+use rayon::iter::IntoParallelIterator;
+use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
+use regex::Regex;
+use serde_json::{json, Value};
+use tempdir::TempDir;
+use thiserror::Error;
+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 warp::chunk::{Chunk, ChunkKind, CompressionType};
+use warp::r#type::chunk::TypeChunk;
+use warp::signature::chunk::SignatureChunk;
+use warp::signature::function::Function;
+use warp::target::Target;
+use warp::{WarpFile, WarpFileHeader};
+
+use crate::cache::cached_type_references;
+use crate::convert::platform_to_target;
+use crate::{build_function, INCLUDE_TAG_ICON, INCLUDE_TAG_NAME};
+
+#[derive(Error, Debug)]
+pub enum ProcessingError {
+ #[error("Failed to open archive: {0}")]
+ ArchiveOpen(std::io::Error),
+
+ #[error("Failed to read archive entry: {0}")]
+ ArchiveRead(std::io::Error),
+
+ #[error("Binary view load error: {0}")]
+ BinaryViewLoad(PathBuf),
+
+ #[error("Existing data load error: {0}")]
+ ExistingDataLoad(PathBuf),
+
+ #[error("Temporary directory creation failed: {0}")]
+ TempDirCreation(std::io::Error),
+
+ #[error("Failed to read file: {0}")]
+ FileRead(std::io::Error),
+
+ #[error("Failed to create chunk, possibly too large")]
+ ChunkCreationFailed,
+
+ #[error("Failed to retrieve path to project file: {0:?}")]
+ NoPathToProjectFile(Ref<ProjectFile>),
+
+ #[error("Processing state has been poisoned")]
+ StatePoisoned,
+
+ #[error("Processing has been cancelled")]
+ Cancelled,
+}
+
+#[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<Regex> {
+ 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))
+ };
+
+ Regex::new(&pattern).ok()
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Default)]
+pub enum FileDataKindField {
+ Symbols,
+ Signatures,
+ Types,
+ #[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,
+ }
+ }
+}
+
+#[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<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),
+ }
+ }
+}
+
+#[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>,
+) {
+ std::thread::spawn(move || {
+ let start = Instant::now();
+ while !task.is_finished() {
+ std::thread::sleep(Duration::from_millis(100));
+ // Check if the user wants to cancel the processing.
+ if task.is_cancelled() {
+ state.cancel();
+ }
+
+ let total = state.total_files();
+ let processed = state.files_with_state(ProcessingFileState::Processed);
+ let unprocessed = state.files_with_state(ProcessingFileState::Unprocessed);
+ let analyzing = state.files_with_state(ProcessingFileState::Analyzing);
+ let processing = state.files_with_state(ProcessingFileState::Processing);
+ let completion = (processed as f64 / total as f64) * 100.0;
+ let elapsed = start.elapsed().as_secs_f32();
+ let text = format!(
+ "Processing {} files... {{{}|{}|{}|{}}} ({:.2}%) [{:.2}s]",
+ total, unprocessed, analyzing, processing, processed, completion, elapsed
+ );
+ task.set_progress_text(&text);
+ }
+ });
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum ProcessingFileState {
+ /// File is yet to be processed.
+ Unprocessed,
+ /// File is being analyzed by Binary Ninja.
+ Analyzing,
+ /// File is currently generating WARP data.
+ /// TODO: (AtomicUsize) for the total and done functions, we can then write to it with Relaxed when processing.
+ Processing,
+ /// File is done being processed.
+ Processed,
+}
+
+#[derive(Debug, Default)]
+pub struct ProcessingState {
+ pub cancelled: AtomicBool,
+ pub files: DashMap<PathBuf, ProcessingFileState>,
+ pub total_functions: AtomicUsize,
+}
+
+impl ProcessingState {
+ pub fn is_cancelled(&self) -> bool {
+ self.cancelled.load(Relaxed)
+ }
+
+ pub fn cancel(&self) {
+ self.cancelled.store(true, Relaxed)
+ }
+
+ pub fn increment_functions(&self) {
+ self.total_functions.fetch_add(1, Relaxed);
+ }
+
+ pub fn total_files(&self) -> usize {
+ self.files.len()
+ }
+
+ pub fn files_with_state(&self, state: ProcessingFileState) -> usize {
+ self.files.iter().filter(|f| *f.value() == state).count()
+ }
+
+ pub fn set_file_state(&self, path: PathBuf, state: ProcessingFileState) {
+ self.files.insert(path, state);
+ }
+}
+
+/// Create a new [`WarpFile`] from files, projects, and directories.
+#[derive(Debug, Clone)]
+pub struct WarpFileProcessor {
+ /// The Binary Ninja settings to use when analyzing the binaries.
+ analysis_settings: Value,
+ /// For any function without an LLIL, request analysis to be run, waiting for analysis to
+ /// complete to include in the analysis.
+ request_analysis: bool,
+ // TODO: Project cache path, so we save to a project instead of some temp path.
+ // 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,
+ included_functions: IncludedFunctionsField,
+ compression_type: CompressionTypeField,
+ /// Regex pattern used to filter out files.
+ file_filter: Option<Regex>,
+ /// 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>,
+}
+
+impl WarpFileProcessor {
+ pub fn new() -> Self {
+ Self {
+ analysis_settings: json!({
+ "analysis.linearSweep.autorun": false,
+ "analysis.signatureMatcher.autorun": false,
+ "analysis.mode": "full",
+ // Disable warp when opening views.
+ "analysis.warp.guid": false,
+ "analysis.warp.matcher": false,
+ "analysis.warp.apply": false,
+ }),
+ request_analysis: true,
+ cache_path: None,
+ file_data: Default::default(),
+ included_functions: Default::default(),
+ compression_type: Default::default(),
+ file_filter: None,
+ state: Arc::new(ProcessingState::default()),
+ }
+ }
+
+ /// Retrieve a thread-safe shared reference to the [`ProcessingState`].
+ pub fn state(&self) -> Arc<ProcessingState> {
+ self.state.clone()
+ }
+
+ pub fn with_analysis_settings(mut self, analysis_settings: Value) -> Self {
+ self.analysis_settings = analysis_settings;
+ self
+ }
+
+ pub fn with_request_analysis(mut self, request_analysis: bool) -> Self {
+ self.request_analysis = request_analysis;
+ self
+ }
+
+ pub fn with_cache_path(mut self, cache_path: PathBuf) -> Self {
+ self.cache_path = Some(cache_path);
+ self
+ }
+
+ pub fn with_file_data(mut self, file_data: FileDataKindField) -> Self {
+ self.file_data = file_data;
+ self
+ }
+
+ pub fn with_included_functions(mut self, included_functions: IncludedFunctionsField) -> Self {
+ self.included_functions = included_functions;
+ self
+ }
+
+ pub fn with_compression_type(mut self, compression_type: CompressionTypeField) -> Self {
+ self.compression_type = compression_type;
+ self
+ }
+
+ pub fn with_file_filter(mut self, file_filter: Regex) -> Self {
+ self.file_filter = Some(file_filter);
+ 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,
+ }
+ }
+
+ /// Place a call to this in places to interrupt when canceled.
+ fn check_cancelled(&self) -> Result<(), ProcessingError> {
+ match self.state.is_cancelled() {
+ true => Err(ProcessingError::Cancelled),
+ false => Ok(()),
+ }
+ }
+
+ pub fn process(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> {
+ match path.extension() {
+ Some(ext) if ext == "a" || ext == "lib" || ext == "rlib" => self.process_archive(path),
+ Some(ext) if ext == "warp" => self.process_warp_file(path),
+ _ if path.is_dir() => self.process_directory(&path),
+ // TODO: process_database?
+ _ => self.process_file(path),
+ }
+ }
+
+ 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();
+
+ // 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.
+ if let Some(path) = project_file.path_on_disk() {
+ self.state
+ .set_file_state(path, ProcessingFileState::Unprocessed);
+ }
+ }
+
+ let unmerged_files: Result<Vec<_>, _> = files
+ .par_iter()
+ .map(|file| {
+ self.check_cancelled()?;
+ self.process_project_file(file)
+ })
+ .filter_map(|res| match res {
+ Ok(result) => Some(Ok(result)),
+ Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)),
+ Err(e) => {
+ log::error!("Project file processing error: {:?}", e);
+ None
+ }
+ })
+ .collect();
+
+ let unmerged_chunks: Vec<_> = unmerged_files?
+ .iter()
+ .flat_map(|f| f.chunks.clone())
+ .collect();
+ let merged_chunks = Chunk::merge(&unmerged_chunks, self.compression_type.into());
+ Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks))
+ }
+
+ pub fn process_project_file(
+ &self,
+ project_file: &ProjectFile,
+ ) -> Result<WarpFile<'static>, ProcessingError> {
+ let file_name = project_file.name();
+ let extension = file_name.split('.').last();
+ let path = project_file
+ .path_on_disk()
+ .ok_or_else(|| ProcessingError::NoPathToProjectFile(project_file.to_owned()))?;
+ match extension {
+ Some(ext) if ext == "a" || ext == "lib" || ext == "rlib" => self.process_archive(path),
+ Some("warp") => self.process_warp_file(path),
+ _ => self.process_file(path),
+ }
+ }
+
+ pub fn process_warp_file(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> {
+ let contents = std::fs::read(&path).map_err(ProcessingError::FileRead)?;
+ let file = WarpFile::from_owned_bytes(contents)
+ .ok_or(ProcessingError::ExistingDataLoad(path.clone()));
+
+ // Inform the state of the new processed warp file.
+ self.state
+ .set_file_state(path, ProcessingFileState::Processed);
+
+ file
+ }
+
+ pub fn process_file(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> {
+ // Inform the state of the new analyzing file.
+ self.state
+ .set_file_state(path.clone(), ProcessingFileState::Analyzing);
+
+ // Load the view, either from the cache or from the given path.
+ // Using the cache can speed up the processing, especially for larger binaries.
+ let settings_str = self.analysis_settings.to_string();
+ let view = match &self.cache_path {
+ Some(cache_path) => {
+ // Processor is caching analysis, try and find our file in the cache.
+ let file_cache_path = cache_path
+ .join(path.file_name().unwrap())
+ .with_extension("bndb");
+ if file_cache_path.exists() {
+ // TODO: Update analysis and wait option
+ log::debug!("Analysis database found in cache: {:?}", file_cache_path);
+ binaryninja::load_with_options(&file_cache_path, true, Some(settings_str))
+ } else {
+ log::debug!("No database found in cache: {:?}", file_cache_path);
+ binaryninja::load_with_options(&path, true, Some(settings_str))
+ }
+ }
+ None => {
+ // Processor is not caching analysis
+ binaryninja::load_with_options(&path, true, Some(settings_str))
+ }
+ }
+ .ok_or(ProcessingError::BinaryViewLoad(path.clone()))?;
+
+ // Analysis is complete, if needed, save the database to cache.
+ if let Some(cache_path) = &self.cache_path {
+ // Before we process the view we should cache the analysis database.
+ // Only cache the analysis database if there has been a change.
+ // TODO: What if there is multiple paths with the same name?
+ // TODO: We need more context than just the path, likely we need a processing path stack.
+ let file_cache_path = cache_path
+ .join(path.file_name().unwrap())
+ .with_extension("bndb");
+ // TODO: We should also update the cache if analysis has changed!
+ if !view.file().is_database_backed() {
+ // Update the cache.
+ log::debug!("Saving analysis database to {:?}", file_cache_path);
+ if !view.file().create_database(&file_cache_path) {
+ // TODO: We might want to error here...
+ log::warn!("Failed to save analysis database to {:?}", file_cache_path);
+ }
+ } else {
+ log::debug!(
+ "Analysis database unchanged, skipping save to {:?}",
+ file_cache_path
+ );
+ }
+ }
+
+ // Process the view
+ let warp_file = self.process_view(path, &view);
+ // Close the view manually, see comment in [`BinaryView`].
+ view.file().close();
+ warp_file
+ }
+
+ pub fn process_directory(&self, path: &Path) -> Result<WarpFile<'static>, ProcessingError> {
+ // Collect all files in the directory
+ let files = WalkDir::new(path)
+ .into_iter()
+ .filter_map(|e| {
+ let path = e.ok()?.into_path();
+ if path.is_file() && self.file_filter(&path) {
+ Some(path)
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+
+ // Inform the state of the new unprocessed files.
+ for entry_file in &files {
+ self.state
+ .set_file_state(entry_file.clone(), ProcessingFileState::Unprocessed);
+ }
+
+ // Process all the files.
+ let unmerged_files: Result<Vec<_>, _> = files
+ .into_par_iter()
+ .inspect(|path| log::debug!("Processing file: {:?}", path))
+ .map(|path| {
+ self.check_cancelled()?;
+ self.process(path)
+ })
+ .filter_map(|res| match res {
+ Ok(result) => Some(Ok(result)),
+ Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)),
+ Err(e) => {
+ log::error!("Directory file processing error: {:?}", e);
+ None
+ }
+ })
+ .collect();
+
+ let unmerged_chunks: Vec<_> = unmerged_files?
+ .iter()
+ .flat_map(|f| f.chunks.clone())
+ .collect();
+ let merged_chunks = Chunk::merge(&unmerged_chunks, self.compression_type.into());
+ Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks))
+ }
+
+ pub fn process_archive(&self, path: PathBuf) -> Result<WarpFile<'static>, ProcessingError> {
+ // Open the archive.
+ let archive_file = File::open(&path).map_err(ProcessingError::ArchiveOpen)?;
+ let mut archive = Archive::new(archive_file);
+
+ // Create a temp directory to store the archive entries.
+ let temp_dir = TempDir::new("tmp_archive").map_err(ProcessingError::TempDirCreation)?;
+
+ // TODO: Use the file_filter? We would need to normalize the path then.
+ // Iterate through the entries in the ar file and make a temp dir with them
+ let mut entry_files: HashSet<PathBuf> = HashSet::new();
+ while let Some(entry) = archive.next_entry() {
+ let mut entry = entry.map_err(ProcessingError::ArchiveRead)?;
+ // NOTE: The entry name here may resemble a full path, on unix this is fine, but
+ // on Windows this will prevent a file from being created, so we "normalize" the file name.
+ let name = String::from_utf8_lossy(entry.header().identifier()).to_string();
+ // Normalize file name for Windows compatibility
+ let normalized_name = name
+ .replace(':', "_")
+ .replace('/', "_")
+ .replace('\\', "_")
+ .split_whitespace()
+ .collect::<Vec<_>>()
+ .join("_");
+ let output_path = temp_dir.path().join(&normalized_name);
+ if !entry_files.contains(&output_path) {
+ let mut output_file =
+ File::create(&output_path).map_err(ProcessingError::TempDirCreation)?;
+ std::io::copy(&mut entry, &mut output_file).map_err(ProcessingError::FileRead)?;
+ entry_files.insert(output_path);
+ } else {
+ log::debug!("Skipping already inserted entry: {}", normalized_name);
+ }
+ }
+
+ // Inform the state of the new unprocessed files.
+ for entry_file in &entry_files {
+ self.state
+ .set_file_state(entry_file.clone(), ProcessingFileState::Unprocessed);
+ }
+
+ // TODO: Par iter?
+ // Process all the entries.
+ let unmerged_files: Result<Vec<_>, _> = entry_files
+ .into_par_iter()
+ .inspect(|path| log::debug!("Processing entry: {:?}", path))
+ .map(|path| {
+ self.check_cancelled()?;
+ self.process_file(path)
+ })
+ .filter_map(|res| match res {
+ Ok(result) => Some(Ok(result)),
+ Err(ProcessingError::Cancelled) => Some(Err(ProcessingError::Cancelled)),
+ Err(e) => {
+ log::error!("Archive file processing error: {:?}", e);
+ None
+ }
+ })
+ .collect();
+
+ let unmerged_chunks: Vec<_> = unmerged_files?
+ .iter()
+ .flat_map(|f| f.chunks.clone())
+ .collect();
+ let merged_chunks = Chunk::merge(&unmerged_chunks, self.compression_type.into());
+ Ok(WarpFile::new(WarpFileHeader::new(), merged_chunks))
+ }
+
+ pub fn process_view(
+ &self,
+ path: PathBuf,
+ view: &BinaryView,
+ ) -> Result<WarpFile<'static>, ProcessingError> {
+ self.state
+ .set_file_state(path.clone(), ProcessingFileState::Processing);
+
+ let mut chunks = Vec::new();
+ if self.file_data != FileDataKindField::Types {
+ let mut signature_chunks = self.create_signature_chunks(view)?;
+ for (target, signature_chunk) in signature_chunks.drain() {
+ let chunk = Chunk::new_with_target(
+ ChunkKind::Signature(signature_chunk),
+ self.compression_type.into(),
+ target,
+ );
+ chunks.push(chunk)
+ }
+ }
+
+ if self.file_data != FileDataKindField::Signatures {
+ chunks.push(Chunk::new(
+ ChunkKind::Type(self.create_type_chunk(view)?),
+ self.compression_type.into(),
+ ));
+ }
+
+ self.state
+ .set_file_state(path, ProcessingFileState::Processed);
+
+ Ok(WarpFile::new(WarpFileHeader::new(), chunks))
+ }
+
+ /// Create signature chunks for each unique [`Target`].
+ ///
+ /// A [`Target`] in Binary Ninja is a [`Platform`], so we just fill in that information.
+ pub fn create_signature_chunks(
+ &self,
+ view: &BinaryView,
+ ) -> Result<HashMap<Target, SignatureChunk<'static>>, ProcessingError> {
+ let is_function_named = |f: &Guard<BNFunction>| {
+ self.included_functions == IncludedFunctionsField::All
+ || view.symbol_by_address(f.start()).is_some()
+ || f.has_user_annotations()
+ };
+ let is_function_tagged = |f: &Guard<BNFunction>| {
+ self.included_functions != IncludedFunctionsField::Selected
+ || !f.function_tags(None, Some(INCLUDE_TAG_NAME)).is_empty()
+ };
+ // TODO: is_function_blacklisted (use tag)
+
+ // TODO: Move this background task to use the ProcessingState.
+ let view_functions = view.functions();
+ let total_functions = view_functions.len();
+ let done_functions = AtomicUsize::default();
+ let background_task = BackgroundTask::new(
+ &format!("Generating signatures... ({}/{})", 0, total_functions),
+ true,
+ );
+
+ // Create all of the "built" functions, for the chunk.
+ // NOTE: This does a bit of filtering to remove undesired functions, look at this if
+ // a desired function is not in the created chunk.
+ // TODO: Make this interruptable. with background_task.is_cancelled.
+ let start = Instant::now();
+ let built_functions: DashMap<Target, Vec<Function>> = view_functions
+ .par_iter()
+ .inspect(|_| {
+ done_functions.fetch_add(1, Relaxed);
+ background_task.set_progress_text(&format!(
+ "Generating signatures... ({}/{}) [{}s]",
+ done_functions.load(Relaxed),
+ total_functions,
+ start.elapsed().as_secs_f32()
+ ))
+ })
+ .filter(is_function_tagged)
+ .filter(is_function_named)
+ .filter(|f| !f.analysis_skipped())
+ .filter_map(|func| {
+ let lifted_il = func.lifted_il().ok()?;
+ let target = platform_to_target(&func.platform());
+ let mut built_function = build_function(&func, &lifted_il);
+ // User asked to only save symbols, so we will remove the function type.
+ if self.file_data == FileDataKindField::Symbols {
+ built_function.ty = None;
+ }
+ Some((target, built_function))
+ })
+ .fold(
+ DashMap::new,
+ |acc: DashMap<Target, Vec<Function>>, (target, function)| {
+ acc.entry(target).or_default().push(function);
+ acc
+ },
+ )
+ .reduce(DashMap::new, |acc, other| {
+ other.into_iter().for_each(|(key, value)| {
+ acc.entry(key).or_default().extend(value);
+ });
+ acc
+ });
+
+ let chunks: Result<HashMap<Target, SignatureChunk<'static>>, ProcessingError> =
+ built_functions
+ .into_iter()
+ .map(|(target, functions)| {
+ Ok((
+ target,
+ SignatureChunk::new(&functions)
+ .ok_or(ProcessingError::ChunkCreationFailed)?,
+ ))
+ })
+ .collect();
+
+ background_task.finish();
+ chunks
+ }
+
+ // TODO: Add a background task here.
+ pub fn create_type_chunk(
+ &self,
+ view: &BinaryView,
+ ) -> Result<TypeChunk<'static>, ProcessingError> {
+ let mut referenced_types = Vec::new();
+ if let Some(ref_ty_cache) = cached_type_references(view) {
+ referenced_types = ref_ty_cache
+ .cache
+ .iter()
+ .filter_map(|t| t.to_owned())
+ .collect::<Vec<_>>();
+ }
+ TypeChunk::new_with_computed(&referenced_types).ok_or(ProcessingError::ChunkCreationFailed)
+ }
+}
+
+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
new file mode 100644
index 00000000..8ba9cc8b
--- /dev/null
+++ b/plugins/warp/src/report.rs
@@ -0,0 +1,185 @@
+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/snapshots/warp_ninja__tests__snapshot__ctype.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__ctype.snap
deleted file mode 100644
index 0cda5d3a..00000000
--- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__ctype.snap
+++ /dev/null
@@ -1,126 +0,0 @@
----
-source: plugins/warp/src/lib.rs
-expression: functions
----
-[
- FunctionGUID {
- guid: 00c71b63-3039-5aa3-94f7-1737530239d9,
- },
- FunctionGUID {
- guid: 06ff982d-b167-5dce-9689-71a631820da6,
- },
- FunctionGUID {
- guid: 0d6d3685-7a8a-5d1c-8b7a-fe14871218ff,
- },
- FunctionGUID {
- guid: 0df54a49-5267-52f7-9665-70a8e66ad0dd,
- },
- FunctionGUID {
- guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18,
- },
- FunctionGUID {
- guid: 15d686e8-3e80-5632-9ce7-17c3510f8238,
- },
- FunctionGUID {
- guid: 18cfce71-47bc-5595-89cb-06e0563b211d,
- },
- FunctionGUID {
- guid: 1b6aa5a3-ac7f-542d-a0ab-bae8f142d8d8,
- },
- FunctionGUID {
- guid: 1e794537-6289-59e7-bef9-0c72f3989db8,
- },
- FunctionGUID {
- guid: 29690354-fa27-54d1-a8be-18535b19b1c3,
- },
- FunctionGUID {
- guid: 33e6bc5f-eeb6-5a07-810e-f04a2dba36cf,
- },
- FunctionGUID {
- guid: 3b992cd7-1721-5f30-b59c-84ea682be808,
- },
- FunctionGUID {
- guid: 3ba4d0cb-9c07-5904-aafe-34e051b89be5,
- },
- FunctionGUID {
- guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904,
- },
- FunctionGUID {
- guid: 43a8c54b-dd4f-5334-ae00-218cb758ad4c,
- },
- FunctionGUID {
- guid: 53b484f3-751a-505d-beae-acc9d69c261d,
- },
- FunctionGUID {
- guid: 5b7cc78f-3b2f-5b82-9726-37c26ad087b4,
- },
- FunctionGUID {
- guid: 5b7f41e9-de1e-558c-ac7d-888acca3a76c,
- },
- FunctionGUID {
- guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9,
- },
- FunctionGUID {
- guid: 6db530d1-8ea4-568e-a089-c61164851f04,
- },
- FunctionGUID {
- guid: 7cae2466-6b19-5fff-869e-9a9717043df4,
- },
- FunctionGUID {
- guid: 7cae2466-6b19-5fff-869e-9a9717043df4,
- },
- FunctionGUID {
- guid: 7f0e055b-1e83-5f7b-a1fd-6a0a3d0b74f5,
- },
- FunctionGUID {
- guid: 88d9b26b-3884-58d6-b164-435d8d888e5c,
- },
- FunctionGUID {
- guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd,
- },
- FunctionGUID {
- guid: 8ff1fc25-6912-5772-8fa3-19e1b996aea7,
- },
- FunctionGUID {
- guid: 951d5c60-d457-5b62-98e7-ade4a37d7cbe,
- },
- FunctionGUID {
- guid: 95bf9d56-3b11-515f-960f-07abf69aed95,
- },
- FunctionGUID {
- guid: 9e30f7d4-0a31-50cf-93f8-490a9e8c300b,
- },
- FunctionGUID {
- guid: a7acb567-eb98-53e4-a843-5c60a8c59f19,
- },
- FunctionGUID {
- guid: ae6f9d26-872c-5b6e-925e-40c0ec95c702,
- },
- FunctionGUID {
- guid: b8b2746b-20aa-5ad2-bcfd-ab5448d952eb,
- },
- FunctionGUID {
- guid: bc4d18a4-96d5-5322-ba99-fcee83648701,
- },
- FunctionGUID {
- guid: c7bd1444-8f5c-5bc6-87c3-48f010b00455,
- },
- FunctionGUID {
- guid: cacf9fe4-7518-5b22-ae3a-ab775dea4c9a,
- },
- FunctionGUID {
- guid: cdb3f650-974f-5686-b81f-10b6544680f4,
- },
- FunctionGUID {
- guid: d344f254-4903-581d-ad93-713c9ff2be2e,
- },
- FunctionGUID {
- guid: d5456209-db22-53a1-906d-1ecb3254ab2d,
- },
- FunctionGUID {
- guid: ec40932b-8293-5ffb-9d31-04c282b6530d,
- },
- FunctionGUID {
- guid: faedbe75-8451-525b-83da-7a57ec96166e,
- },
-]
diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__fptostr.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__fptostr.snap
deleted file mode 100644
index 5628853f..00000000
--- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__fptostr.snap
+++ /dev/null
@@ -1,9 +0,0 @@
----
-source: plugins/warp/src/lib.rs
-expression: functions
----
-[
- FunctionGUID {
- guid: 9d4dba66-7106-5215-8e64-a47279f67dfe,
- },
-]
diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__mbslen.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__mbslen.snap
deleted file mode 100644
index 01ca9ec5..00000000
--- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__mbslen.snap
+++ /dev/null
@@ -1,39 +0,0 @@
----
-source: plugins/warp/src/lib.rs
-expression: functions
----
-[
- FunctionGUID {
- guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18,
- },
- FunctionGUID {
- guid: 1e794537-6289-59e7-bef9-0c72f3989db8,
- },
- FunctionGUID {
- guid: 29690354-fa27-54d1-a8be-18535b19b1c3,
- },
- FunctionGUID {
- guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904,
- },
- FunctionGUID {
- guid: 6a7eb6dd-be3e-50dd-ae91-cea8f3eb4f06,
- },
- FunctionGUID {
- guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9,
- },
- FunctionGUID {
- guid: 6b0d368e-83d9-55fc-9683-5812e651a49b,
- },
- FunctionGUID {
- guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd,
- },
- FunctionGUID {
- guid: bfaf2f0a-d92d-5ef7-879d-86eb0ca01257,
- },
- FunctionGUID {
- guid: f034945d-32e2-5d86-8a86-3761cc54f419,
- },
- FunctionGUID {
- guid: f552d8ee-3064-50ed-af18-4e60f095c9ea,
- },
-]
diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__memicmp.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__memicmp.snap
deleted file mode 100644
index 8f83530e..00000000
--- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__memicmp.snap
+++ /dev/null
@@ -1,9 +0,0 @@
----
-source: plugins/warp/src/lib.rs
-expression: functions
----
-[
- FunctionGUID {
- guid: cf6e80f2-69aa-5757-a06f-fb2e4866ab14,
- },
-]
diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__strnicm.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__strnicm.snap
deleted file mode 100644
index 9d661016..00000000
--- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__strnicm.snap
+++ /dev/null
@@ -1,9 +0,0 @@
----
-source: plugins/warp/src/lib.rs
-expression: functions
----
-[
- FunctionGUID {
- guid: 7c350a2d-2282-5625-943e-009f317b8d2c,
- },
-]
diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__wctype.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__wctype.snap
deleted file mode 100644
index 2d9f4e73..00000000
--- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot__wctype.snap
+++ /dev/null
@@ -1,117 +0,0 @@
----
-source: plugins/warp/src/lib.rs
-expression: functions
----
-[
- FunctionGUID {
- guid: 0a9027e0-fc87-5f83-a9da-ea7b1eef0761,
- },
- FunctionGUID {
- guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18,
- },
- FunctionGUID {
- guid: 1b434f0e-26b8-539b-9679-33d96a683190,
- },
- FunctionGUID {
- guid: 1b434f0e-26b8-539b-9679-33d96a683190,
- },
- FunctionGUID {
- guid: 1e34d278-fd31-5cdf-98dc-395cb29419a7,
- },
- FunctionGUID {
- guid: 1e34d278-fd31-5cdf-98dc-395cb29419a7,
- },
- FunctionGUID {
- guid: 1e794537-6289-59e7-bef9-0c72f3989db8,
- },
- FunctionGUID {
- guid: 23ba7382-bd48-5d47-a1f0-79ad5db87730,
- },
- FunctionGUID {
- guid: 23ba7382-bd48-5d47-a1f0-79ad5db87730,
- },
- FunctionGUID {
- guid: 29690354-fa27-54d1-a8be-18535b19b1c3,
- },
- FunctionGUID {
- guid: 33ce8a60-e3fa-5941-9ca6-09707bb1f579,
- },
- FunctionGUID {
- guid: 33ce8a60-e3fa-5941-9ca6-09707bb1f579,
- },
- FunctionGUID {
- guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904,
- },
- FunctionGUID {
- guid: 607a10c5-8943-5dec-b888-f5210f08b311,
- },
- FunctionGUID {
- guid: 607a10c5-8943-5dec-b888-f5210f08b311,
- },
- FunctionGUID {
- guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9,
- },
- FunctionGUID {
- guid: 6cebba56-9929-5f3d-9dd3-fc56f8e84593,
- },
- FunctionGUID {
- guid: 75b0aba8-582a-5085-abbb-cebf55d2498d,
- },
- FunctionGUID {
- guid: 75b0aba8-582a-5085-abbb-cebf55d2498d,
- },
- FunctionGUID {
- guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd,
- },
- FunctionGUID {
- guid: 90cb6aa0-f402-5874-ae2d-e10752ad6462,
- },
- FunctionGUID {
- guid: 90cb6aa0-f402-5874-ae2d-e10752ad6462,
- },
- FunctionGUID {
- guid: 9dd8ec46-e680-5fbe-b28b-30505b505fa3,
- },
- FunctionGUID {
- guid: 9dd8ec46-e680-5fbe-b28b-30505b505fa3,
- },
- FunctionGUID {
- guid: b08dcc9b-b766-56e0-a9af-1d24cc6c6574,
- },
- FunctionGUID {
- guid: b08dcc9b-b766-56e0-a9af-1d24cc6c6574,
- },
- FunctionGUID {
- guid: b098f8a1-ecdd-5938-8da3-bb37935c6b7e,
- },
- FunctionGUID {
- guid: b2339a04-7dcf-5791-b74e-806220d37f56,
- },
- FunctionGUID {
- guid: b2339a04-7dcf-5791-b74e-806220d37f56,
- },
- FunctionGUID {
- guid: b3723a76-a709-510c-9d93-97dc42b163a3,
- },
- FunctionGUID {
- guid: b3723a76-a709-510c-9d93-97dc42b163a3,
- },
- FunctionGUID {
- guid: cc259c7e-0ada-5680-8ad7-b077d60bd2a1,
- },
- FunctionGUID {
- guid: cc259c7e-0ada-5680-8ad7-b077d60bd2a1,
- },
- FunctionGUID {
- guid: eed08c42-3616-5c27-82ad-719db5cce36e,
- },
- FunctionGUID {
- guid: eed08c42-3616-5c27-82ad-719db5cce36e,
- },
- FunctionGUID {
- guid: f6535880-21d4-5272-b2c0-5e63cbc208e7,
- },
- FunctionGUID {
- guid: f6535880-21d4-5272-b2c0-5e63cbc208e7,
- },
-]
diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atof.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atof.snap
deleted file mode 100644
index 4ff8251c..00000000
--- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atof.snap
+++ /dev/null
@@ -1,390 +0,0 @@
----
-source: plugins/warp/src/lib.rs
-expression: functions
----
-[
- FunctionGUID {
- guid: 02d864ef-80bc-5265-b78f-0e42815a4d1d,
- },
- FunctionGUID {
- guid: 02d864ef-80bc-5265-b78f-0e42815a4d1d,
- },
- FunctionGUID {
- guid: 06a9339a-7249-50f3-9c3d-131d3248d560,
- },
- FunctionGUID {
- guid: 06a9339a-7249-50f3-9c3d-131d3248d560,
- },
- FunctionGUID {
- guid: 06a9339a-7249-50f3-9c3d-131d3248d560,
- },
- FunctionGUID {
- guid: 06a9339a-7249-50f3-9c3d-131d3248d560,
- },
- FunctionGUID {
- guid: 07c4bac9-cf7d-5e00-833d-d23ba44c07d9,
- },
- FunctionGUID {
- guid: 0916139e-a5db-51c8-be24-2c6fb20a9e3a,
- },
- FunctionGUID {
- guid: 0c7b078d-dafb-5be7-8b10-a25b1e691b33,
- },
- FunctionGUID {
- guid: 0e6feb85-ff6f-5b86-bd2c-26fb9829241c,
- },
- FunctionGUID {
- guid: 0e6feb85-ff6f-5b86-bd2c-26fb9829241c,
- },
- FunctionGUID {
- guid: 11592e15-a2a6-51b3-bda2-534cc0434df0,
- },
- FunctionGUID {
- guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18,
- },
- FunctionGUID {
- guid: 1b1669c2-4a81-5300-99f7-14cba1cc7b24,
- },
- FunctionGUID {
- guid: 1b969932-96ce-5a6f-b91c-82e5f7b7f33d,
- },
- FunctionGUID {
- guid: 1c037c1e-9af1-59af-b19e-122fdf885148,
- },
- FunctionGUID {
- guid: 1de88f85-a19c-5454-9122-ddc80f7509b4,
- },
- FunctionGUID {
- guid: 1de88f85-a19c-5454-9122-ddc80f7509b4,
- },
- FunctionGUID {
- guid: 1e794537-6289-59e7-bef9-0c72f3989db8,
- },
- FunctionGUID {
- guid: 1eee5e05-95ef-5f99-8713-504c2003183c,
- },
- FunctionGUID {
- guid: 2833f544-9acb-552d-aa4b-df7d51dbab64,
- },
- FunctionGUID {
- guid: 29690354-fa27-54d1-a8be-18535b19b1c3,
- },
- FunctionGUID {
- guid: 31a8e73e-74fe-5a33-b0ab-aa1b136e021d,
- },
- FunctionGUID {
- guid: 364c5864-0cd1-5527-8ca0-2378d5b4c0a5,
- },
- FunctionGUID {
- guid: 3c38a678-9c4e-527a-b543-3075b345d532,
- },
- FunctionGUID {
- guid: 3c38a678-9c4e-527a-b543-3075b345d532,
- },
- FunctionGUID {
- guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904,
- },
- FunctionGUID {
- guid: 3eb2a4c5-84f5-5687-92a0-35ef66c8b746,
- },
- FunctionGUID {
- guid: 3efd6327-6038-53ed-904c-a800bee86d2e,
- },
- FunctionGUID {
- guid: 40358a63-7e16-52da-9270-434c7d444b10,
- },
- FunctionGUID {
- guid: 40358a63-7e16-52da-9270-434c7d444b10,
- },
- FunctionGUID {
- guid: 40358a63-7e16-52da-9270-434c7d444b10,
- },
- FunctionGUID {
- guid: 410b1cab-55ff-5931-a894-54842e56bee2,
- },
- FunctionGUID {
- guid: 41fd6d24-0f8b-5848-9873-65b98bd370ab,
- },
- FunctionGUID {
- guid: 4345609a-e619-5f33-84ca-7cc1ccdfa2d0,
- },
- FunctionGUID {
- guid: 43a8c54b-dd4f-5334-ae00-218cb758ad4c,
- },
- FunctionGUID {
- guid: 45770948-1c56-5b24-bd38-f5a559ae8d22,
- },
- FunctionGUID {
- guid: 45770948-1c56-5b24-bd38-f5a559ae8d22,
- },
- FunctionGUID {
- guid: 479b6feb-0dff-5c33-8f30-fd0c563b9abc,
- },
- FunctionGUID {
- guid: 4b60a419-4ad1-5808-b41f-d341868c9234,
- },
- FunctionGUID {
- guid: 4ba8a311-60f3-57d4-a17a-c0cc93f5c2a4,
- },
- FunctionGUID {
- guid: 4f0bdeb9-691c-52f8-8541-6c834f3fbe64,
- },
- FunctionGUID {
- guid: 4f645f3c-3e89-5b5d-a185-9f1a25e94ed2,
- },
- FunctionGUID {
- guid: 52b2a610-5304-5e4f-8b79-9b975e9efb86,
- },
- FunctionGUID {
- guid: 53b484f3-751a-505d-beae-acc9d69c261d,
- },
- FunctionGUID {
- guid: 53b484f3-751a-505d-beae-acc9d69c261d,
- },
- FunctionGUID {
- guid: 56c82b1f-380c-52fe-ae70-a39962e5ffdc,
- },
- FunctionGUID {
- guid: 5779439c-a22f-5d33-8868-4e1832ad49e9,
- },
- FunctionGUID {
- guid: 5b55a8c8-1d3a-5825-a0d9-917f434e07af,
- },
- FunctionGUID {
- guid: 5b7f41e9-de1e-558c-ac7d-888acca3a76c,
- },
- FunctionGUID {
- guid: 65be15d9-ef02-58a4-af63-b40ddb3e56b7,
- },
- FunctionGUID {
- guid: 669a32f2-f1e9-5b38-98aa-9eb8431ebc4e,
- },
- FunctionGUID {
- guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9,
- },
- FunctionGUID {
- guid: 6c01cd47-89a2-59f3-ac6a-cd7806fb8f79,
- },
- FunctionGUID {
- guid: 72452dc5-da9c-54fc-82de-032aab1780a4,
- },
- FunctionGUID {
- guid: 7995ca73-04fe-5b30-9e88-1e69197007c1,
- },
- FunctionGUID {
- guid: 7995ca73-04fe-5b30-9e88-1e69197007c1,
- },
- FunctionGUID {
- guid: 7c43c697-22ef-57a9-a43a-2214e627749d,
- },
- FunctionGUID {
- guid: 7cbd1cef-0134-5a29-a8c6-82697f5717fe,
- },
- FunctionGUID {
- guid: 7d4d1429-5fef-5c8b-9074-93e9d97ab57e,
- },
- FunctionGUID {
- guid: 82e98c80-859c-5ab3-83ab-b597fc520a3f,
- },
- FunctionGUID {
- guid: 84429ba2-9f72-585f-8cda-31e8ef4983fd,
- },
- FunctionGUID {
- guid: 86ab6ab4-45e5-5c08-9a47-05344ea503ab,
- },
- FunctionGUID {
- guid: 86ab6ab4-45e5-5c08-9a47-05344ea503ab,
- },
- FunctionGUID {
- guid: 87371a48-38c9-5e0e-81db-5e2d6da49202,
- },
- FunctionGUID {
- guid: 879b1d59-7301-5cb7-911e-db05292bee8f,
- },
- FunctionGUID {
- guid: 88d9b26b-3884-58d6-b164-435d8d888e5c,
- },
- FunctionGUID {
- guid: 89f0e45b-c1f6-5aeb-94a8-26e72bb66e6c,
- },
- FunctionGUID {
- guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd,
- },
- FunctionGUID {
- guid: 8c736b66-b63f-5000-a060-2e60a1b8952a,
- },
- FunctionGUID {
- guid: 8c736b66-b63f-5000-a060-2e60a1b8952a,
- },
- FunctionGUID {
- guid: 8c736b66-b63f-5000-a060-2e60a1b8952a,
- },
- FunctionGUID {
- guid: 8dca0c9d-bdb7-56a1-9832-431aec16a68f,
- },
- FunctionGUID {
- guid: 9a9e3287-e407-5526-885b-1b5ee78a5300,
- },
- FunctionGUID {
- guid: 9e4c3806-b291-569c-a687-0bcaa48e248b,
- },
- FunctionGUID {
- guid: 9ef21651-3eaf-5940-8e72-104bda834e85,
- },
- FunctionGUID {
- guid: a0c07bfa-a027-5c19-9da4-d2d35d7beb40,
- },
- FunctionGUID {
- guid: a0c07bfa-a027-5c19-9da4-d2d35d7beb40,
- },
- FunctionGUID {
- guid: a23505e4-e7cd-5543-897c-46b97c1eaef3,
- },
- FunctionGUID {
- guid: a482559e-6a69-505e-b692-147deeced692,
- },
- FunctionGUID {
- guid: a482559e-6a69-505e-b692-147deeced692,
- },
- FunctionGUID {
- guid: a5b947d4-a7e0-5072-86b3-240474c16595,
- },
- FunctionGUID {
- guid: a603d813-2b33-567d-80c8-06e31562a400,
- },
- FunctionGUID {
- guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5,
- },
- FunctionGUID {
- guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5,
- },
- FunctionGUID {
- guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5,
- },
- FunctionGUID {
- guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5,
- },
- FunctionGUID {
- guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5,
- },
- FunctionGUID {
- guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5,
- },
- FunctionGUID {
- guid: adadfe23-cb3f-5c9f-ae8d-1dd2fd922367,
- },
- FunctionGUID {
- guid: b098f8a1-ecdd-5938-8da3-bb37935c6b7e,
- },
- FunctionGUID {
- guid: b098f8a1-ecdd-5938-8da3-bb37935c6b7e,
- },
- FunctionGUID {
- guid: b0aca296-adc3-5b46-a6b1-e82ef2100ead,
- },
- FunctionGUID {
- guid: b9111cf4-dcff-52a5-8755-2dedea6d5c72,
- },
- FunctionGUID {
- guid: bbb9b018-a0cd-5f98-b941-367f8707434d,
- },
- FunctionGUID {
- guid: bbb9b018-a0cd-5f98-b941-367f8707434d,
- },
- FunctionGUID {
- guid: bfaf2f0a-d92d-5ef7-879d-86eb0ca01257,
- },
- FunctionGUID {
- guid: bfaf2f0a-d92d-5ef7-879d-86eb0ca01257,
- },
- FunctionGUID {
- guid: c3842e85-b9f2-54d5-b353-e9bb2a0b1203,
- },
- FunctionGUID {
- guid: c3842e85-b9f2-54d5-b353-e9bb2a0b1203,
- },
- FunctionGUID {
- guid: c44bdef1-d78a-57ec-bff5-4e7d32ff4337,
- },
- FunctionGUID {
- guid: c71f4559-f0af-54dc-a224-e1f341311988,
- },
- FunctionGUID {
- guid: c71f4559-f0af-54dc-a224-e1f341311988,
- },
- FunctionGUID {
- guid: c74ed160-24fe-50d7-ba1c-9751c4599420,
- },
- FunctionGUID {
- guid: ccbd9943-6b24-59d8-ae4d-3ee868a2729c,
- },
- FunctionGUID {
- guid: ccbd9943-6b24-59d8-ae4d-3ee868a2729c,
- },
- FunctionGUID {
- guid: ce28e080-0609-5bcd-abe1-2a6e57c91f83,
- },
- FunctionGUID {
- guid: ce28e080-0609-5bcd-abe1-2a6e57c91f83,
- },
- FunctionGUID {
- guid: ce28e080-0609-5bcd-abe1-2a6e57c91f83,
- },
- FunctionGUID {
- guid: cf4808fe-2fea-517d-8f85-f88ddc96cc78,
- },
- FunctionGUID {
- guid: cf4808fe-2fea-517d-8f85-f88ddc96cc78,
- },
- FunctionGUID {
- guid: cfa4374b-0fc7-575d-9ba1-4a37d2de0406,
- },
- FunctionGUID {
- guid: d72f40b9-0557-5695-aa4d-468515ec4b8e,
- },
- FunctionGUID {
- guid: d8ba616a-1747-587c-9265-cd4e26ae35c8,
- },
- FunctionGUID {
- guid: db957206-4cca-5631-893d-1d3dc500c804,
- },
- FunctionGUID {
- guid: dd480d6b-04f7-5fb2-a7b2-236e1d78b2b0,
- },
- FunctionGUID {
- guid: e1b1e1df-fdb9-59d6-b966-5b7f926b5649,
- },
- FunctionGUID {
- guid: e480dd82-0b7d-5c75-ac67-ba8c0bb755cb,
- },
- FunctionGUID {
- guid: e4ba95bb-0221-5dbd-9884-13a39c4f9d55,
- },
- FunctionGUID {
- guid: e8290b3d-cf56-5c1b-8315-abce0f135559,
- },
- FunctionGUID {
- guid: e8290b3d-cf56-5c1b-8315-abce0f135559,
- },
- FunctionGUID {
- guid: ecca9a9d-1722-5344-b68f-54f4527c0d78,
- },
- FunctionGUID {
- guid: ed23805d-3729-590d-83a3-3b0803a56870,
- },
- FunctionGUID {
- guid: eefbc8a9-f4f1-5725-9a4f-9dcd04ea3478,
- },
- FunctionGUID {
- guid: f37337a6-4cf0-55b4-a0da-25a15e3d9bcb,
- },
- FunctionGUID {
- guid: f37cf37b-b19d-5575-b2c7-a6604d3faa69,
- },
- FunctionGUID {
- guid: f3d57592-4cb1-5356-9591-1f9327c58565,
- },
- FunctionGUID {
- guid: fb20fa7c-c796-58e4-a2f9-7a128baaf5b1,
- },
-]
diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atoldbl.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atoldbl.snap
deleted file mode 100644
index 5dc305cc..00000000
--- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atoldbl.snap
+++ /dev/null
@@ -1,192 +0,0 @@
----
-source: plugins/warp/src/lib.rs
-expression: functions
----
-[
- FunctionGUID {
- guid: 02d864ef-80bc-5265-b78f-0e42815a4d1d,
- },
- FunctionGUID {
- guid: 07b3f23f-333b-5c13-873f-5f1973cee8e2,
- },
- FunctionGUID {
- guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18,
- },
- FunctionGUID {
- guid: 1de88f85-a19c-5454-9122-ddc80f7509b4,
- },
- FunctionGUID {
- guid: 1e794537-6289-59e7-bef9-0c72f3989db8,
- },
- FunctionGUID {
- guid: 22c32840-7bb0-5d80-858c-55dd04553db0,
- },
- FunctionGUID {
- guid: 29690354-fa27-54d1-a8be-18535b19b1c3,
- },
- FunctionGUID {
- guid: 29690354-fa27-54d1-a8be-18535b19b1c3,
- },
- FunctionGUID {
- guid: 30f44555-ef86-5e85-96c3-6153f4bcc11e,
- },
- FunctionGUID {
- guid: 364c5864-0cd1-5527-8ca0-2378d5b4c0a5,
- },
- FunctionGUID {
- guid: 39cd0d51-d688-58e2-a757-ac868ff8635e,
- },
- FunctionGUID {
- guid: 3c83a5f5-215d-5d25-97c8-ffe65f73907c,
- },
- FunctionGUID {
- guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904,
- },
- FunctionGUID {
- guid: 41fd6d24-0f8b-5848-9873-65b98bd370ab,
- },
- FunctionGUID {
- guid: 43a8c54b-dd4f-5334-ae00-218cb758ad4c,
- },
- FunctionGUID {
- guid: 45770948-1c56-5b24-bd38-f5a559ae8d22,
- },
- FunctionGUID {
- guid: 4738dcb9-abab-5157-baef-8edf38d9fabc,
- },
- FunctionGUID {
- guid: 48551d5f-2e1f-557b-85a3-a122d3ce07cf,
- },
- FunctionGUID {
- guid: 4ba8a311-60f3-57d4-a17a-c0cc93f5c2a4,
- },
- FunctionGUID {
- guid: 4f645f3c-3e89-5b5d-a185-9f1a25e94ed2,
- },
- FunctionGUID {
- guid: 5779439c-a22f-5d33-8868-4e1832ad49e9,
- },
- FunctionGUID {
- guid: 5b7f41e9-de1e-558c-ac7d-888acca3a76c,
- },
- FunctionGUID {
- guid: 61d32a04-c077-5985-b241-c326dd5b1864,
- },
- FunctionGUID {
- guid: 6444ff14-38c5-520e-b9e3-6e3ae15215c4,
- },
- FunctionGUID {
- guid: 6589669f-6b11-569e-9adf-ca3c6d6bc62e,
- },
- FunctionGUID {
- guid: 6590d978-d2b0-510c-b5a6-a81361800609,
- },
- FunctionGUID {
- guid: 67f303c6-d2e4-55e1-8a5f-5492da69e69f,
- },
- FunctionGUID {
- guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9,
- },
- FunctionGUID {
- guid: 6ef93fa8-909e-596c-9454-74b89e8cabb7,
- },
- FunctionGUID {
- guid: 7995ca73-04fe-5b30-9e88-1e69197007c1,
- },
- FunctionGUID {
- guid: 7c43c697-22ef-57a9-a43a-2214e627749d,
- },
- FunctionGUID {
- guid: 86ab6ab4-45e5-5c08-9a47-05344ea503ab,
- },
- FunctionGUID {
- guid: 88d9b26b-3884-58d6-b164-435d8d888e5c,
- },
- FunctionGUID {
- guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd,
- },
- FunctionGUID {
- guid: 8c736b66-b63f-5000-a060-2e60a1b8952a,
- },
- FunctionGUID {
- guid: 8c736b66-b63f-5000-a060-2e60a1b8952a,
- },
- FunctionGUID {
- guid: 8c736b66-b63f-5000-a060-2e60a1b8952a,
- },
- FunctionGUID {
- guid: 8ccf8b37-65c5-525a-b96a-64f26d711c75,
- },
- FunctionGUID {
- guid: 9676fc90-ecba-595b-8d24-5df4e66ac683,
- },
- FunctionGUID {
- guid: 9891b84c-000b-529a-8c5b-d762120eff9d,
- },
- FunctionGUID {
- guid: a030f6f1-e29b-5e4a-8f83-56e6c6c51c15,
- },
- FunctionGUID {
- guid: a23505e4-e7cd-5543-897c-46b97c1eaef3,
- },
- FunctionGUID {
- guid: a45d9322-0d45-5dc9-b17b-32b843416c4b,
- },
- FunctionGUID {
- guid: a482559e-6a69-505e-b692-147deeced692,
- },
- FunctionGUID {
- guid: a603d813-2b33-567d-80c8-06e31562a400,
- },
- FunctionGUID {
- guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5,
- },
- FunctionGUID {
- guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5,
- },
- FunctionGUID {
- guid: a707a2d3-c7d2-5b93-97c9-1db77d8ccfe5,
- },
- FunctionGUID {
- guid: b9e4b6b0-47c3-54ca-9a01-56916ea0db8e,
- },
- FunctionGUID {
- guid: bae57993-863a-5c22-b8bb-92b862148af3,
- },
- FunctionGUID {
- guid: bfaf2f0a-d92d-5ef7-879d-86eb0ca01257,
- },
- FunctionGUID {
- guid: c3842e85-b9f2-54d5-b353-e9bb2a0b1203,
- },
- FunctionGUID {
- guid: c5766cb0-afbc-5701-ad2f-23d980f4bc15,
- },
- FunctionGUID {
- guid: cb8ebff6-30e8-5a8a-9c1b-ec5f26041d65,
- },
- FunctionGUID {
- guid: ccbd9943-6b24-59d8-ae4d-3ee868a2729c,
- },
- FunctionGUID {
- guid: ce28e080-0609-5bcd-abe1-2a6e57c91f83,
- },
- FunctionGUID {
- guid: cf4808fe-2fea-517d-8f85-f88ddc96cc78,
- },
- FunctionGUID {
- guid: e3b83b64-6ba1-5fe5-97da-5db987d79b07,
- },
- FunctionGUID {
- guid: eefbc8a9-f4f1-5725-9a4f-9dcd04ea3478,
- },
- FunctionGUID {
- guid: efcd394f-9201-57f7-aa2d-fc89ad355326,
- },
- FunctionGUID {
- guid: efcd394f-9201-57f7-aa2d-fc89ad355326,
- },
- FunctionGUID {
- guid: f25e7750-c61c-5ed8-a43d-959c3ba3a5b4,
- },
-]
diff --git a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atox.snap b/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atox.snap
deleted file mode 100644
index c9960a24..00000000
--- a/plugins/warp/src/snapshots/warp_ninja__tests__snapshot_atox.snap
+++ /dev/null
@@ -1,186 +0,0 @@
----
-source: plugins/warp/src/lib.rs
-expression: functions
----
-[
- FunctionGUID {
- guid: 0244fa6b-6425-5aa9-ad87-2378cc4992e7,
- },
- FunctionGUID {
- guid: 02d864ef-80bc-5265-b78f-0e42815a4d1d,
- },
- FunctionGUID {
- guid: 02d864ef-80bc-5265-b78f-0e42815a4d1d,
- },
- FunctionGUID {
- guid: 067d6811-472a-5e4a-b65f-3760df0b7bd5,
- },
- FunctionGUID {
- guid: 0916139e-a5db-51c8-be24-2c6fb20a9e3a,
- },
- FunctionGUID {
- guid: 13b16f81-0c6f-5aee-ad9f-0d142658cf18,
- },
- FunctionGUID {
- guid: 1de88f85-a19c-5454-9122-ddc80f7509b4,
- },
- FunctionGUID {
- guid: 1de88f85-a19c-5454-9122-ddc80f7509b4,
- },
- FunctionGUID {
- guid: 1e794537-6289-59e7-bef9-0c72f3989db8,
- },
- FunctionGUID {
- guid: 283e5e36-a239-5f0c-8937-f48a4b1d86e0,
- },
- FunctionGUID {
- guid: 29690354-fa27-54d1-a8be-18535b19b1c3,
- },
- FunctionGUID {
- guid: 31a8e73e-74fe-5a33-b0ab-aa1b136e021d,
- },
- FunctionGUID {
- guid: 33717d5d-a15f-5c10-bc4a-d85cce963aca,
- },
- FunctionGUID {
- guid: 39d556f9-435a-5105-b56c-6558ddc63fc0,
- },
- FunctionGUID {
- guid: 3cc2e827-b707-5c9d-82e9-d76ac3abc904,
- },
- FunctionGUID {
- guid: 3f9f8782-0b8c-5843-8b1d-831a1ef9b024,
- },
- FunctionGUID {
- guid: 3f9f8782-0b8c-5843-8b1d-831a1ef9b024,
- },
- FunctionGUID {
- guid: 3f9f8782-0b8c-5843-8b1d-831a1ef9b024,
- },
- FunctionGUID {
- guid: 3f9f8782-0b8c-5843-8b1d-831a1ef9b024,
- },
- FunctionGUID {
- guid: 43a8c54b-dd4f-5334-ae00-218cb758ad4c,
- },
- FunctionGUID {
- guid: 46eae5e0-61e2-5316-9e4d-030dd2240567,
- },
- FunctionGUID {
- guid: 4ba8a311-60f3-57d4-a17a-c0cc93f5c2a4,
- },
- FunctionGUID {
- guid: 56c82b1f-380c-52fe-ae70-a39962e5ffdc,
- },
- FunctionGUID {
- guid: 5779439c-a22f-5d33-8868-4e1832ad49e9,
- },
- FunctionGUID {
- guid: 580affd8-2657-50b2-9b65-3327422894de,
- },
- FunctionGUID {
- guid: 580affd8-2657-50b2-9b65-3327422894de,
- },
- FunctionGUID {
- guid: 580affd8-2657-50b2-9b65-3327422894de,
- },
- FunctionGUID {
- guid: 580affd8-2657-50b2-9b65-3327422894de,
- },
- FunctionGUID {
- guid: 580affd8-2657-50b2-9b65-3327422894de,
- },
- FunctionGUID {
- guid: 580affd8-2657-50b2-9b65-3327422894de,
- },
- FunctionGUID {
- guid: 580affd8-2657-50b2-9b65-3327422894de,
- },
- FunctionGUID {
- guid: 580affd8-2657-50b2-9b65-3327422894de,
- },
- FunctionGUID {
- guid: 5b7f41e9-de1e-558c-ac7d-888acca3a76c,
- },
- FunctionGUID {
- guid: 65cb411a-0ae9-546f-9293-3abaa3fd1c31,
- },
- FunctionGUID {
- guid: 6abe3fc2-6d29-5fde-a31e-bb8249db6bf9,
- },
- FunctionGUID {
- guid: 7995ca73-04fe-5b30-9e88-1e69197007c1,
- },
- FunctionGUID {
- guid: 7995ca73-04fe-5b30-9e88-1e69197007c1,
- },
- FunctionGUID {
- guid: 88d9b26b-3884-58d6-b164-435d8d888e5c,
- },
- FunctionGUID {
- guid: 8a2b7bda-5fdb-5ba3-888b-20d5b8d7b2cd,
- },
- FunctionGUID {
- guid: 9ef21651-3eaf-5940-8e72-104bda834e85,
- },
- FunctionGUID {
- guid: a23505e4-e7cd-5543-897c-46b97c1eaef3,
- },
- FunctionGUID {
- guid: a482559e-6a69-505e-b692-147deeced692,
- },
- FunctionGUID {
- guid: a482559e-6a69-505e-b692-147deeced692,
- },
- FunctionGUID {
- guid: a603d813-2b33-567d-80c8-06e31562a400,
- },
- FunctionGUID {
- guid: a8a9d162-b637-5ce6-82f8-a8ac0fb740dc,
- },
- FunctionGUID {
- guid: bdf90a04-d375-59b1-8a92-b962b3f914ae,
- },
- FunctionGUID {
- guid: bdf90a04-d375-59b1-8a92-b962b3f914ae,
- },
- FunctionGUID {
- guid: bdf90a04-d375-59b1-8a92-b962b3f914ae,
- },
- FunctionGUID {
- guid: bdf90a04-d375-59b1-8a92-b962b3f914ae,
- },
- FunctionGUID {
- guid: bdf90a04-d375-59b1-8a92-b962b3f914ae,
- },
- FunctionGUID {
- guid: bdf90a04-d375-59b1-8a92-b962b3f914ae,
- },
- FunctionGUID {
- guid: bdf90a04-d375-59b1-8a92-b962b3f914ae,
- },
- FunctionGUID {
- guid: bdf90a04-d375-59b1-8a92-b962b3f914ae,
- },
- FunctionGUID {
- guid: c3842e85-b9f2-54d5-b353-e9bb2a0b1203,
- },
- FunctionGUID {
- guid: c3842e85-b9f2-54d5-b353-e9bb2a0b1203,
- },
- FunctionGUID {
- guid: c66dd704-899b-5d58-9d4a-136063f5f552,
- },
- FunctionGUID {
- guid: ccbd9943-6b24-59d8-ae4d-3ee868a2729c,
- },
- FunctionGUID {
- guid: ccbd9943-6b24-59d8-ae4d-3ee868a2729c,
- },
- FunctionGUID {
- guid: f674b84a-7a05-5ca0-aac4-5b5598683087,
- },
- FunctionGUID {
- guid: f762c527-0674-52ce-b59f-49bf4e13d878,
- },
-]
diff --git a/plugins/warp/src/templates/file.html b/plugins/warp/src/templates/file.html
new file mode 100644
index 00000000..7ea465ef
--- /dev/null
+++ b/plugins/warp/src/templates/file.html
@@ -0,0 +1,37 @@
+<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
new file mode 100644
index 00000000..764d35cd
--- /dev/null
+++ b/plugins/warp/src/templates/file.json
@@ -0,0 +1,21 @@
+{
+ "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
new file mode 100644
index 00000000..433cb8e9
--- /dev/null
+++ b/plugins/warp/src/templates/file.md
@@ -0,0 +1,15 @@
+# {{ 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 %}