summaryrefslogtreecommitdiff
path: root/rust
diff options
context:
space:
mode:
authorrbran <git@rubens.io>2025-04-23 19:24:45 +0000
committerMason Reed <35282038+emesare@users.noreply.github.com>2025-05-12 17:45:24 -0400
commitc2d90f2e5a010546d0e1f1deeb1a6d8531ea4d4f (patch)
tree20a6c6268943154fdda6a4d6884f2f29d9d4de50 /rust
parent4a4a8488269922269b20a12a7b71272b925b9f18 (diff)
[Rust] Implement custom interactive handler
Diffstat (limited to 'rust')
-rw-r--r--rust/src/binary_view.rs40
-rw-r--r--rust/src/function.rs16
-rw-r--r--rust/src/interaction.rs433
-rw-r--r--rust/src/interaction/form.rs411
-rw-r--r--rust/src/interaction/handler.rs598
-rw-r--r--rust/src/interaction/report.rs311
-rw-r--r--rust/src/language_representation.rs23
-rw-r--r--rust/src/line_formatter.rs21
-rw-r--r--rust/src/string.rs12
-rw-r--r--rust/tests/interaction.rs169
10 files changed, 1610 insertions, 424 deletions
diff --git a/rust/src/binary_view.rs b/rust/src/binary_view.rs
index 55b0d4db..65bab463 100644
--- a/rust/src/binary_view.rs
+++ b/rust/src/binary_view.rs
@@ -1103,6 +1103,46 @@ pub trait BinaryViewExt: BinaryViewBase {
unsafe { BNApplyDebugInfo(self.as_ref().handle, debug_info.handle) }
}
+ fn show_plaintext_report(&self, title: &str, plaintext: &str) {
+ let title = title.to_cstr();
+ let plaintext = plaintext.to_cstr();
+ unsafe {
+ BNShowPlainTextReport(
+ self.as_ref().handle,
+ title.as_ref().as_ptr() as *mut _,
+ plaintext.as_ref().as_ptr() as *mut _,
+ )
+ }
+ }
+
+ fn show_markdown_report(&self, title: &str, contents: &str, plaintext: &str) {
+ let title = title.to_cstr();
+ let contents = contents.to_cstr();
+ let plaintext = plaintext.to_cstr();
+ unsafe {
+ BNShowMarkdownReport(
+ self.as_ref().handle,
+ title.as_ref().as_ptr() as *mut _,
+ contents.as_ref().as_ptr() as *mut _,
+ plaintext.as_ref().as_ptr() as *mut _,
+ )
+ }
+ }
+
+ fn show_html_report(&self, title: &str, contents: &str, plaintext: &str) {
+ let title = title.to_cstr();
+ let contents = contents.to_cstr();
+ let plaintext = plaintext.to_cstr();
+ unsafe {
+ BNShowHTMLReport(
+ self.as_ref().handle,
+ title.as_ref().as_ptr() as *mut _,
+ contents.as_ref().as_ptr() as *mut _,
+ plaintext.as_ref().as_ptr() as *mut _,
+ )
+ }
+ }
+
fn show_graph_report(&self, raw_name: &str, graph: &FlowGraph) {
let raw_name = raw_name.to_cstr();
unsafe {
diff --git a/rust/src/function.rs b/rust/src/function.rs
index edde15a7..e7e5e9b0 100644
--- a/rust/src/function.rs
+++ b/rust/src/function.rs
@@ -480,15 +480,15 @@ impl Function {
/// Get the language representation of the function.
///
/// * `language` - The language representation, ex. "Pseudo C".
- pub fn language_representation<S: BnStrCompatible>(
+ pub fn language_representation(
&self,
- language: S,
+ language: &str,
) -> Option<Ref<CoreLanguageRepresentationFunction>> {
- let lang_name = language.into_bytes_with_nul();
+ let lang_name = language.to_cstr();
let repr = unsafe {
BNGetFunctionLanguageRepresentation(
self.handle,
- lang_name.as_ref().as_ptr() as *const c_char,
+ lang_name.as_ptr(),
)
};
NonNull::new(repr)
@@ -498,15 +498,15 @@ impl Function {
/// Get the language representation of the function, if available.
///
/// * `language` - The language representation, ex. "Pseudo C".
- pub fn language_representation_if_available<S: BnStrCompatible>(
+ pub fn language_representation_if_available(
&self,
- language: S,
+ language: &str,
) -> Option<Ref<CoreLanguageRepresentationFunction>> {
- let lang_name = language.into_bytes_with_nul();
+ let lang_name = language.to_cstr();
let repr = unsafe {
BNGetFunctionLanguageRepresentationIfAvailable(
self.handle,
- lang_name.as_ref().as_ptr() as *const c_char,
+ lang_name.as_ptr(),
)
};
NonNull::new(repr)
diff --git a/rust/src/interaction.rs b/rust/src/interaction.rs
index fad207d1..1a3ef2aa 100644
--- a/rust/src/interaction.rs
+++ b/rust/src/interaction.rs
@@ -14,15 +14,17 @@
//! Interfaces for asking the user for information: forms, opening files, etc.
-use binaryninjacore_sys::*;
-
-use std::ffi::{c_char, c_void, CStr};
+use std::ffi::{c_char, c_void};
use std::path::PathBuf;
-use crate::binary_view::BinaryView;
-use crate::rc::Ref;
+use binaryninjacore_sys::*;
+
use crate::string::{BnString, IntoCStr};
+pub mod form;
+pub mod handler;
+pub mod report;
+
pub type MessageBoxButtonSet = BNMessageBoxButtonSet;
pub type MessageBoxIcon = BNMessageBoxIcon;
pub type MessageBoxButtonResult = BNMessageBoxButtonResult;
@@ -76,6 +78,48 @@ pub fn get_address_input(prompt: &str, title: &str) -> Option<u64> {
Some(value)
}
+pub fn get_choice_input(prompt: &str, title: &str, choices: &[&str]) -> Option<usize> {
+ let prompt = prompt.to_cstr();
+ let title = title.to_cstr();
+ let mut choices_inner: Vec<BnString> = choices.iter().copied().map(BnString::new).collect();
+ // SAFETY BnString and *const c_char are transparent
+ let choices: &mut [*const c_char] = unsafe {
+ core::mem::transmute::<&mut [BnString], &mut [*const c_char]>(&mut choices_inner[..])
+ };
+ let mut result = 0;
+ let succ = unsafe {
+ BNGetChoiceInput(
+ &mut result,
+ prompt.as_ptr(),
+ title.as_ptr(),
+ choices.as_mut_ptr(),
+ choices.len(),
+ )
+ };
+ succ.then_some(result)
+}
+
+pub fn get_large_choice_input(prompt: &str, title: &str, choices: &[&str]) -> Option<usize> {
+ let prompt = prompt.to_cstr();
+ let title = title.to_cstr();
+ let mut choices_inner: Vec<BnString> = choices.iter().copied().map(BnString::new).collect();
+ // SAFETY BnString and *const c_char are transparent
+ let choices: &mut [*const c_char] = unsafe {
+ core::mem::transmute::<&mut [BnString], &mut [*const c_char]>(&mut choices_inner[..])
+ };
+ let mut result = 0;
+ let succ = unsafe {
+ BNGetLargeChoiceInput(
+ &mut result,
+ prompt.as_ptr(),
+ title.as_ptr(),
+ choices.as_mut_ptr(),
+ choices.len(),
+ )
+ };
+ succ.then_some(result)
+}
+
pub fn get_open_filename_input(prompt: &str, extension: &str) -> Option<PathBuf> {
let mut value: *mut c_char = std::ptr::null_mut();
@@ -142,385 +186,6 @@ pub fn show_message_box(
unsafe { BNShowMessageBox(title.as_ptr(), text.as_ptr(), buttons, icon) }
}
-pub enum FormResponses {
- None,
- String(String),
- Integer(i64),
- Address(u64),
- Index(usize),
-}
-
-enum FormData {
- Label {
- _text: BnString,
- },
- Text {
- _prompt: BnString,
- _default: Option<BnString>,
- },
- Choice {
- _prompt: BnString,
- _choices: Vec<BnString>,
- _raw: Vec<*const c_char>,
- },
- File {
- _prompt: BnString,
- _ext: BnString,
- _default: Option<BnString>,
- },
- FileSave {
- _prompt: BnString,
- _ext: BnString,
- _default_name: BnString,
- _default: Option<BnString>,
- },
-}
-
-pub struct FormInputBuilder {
- fields: Vec<BNFormInputField>,
- data: Vec<FormData>,
-}
-
-impl FormInputBuilder {
- pub fn new() -> Self {
- Self {
- fields: vec![],
- data: vec![],
- }
- }
-
- /// Form Field: Text output
- pub fn label_field(mut self, text: &str) -> Self {
- let text = BnString::new(text);
-
- let mut result = unsafe { std::mem::zeroed::<BNFormInputField>() };
- result.type_ = BNFormInputFieldType::LabelFormField;
- result.hasDefault = false;
- result.prompt = text.as_ptr();
- self.fields.push(result);
-
- self.data.push(FormData::Label { _text: text });
- self
- }
-
- /// Form Field: Vertical spacing
- pub fn separator_field(mut self) -> Self {
- let mut result = unsafe { std::mem::zeroed::<BNFormInputField>() };
- result.type_ = BNFormInputFieldType::SeparatorFormField;
- result.hasDefault = false;
- self.fields.push(result);
- self
- }
-
- /// Form Field: Prompt for a string value
- pub fn text_field(mut self, prompt: &str, default: Option<&str>) -> Self {
- let prompt = BnString::new(prompt);
- let default = default.map(BnString::new);
-
- let mut result = unsafe { std::mem::zeroed::<BNFormInputField>() };
- result.type_ = BNFormInputFieldType::TextLineFormField;
- result.prompt = prompt.as_ptr();
- result.hasDefault = default.is_some();
- if let Some(ref default) = default {
- result.stringDefault = default.as_ptr();
- }
- self.fields.push(result);
-
- self.data.push(FormData::Text {
- _prompt: prompt,
- _default: default,
- });
- self
- }
-
- /// Form Field: Prompt for multi-line string value
- pub fn multiline_field(mut self, prompt: &str, default: Option<&str>) -> Self {
- let prompt = BnString::new(prompt);
- let default = default.map(BnString::new);
-
- let mut result = unsafe { std::mem::zeroed::<BNFormInputField>() };
- result.type_ = BNFormInputFieldType::MultilineTextFormField;
- result.prompt = prompt.as_ptr();
- result.hasDefault = default.is_some();
- if let Some(ref default) = default {
- result.stringDefault = default.as_ptr();
- }
- self.fields.push(result);
-
- self.data.push(FormData::Text {
- _prompt: prompt,
- _default: default,
- });
- self
- }
-
- /// Form Field: Prompt for an integer
- pub fn integer_field(mut self, prompt: &str, default: Option<i64>) -> Self {
- let prompt = BnString::new(prompt);
-
- let mut result = unsafe { std::mem::zeroed::<BNFormInputField>() };
- result.type_ = BNFormInputFieldType::IntegerFormField;
- result.prompt = prompt.as_ptr();
- result.hasDefault = default.is_some();
- if let Some(default) = default {
- result.intDefault = default;
- }
- self.fields.push(result);
-
- self.data.push(FormData::Label { _text: prompt });
- self
- }
-
- /// Form Field: Prompt for an address
- pub fn address_field(
- mut self,
- prompt: &str,
- view: Option<Ref<BinaryView>>,
- current_address: Option<u64>,
- default: Option<u64>,
- ) -> Self {
- let prompt = BnString::new(prompt);
-
- let mut result = unsafe { std::mem::zeroed::<BNFormInputField>() };
- result.type_ = BNFormInputFieldType::AddressFormField;
- result.prompt = prompt.as_ptr();
- if let Some(view) = view {
- // the view is being moved into result, there is no need to clone
- // and drop is intentionally being avoided with `Ref::into_raw`
- result.view = unsafe { Ref::into_raw(view) }.handle;
- }
- result.currentAddress = current_address.unwrap_or(0);
- result.hasDefault = default.is_some();
- if let Some(default) = default {
- result.addressDefault = default;
- }
- self.fields.push(result);
-
- self.data.push(FormData::Label { _text: prompt });
- self
- }
-
- /// Form Field: Prompt for a choice from provided options
- pub fn choice_field(mut self, prompt: &str, choices: &[&str], default: Option<usize>) -> Self {
- let prompt = BnString::new(prompt);
- let choices: Vec<BnString> = choices.iter().map(|&s| BnString::new(s)).collect();
-
- let mut result = unsafe { std::mem::zeroed::<BNFormInputField>() };
- result.type_ = BNFormInputFieldType::ChoiceFormField;
- result.prompt = prompt.as_ptr();
- let mut raw_choices: Vec<*const c_char> = choices.iter().map(|c| c.as_ptr()).collect();
- result.choices = raw_choices.as_mut_ptr();
- result.count = choices.len();
- result.hasDefault = default.is_some();
- if let Some(default) = default {
- result.indexDefault = default;
- }
- self.fields.push(result);
-
- self.data.push(FormData::Choice {
- _prompt: prompt,
- _choices: choices,
- _raw: raw_choices,
- });
- self
- }
-
- /// Form Field: Prompt for file to open
- pub fn open_file_field(
- mut self,
- prompt: &str,
- ext: Option<&str>,
- default: Option<&str>,
- ) -> Self {
- let prompt = BnString::new(prompt);
- let ext = if let Some(ext) = ext {
- BnString::new(ext)
- } else {
- BnString::new("")
- };
- let default = default.map(BnString::new);
-
- let mut result = unsafe { std::mem::zeroed::<BNFormInputField>() };
- result.type_ = BNFormInputFieldType::OpenFileNameFormField;
- result.prompt = prompt.as_ptr();
- result.ext = ext.as_ptr();
- result.hasDefault = default.is_some();
- if let Some(ref default) = default {
- result.stringDefault = default.as_ptr();
- }
- self.fields.push(result);
-
- self.data.push(FormData::File {
- _prompt: prompt,
- _ext: ext,
- _default: default,
- });
- self
- }
-
- /// Form Field: Prompt for file to save to
- pub fn save_file_field(
- mut self,
- prompt: &str,
- ext: Option<&str>,
- default_name: Option<&str>,
- default: Option<&str>,
- ) -> Self {
- let prompt = BnString::new(prompt);
- let ext = if let Some(ext) = ext {
- BnString::new(ext)
- } else {
- BnString::new("")
- };
- let default_name = if let Some(default_name) = default_name {
- BnString::new(default_name)
- } else {
- BnString::new("")
- };
- let default = default.map(BnString::new);
-
- let mut result = unsafe { std::mem::zeroed::<BNFormInputField>() };
- result.type_ = BNFormInputFieldType::SaveFileNameFormField;
- result.prompt = prompt.as_ptr();
- result.ext = ext.as_ptr();
- result.defaultName = default_name.as_ptr();
- result.hasDefault = default.is_some();
- if let Some(ref default) = default {
- result.stringDefault = default.as_ptr();
- }
- self.fields.push(result);
-
- self.data.push(FormData::FileSave {
- _prompt: prompt,
- _ext: ext,
- _default_name: default_name,
- _default: default,
- });
- self
- }
-
- /// Form Field: Prompt for directory name
- pub fn directory_name_field(
- mut self,
- prompt: &str,
- default_name: Option<&str>,
- default: Option<&str>,
- ) -> Self {
- let prompt = BnString::new(prompt);
- let default_name = if let Some(default_name) = default_name {
- BnString::new(default_name)
- } else {
- BnString::new("")
- };
- let default = default.map(BnString::new);
-
- let mut result = unsafe { std::mem::zeroed::<BNFormInputField>() };
- result.type_ = BNFormInputFieldType::DirectoryNameFormField;
- result.prompt = prompt.as_ptr();
- result.defaultName = default_name.as_ptr();
- result.hasDefault = default.is_some();
- if let Some(ref default) = default {
- result.stringDefault = default.as_ptr();
- }
- self.fields.push(result);
-
- self.data.push(FormData::File {
- _prompt: prompt,
- _ext: default_name,
- _default: default,
- });
- self
- }
-
- /// Prompts the user for a set of inputs specified in `fields` with given title.
- /// The fields parameter is a list which can contain the following types:
- ///
- /// This API is flexible and works both in the UI via a pop-up dialog and on the command-line.
- ///
- /// ```no_run
- /// # use binaryninja::interaction::FormInputBuilder;
- /// # use binaryninja::interaction::FormResponses;
- /// let responses = FormInputBuilder::new()
- /// .text_field("First Name", None)
- /// .text_field("Last Name", None)
- /// .choice_field(
- /// "Favorite Food",
- /// &vec![
- /// "Pizza",
- /// "Also Pizza",
- /// "Also Pizza",
- /// "Yummy Pizza",
- /// "Wrong Answer",
- /// ],
- /// Some(0),
- /// )
- /// .get_form_input("Form Title");
- ///
- /// let food = match responses[2] {
- /// FormResponses::Index(0) => "Pizza",
- /// FormResponses::Index(1) => "Also Pizza",
- /// FormResponses::Index(2) => "Also Pizza",
- /// FormResponses::Index(3) => "Wrong Answer",
- /// _ => panic!("This person doesn't like pizza?!?"),
- /// };
- ///
- /// let FormResponses::String(last_name) = &responses[0] else {
- /// unreachable!()
- /// };
- /// let FormResponses::String(first_name) = &responses[1] else {
- /// unreachable!()
- /// };
- ///
- /// println!("{} {} likes {}", &first_name, &last_name, food);
- /// ```
- pub fn get_form_input(&mut self, title: &str) -> Vec<FormResponses> {
- let title = title.to_cstr();
- if unsafe { BNGetFormInput(self.fields.as_mut_ptr(), self.fields.len(), title.as_ptr()) } {
- let result = self
- .fields
- .iter()
- .map(|form_field| match form_field.type_ {
- BNFormInputFieldType::LabelFormField
- | BNFormInputFieldType::SeparatorFormField => FormResponses::None,
-
- BNFormInputFieldType::TextLineFormField
- | BNFormInputFieldType::MultilineTextFormField
- | BNFormInputFieldType::OpenFileNameFormField
- | BNFormInputFieldType::SaveFileNameFormField
- | BNFormInputFieldType::DirectoryNameFormField => {
- FormResponses::String(unsafe {
- CStr::from_ptr(form_field.stringResult)
- .to_str()
- .unwrap()
- .to_owned()
- })
- }
-
- BNFormInputFieldType::IntegerFormField => {
- FormResponses::Integer(form_field.intResult)
- }
- BNFormInputFieldType::AddressFormField => {
- FormResponses::Address(form_field.addressResult)
- }
- BNFormInputFieldType::ChoiceFormField => {
- FormResponses::Index(form_field.indexResult)
- }
- })
- .collect();
- unsafe { BNFreeFormInputResults(self.fields.as_mut_ptr(), self.fields.len()) };
- result
- } else {
- vec![]
- }
- }
-}
-
-impl Default for FormInputBuilder {
- fn default() -> Self {
- Self::new()
- }
-}
-
struct TaskContext<F: Fn(Box<dyn Fn(usize, usize) -> Result<(), ()>>)>(F);
pub fn run_progress_dialog<F: Fn(Box<dyn Fn(usize, usize) -> Result<(), ()>>)>(
diff --git a/rust/src/interaction/form.rs b/rust/src/interaction/form.rs
new file mode 100644
index 00000000..06799681
--- /dev/null
+++ b/rust/src/interaction/form.rs
@@ -0,0 +1,411 @@
+use std::ffi::c_char;
+
+use binaryninjacore_sys::*;
+
+use crate::binary_view::BinaryView;
+use crate::rc::{Array, Ref};
+use crate::string::{raw_to_string, strings_to_string_list, BnString, IntoCStr};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Form {
+ pub title: String,
+ pub fields: Vec<FormInputField>,
+}
+
+impl Form {
+ pub fn new(title: impl Into<String>) -> Self {
+ Self::new_with_fields(title, vec![])
+ }
+
+ pub fn new_with_fields(title: impl Into<String>, fields: Vec<FormInputField>) -> Self {
+ Self {
+ title: title.into(),
+ fields,
+ }
+ }
+
+ pub fn add_field(&mut self, field: FormInputField) {
+ self.fields.push(field);
+ }
+
+ /// Prompt the user (or interaction handler) for the form input.
+ ///
+ /// Updates the field's values and returns whether the form was accepted or not.
+ pub fn prompt(&mut self) -> bool {
+ let title = self.title.clone().to_cstr();
+ let mut raw_fields = self
+ .fields
+ .iter()
+ .map(FormInputField::into_raw)
+ .collect::<Vec<_>>();
+ let success =
+ unsafe { BNGetFormInput(raw_fields.as_mut_ptr(), raw_fields.len(), title.as_ptr()) };
+ // Update the fields with the new field values.
+ self.fields = raw_fields
+ .into_iter()
+ .map(FormInputField::from_owned_raw)
+ .collect();
+ success
+ }
+
+ pub fn get_field_with_name(&self, name: &str) -> Option<&FormInputField> {
+ self.fields
+ .iter()
+ .find(|field| field.try_prompt() == Some(name.to_string()))
+ }
+}
+
+/// A field within a form.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum FormInputField {
+ Label {
+ prompt: String,
+ },
+ Separator,
+ TextLine {
+ prompt: String,
+ default: Option<String>,
+ value: Option<String>,
+ },
+ MultilineText {
+ prompt: String,
+ default: Option<String>,
+ value: Option<String>,
+ },
+ Integer {
+ prompt: String,
+ default: Option<i64>,
+ value: i64,
+ },
+ Address {
+ prompt: String,
+ view: Option<Ref<BinaryView>>,
+ current_address: u64,
+ default: Option<u64>,
+ value: u64,
+ },
+ Choice {
+ prompt: String,
+ choices: Vec<String>,
+ default: Option<usize>,
+ value: usize,
+ },
+ OpenFileName {
+ prompt: String,
+ /// File extension to filter on.
+ extension: Option<String>,
+ default: Option<String>,
+ value: Option<String>,
+ },
+ SaveFileName {
+ prompt: String,
+ /// File extension to filter on.
+ extension: Option<String>,
+ /// Default file name to fill.
+ default_name: Option<String>,
+ default: Option<String>,
+ value: Option<String>,
+ },
+ DirectoryName {
+ prompt: String,
+ default_name: Option<String>,
+ default: Option<String>,
+ value: Option<String>,
+ },
+}
+
+impl FormInputField {
+ pub fn from_raw(value: &BNFormInputField) -> Self {
+ let prompt = raw_to_string(value.prompt).unwrap_or_default();
+ let view = match value.view.is_null() {
+ false => Some(unsafe { BinaryView::from_raw(value.view) }.to_owned()),
+ true => None,
+ };
+ let name_default = value
+ .hasDefault
+ .then_some(raw_to_string(value.defaultName).unwrap_or_default());
+ let string_default = value
+ .hasDefault
+ .then_some(raw_to_string(value.stringDefault).unwrap_or_default());
+ let int_default = value.hasDefault.then_some(value.intDefault);
+ let address_default = value.hasDefault.then_some(value.addressDefault);
+ let index_default = value.hasDefault.then_some(value.indexDefault);
+ let extension = raw_to_string(value.ext);
+ let current_address = value.currentAddress;
+ let string_result = raw_to_string(value.stringResult);
+ let int_result = value.intResult;
+ let address_result = value.addressResult;
+ let index_result = value.indexResult;
+ match value.type_ {
+ BNFormInputFieldType::LabelFormField => Self::Label { prompt },
+ BNFormInputFieldType::SeparatorFormField => Self::Separator,
+ BNFormInputFieldType::TextLineFormField => Self::TextLine {
+ prompt,
+ default: string_default,
+ value: string_result,
+ },
+ BNFormInputFieldType::MultilineTextFormField => Self::MultilineText {
+ prompt,
+ default: string_default,
+ value: string_result,
+ },
+ BNFormInputFieldType::IntegerFormField => Self::Integer {
+ prompt,
+ default: int_default,
+ value: int_result,
+ },
+ BNFormInputFieldType::AddressFormField => Self::Address {
+ prompt,
+ view,
+ current_address,
+ default: address_default,
+ value: address_result,
+ },
+ BNFormInputFieldType::ChoiceFormField => Self::Choice {
+ prompt,
+ choices: vec![],
+ default: index_default,
+ value: index_result,
+ },
+ BNFormInputFieldType::OpenFileNameFormField => Self::OpenFileName {
+ prompt,
+ extension,
+ default: string_default,
+ value: string_result,
+ },
+ BNFormInputFieldType::SaveFileNameFormField => Self::SaveFileName {
+ prompt,
+ extension,
+ default_name: name_default,
+ default: string_default,
+ value: string_result,
+ },
+ BNFormInputFieldType::DirectoryNameFormField => Self::DirectoryName {
+ prompt,
+ default_name: name_default,
+ default: string_default,
+ value: string_result,
+ },
+ }
+ }
+
+ pub fn from_owned_raw(value: BNFormInputField) -> Self {
+ let owned = Self::from_raw(&value);
+ Self::free_raw(value);
+ owned
+ }
+
+ pub fn into_raw(&self) -> BNFormInputField {
+ let bn_prompt = BnString::new(self.try_prompt().unwrap_or_default());
+ let bn_extension = BnString::new(self.try_extension().unwrap_or_default());
+ let bn_default_string = BnString::new(self.try_default_string().unwrap_or_default());
+ let bn_default_name = BnString::new(self.try_default_name().unwrap_or_default());
+ let bn_value_string = BnString::new(self.try_value_string().unwrap_or_default());
+ // Expected to be freed by [`FormInputField::free_raw`].
+ BNFormInputField {
+ type_: self.as_type(),
+ prompt: BnString::into_raw(bn_prompt),
+ view: match self.try_view() {
+ None => std::ptr::null_mut(),
+ Some(view) => unsafe { Ref::into_raw(view) }.handle,
+ },
+ currentAddress: self.try_current_address().unwrap_or_default(),
+ choices: match self.try_choices() {
+ None => std::ptr::null_mut(),
+ Some(choices) => strings_to_string_list(choices.as_slice()) as *mut *const c_char,
+ },
+ // NOTE: `count` is the length of the `choices` array.
+ count: self.try_choices().unwrap_or_default().len(),
+ ext: BnString::into_raw(bn_extension),
+ defaultName: BnString::into_raw(bn_default_name),
+ intResult: self.try_value_int().unwrap_or_default(),
+ addressResult: self.try_value_address().unwrap_or_default(),
+ stringResult: BnString::into_raw(bn_value_string),
+ indexResult: self.try_value_index().unwrap_or_default(),
+ hasDefault: self.try_has_default().unwrap_or_default(),
+ intDefault: self.try_default_int().unwrap_or_default(),
+ addressDefault: self.try_default_address().unwrap_or_default(),
+ stringDefault: BnString::into_raw(bn_default_string),
+ indexDefault: self.try_default_index().unwrap_or_default(),
+ }
+ }
+
+ pub fn free_raw(value: BNFormInputField) {
+ unsafe {
+ BnString::free_raw(value.defaultName as *mut c_char);
+ BnString::free_raw(value.prompt as *mut c_char);
+ BnString::free_raw(value.ext as *mut c_char);
+ BnString::free_raw(value.stringDefault as *mut c_char);
+ BnString::free_raw(value.stringResult);
+ // TODO: Would like access to a `Array::free_raw` or something.
+ Array::<BnString>::new(value.choices as *mut *mut c_char, value.count, ());
+ // Free the view ref if provided.
+ if !value.view.is_null() {
+ BinaryView::ref_from_raw(value.view);
+ }
+ }
+ }
+
+ pub fn as_type(&self) -> BNFormInputFieldType {
+ match self {
+ FormInputField::Label { .. } => BNFormInputFieldType::LabelFormField,
+ FormInputField::Separator => BNFormInputFieldType::SeparatorFormField,
+ FormInputField::TextLine { .. } => BNFormInputFieldType::TextLineFormField,
+ FormInputField::MultilineText { .. } => BNFormInputFieldType::MultilineTextFormField,
+ FormInputField::Integer { .. } => BNFormInputFieldType::IntegerFormField,
+ FormInputField::Address { .. } => BNFormInputFieldType::AddressFormField,
+ FormInputField::Choice { .. } => BNFormInputFieldType::ChoiceFormField,
+ FormInputField::OpenFileName { .. } => BNFormInputFieldType::OpenFileNameFormField,
+ FormInputField::SaveFileName { .. } => BNFormInputFieldType::SaveFileNameFormField,
+ FormInputField::DirectoryName { .. } => BNFormInputFieldType::DirectoryNameFormField,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::prompt`] field.
+ pub fn try_prompt(&self) -> Option<String> {
+ match self {
+ FormInputField::Label { prompt, .. } => Some(prompt.clone()),
+ FormInputField::Separator => None,
+ FormInputField::TextLine { prompt, .. } => Some(prompt.clone()),
+ FormInputField::MultilineText { prompt, .. } => Some(prompt.clone()),
+ FormInputField::Integer { prompt, .. } => Some(prompt.clone()),
+ FormInputField::Address { prompt, .. } => Some(prompt.clone()),
+ FormInputField::Choice { prompt, .. } => Some(prompt.clone()),
+ FormInputField::OpenFileName { prompt, .. } => Some(prompt.clone()),
+ FormInputField::SaveFileName { prompt, .. } => Some(prompt.clone()),
+ FormInputField::DirectoryName { prompt, .. } => Some(prompt.clone()),
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::view`] field.
+ pub fn try_view(&self) -> Option<Ref<BinaryView>> {
+ match self {
+ FormInputField::Address { view, .. } => view.clone(),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::currentAddress`] field.
+ pub fn try_current_address(&self) -> Option<u64> {
+ match self {
+ FormInputField::Address {
+ current_address, ..
+ } => Some(*current_address),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::choices`] field.
+ pub fn try_choices(&self) -> Option<Vec<String>> {
+ match self {
+ FormInputField::Choice { choices, .. } => Some(choices.clone()),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::ext`] field.
+ pub fn try_extension(&self) -> Option<String> {
+ match self {
+ Self::SaveFileName { extension, .. } => extension.clone(),
+ Self::OpenFileName { extension, .. } => extension.clone(),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::hasDefault`] field.
+ pub fn try_has_default(&self) -> Option<bool> {
+ match self {
+ FormInputField::Label { .. } => None,
+ FormInputField::Separator => None,
+ FormInputField::TextLine { default, .. } => Some(default.is_some()),
+ FormInputField::MultilineText { default, .. } => Some(default.is_some()),
+ FormInputField::Integer { default, .. } => Some(default.is_some()),
+ FormInputField::Address { default, .. } => Some(default.is_some()),
+ FormInputField::Choice { default, .. } => Some(default.is_some()),
+ FormInputField::OpenFileName { default, .. } => Some(default.is_some()),
+ FormInputField::SaveFileName { default, .. } => Some(default.is_some()),
+ FormInputField::DirectoryName { default, .. } => Some(default.is_some()),
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::defaultName`] field.
+ pub fn try_default_name(&self) -> Option<String> {
+ match self {
+ Self::SaveFileName { default_name, .. } => default_name.clone(),
+ Self::DirectoryName { default_name, .. } => default_name.clone(),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::intDefault`] field.
+ pub fn try_default_int(&self) -> Option<i64> {
+ match self {
+ FormInputField::Integer { default, .. } => *default,
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::addressDefault`] field.
+ pub fn try_default_address(&self) -> Option<u64> {
+ match self {
+ FormInputField::Address { default, .. } => *default,
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::stringDefault`] field.
+ pub fn try_default_string(&self) -> Option<String> {
+ match self {
+ FormInputField::TextLine { default, .. } => default.clone(),
+ FormInputField::MultilineText { default, .. } => default.clone(),
+ FormInputField::OpenFileName { default, .. } => default.clone(),
+ FormInputField::SaveFileName { default, .. } => default.clone(),
+ FormInputField::DirectoryName { default, .. } => default.clone(),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::indexDefault`] field.
+ pub fn try_default_index(&self) -> Option<usize> {
+ match self {
+ FormInputField::Choice { default, .. } => *default,
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::intResult`] field.
+ pub fn try_value_int(&self) -> Option<i64> {
+ match self {
+ FormInputField::Integer { value, .. } => Some(*value),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::addressResult`] field.
+ pub fn try_value_address(&self) -> Option<u64> {
+ match self {
+ FormInputField::Address { value, .. } => Some(*value),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::stringResult`] field.
+ pub fn try_value_string(&self) -> Option<String> {
+ match self {
+ FormInputField::TextLine { value, .. } => value.clone(),
+ FormInputField::MultilineText { value, .. } => value.clone(),
+ FormInputField::OpenFileName { value, .. } => value.clone(),
+ FormInputField::SaveFileName { value, .. } => value.clone(),
+ FormInputField::DirectoryName { value, .. } => value.clone(),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::indexResult`] field.
+ pub fn try_value_index(&self) -> Option<usize> {
+ match self {
+ FormInputField::Choice { value, .. } => Some(*value),
+ _ => None,
+ }
+ }
+}
diff --git a/rust/src/interaction/handler.rs b/rust/src/interaction/handler.rs
new file mode 100644
index 00000000..fa1681d1
--- /dev/null
+++ b/rust/src/interaction/handler.rs
@@ -0,0 +1,598 @@
+use std::ffi::{c_char, c_void, CStr};
+use std::ptr;
+
+use binaryninjacore_sys::*;
+
+use crate::binary_view::BinaryView;
+use crate::flowgraph::FlowGraph;
+use crate::interaction::form::{Form, FormInputField};
+use crate::interaction::report::{Report, ReportCollection};
+use crate::interaction::{MessageBoxButtonResult, MessageBoxButtonSet, MessageBoxIcon};
+use crate::string::{raw_to_string, BnString};
+
+pub fn register_interaction_handler<R: InteractionHandler>(custom: R) {
+ let leak_custom = Box::leak(Box::new(custom));
+ let mut callbacks = BNInteractionHandlerCallbacks {
+ context: leak_custom as *mut R as *mut c_void,
+ showPlainTextReport: Some(cb_show_plain_text_report::<R>),
+ showMarkdownReport: Some(cb_show_markdown_report::<R>),
+ showHTMLReport: Some(cb_show_html_report::<R>),
+ showGraphReport: Some(cb_show_graph_report::<R>),
+ showReportCollection: Some(cb_show_report_collection::<R>),
+ getTextLineInput: Some(cb_get_text_line_input::<R>),
+ getIntegerInput: Some(cb_get_integer_input::<R>),
+ getAddressInput: Some(cb_get_address_input::<R>),
+ getChoiceInput: Some(cb_get_choice_input::<R>),
+ getLargeChoiceInput: Some(cb_get_large_choice_input::<R>),
+ getOpenFileNameInput: Some(cb_get_open_file_name_input::<R>),
+ getSaveFileNameInput: Some(cb_get_save_file_name_input::<R>),
+ getDirectoryNameInput: Some(cb_get_directory_name_input::<R>),
+ getFormInput: Some(cb_get_form_input::<R>),
+ showMessageBox: Some(cb_show_message_box::<R>),
+ openUrl: Some(cb_open_url::<R>),
+ runProgressDialog: Some(cb_run_progress_dialog::<R>),
+ };
+ unsafe { BNRegisterInteractionHandler(&mut callbacks) }
+}
+
+pub trait InteractionHandler: Sync + Send + 'static {
+ fn show_message_box(
+ &mut self,
+ title: &str,
+ text: &str,
+ buttons: MessageBoxButtonSet,
+ icon: MessageBoxIcon,
+ ) -> MessageBoxButtonResult;
+
+ fn open_url(&mut self, url: &str) -> bool;
+
+ fn run_progress_dialog(
+ &mut self,
+ title: &str,
+ can_cancel: bool,
+ task: &InteractionHandlerTask,
+ ) -> bool;
+
+ fn show_plain_text_report(&mut self, view: &BinaryView, title: &str, contents: &str);
+
+ fn show_graph_report(&mut self, view: &BinaryView, title: &str, graph: &FlowGraph);
+
+ fn show_markdown_report(
+ &mut self,
+ view: &BinaryView,
+ title: &str,
+ _contents: &str,
+ plain_text: &str,
+ ) {
+ self.show_plain_text_report(view, title, plain_text);
+ }
+
+ fn show_html_report(
+ &mut self,
+ view: &BinaryView,
+ title: &str,
+ _contents: &str,
+ plain_text: &str,
+ ) {
+ self.show_plain_text_report(view, title, plain_text);
+ }
+
+ fn show_report_collection(&mut self, _title: &str, reports: &ReportCollection) {
+ for report in reports {
+ match &report {
+ Report::PlainText(rpt) => {
+ self.show_plain_text_report(&report.view(), &report.title(), &rpt.contents())
+ }
+ Report::Markdown(rm) => self.show_markdown_report(
+ &report.view(),
+ &report.title(),
+ &rm.contents(),
+ &rm.plaintext(),
+ ),
+ Report::Html(rh) => self.show_html_report(
+ &report.view(),
+ &report.title(),
+ &rh.contents(),
+ &rh.plaintext(),
+ ),
+ Report::FlowGraph(rfg) => {
+ self.show_graph_report(&report.view(), &report.title(), &rfg.flow_graph())
+ }
+ }
+ }
+ }
+
+ fn get_form_input(&mut self, form: &mut Form) -> bool;
+
+ fn get_text_line_input(&mut self, prompt: &str, title: &str) -> Option<String> {
+ let mut form = Form::new(title.to_owned());
+ form.add_field(FormInputField::TextLine {
+ prompt: prompt.to_string(),
+ default: None,
+ value: None,
+ });
+ if !self.get_form_input(&mut form) {
+ return None;
+ }
+ form.get_field_with_name(prompt)
+ .and_then(|f| f.try_value_string())
+ }
+
+ fn get_integer_input(&mut self, prompt: &str, title: &str) -> Option<i64> {
+ let mut form = Form::new(title.to_owned());
+ form.add_field(FormInputField::Integer {
+ prompt: prompt.to_string(),
+ value: 0,
+ default: None,
+ });
+ if !self.get_form_input(&mut form) {
+ return None;
+ }
+ form.get_field_with_name(prompt)
+ .and_then(|f| f.try_value_int())
+ }
+
+ fn get_address_input(
+ &mut self,
+ prompt: &str,
+ title: &str,
+ view: Option<&BinaryView>,
+ current_addr: u64,
+ ) -> Option<u64> {
+ let mut form = Form::new(title.to_owned());
+ form.add_field(FormInputField::Address {
+ prompt: prompt.to_string(),
+ view: view.map(|v| v.to_owned()),
+ current_address: current_addr,
+ value: 0,
+ default: None,
+ });
+ if !self.get_form_input(&mut form) {
+ return None;
+ }
+ form.get_field_with_name(prompt)
+ .and_then(|f| f.try_value_address())
+ }
+
+ fn get_choice_input(
+ &mut self,
+ prompt: &str,
+ title: &str,
+ choices: Vec<String>,
+ ) -> Option<usize> {
+ let mut form = Form::new(title.to_owned());
+ form.add_field(FormInputField::Choice {
+ prompt: prompt.to_string(),
+ choices,
+ default: None,
+ value: 0,
+ });
+ if !self.get_form_input(&mut form) {
+ return None;
+ }
+ form.get_field_with_name(prompt)
+ .and_then(|f| f.try_value_index())
+ }
+
+ fn get_large_choice_input(
+ &mut self,
+ prompt: &str,
+ title: &str,
+ choices: Vec<String>,
+ ) -> Option<usize> {
+ self.get_choice_input(prompt, title, choices)
+ }
+
+ fn get_open_file_name_input(
+ &mut self,
+ prompt: &str,
+ extension: Option<String>,
+ ) -> Option<String> {
+ let mut form = Form::new(prompt.to_owned());
+ form.add_field(FormInputField::OpenFileName {
+ prompt: prompt.to_string(),
+ default: None,
+ value: None,
+ extension,
+ });
+ if !self.get_form_input(&mut form) {
+ return None;
+ }
+ form.get_field_with_name(prompt)
+ .and_then(|f| f.try_value_string())
+ }
+
+ fn get_save_file_name_input(
+ &mut self,
+ prompt: &str,
+ extension: Option<String>,
+ default_name: Option<String>,
+ ) -> Option<String> {
+ let mut form = Form::new(prompt.to_owned());
+ form.add_field(FormInputField::SaveFileName {
+ prompt: prompt.to_string(),
+ extension,
+ default: None,
+ value: None,
+ default_name,
+ });
+ if !self.get_form_input(&mut form) {
+ return None;
+ }
+ form.get_field_with_name(prompt)
+ .and_then(|f| f.try_value_string())
+ }
+
+ fn get_directory_name_input(
+ &mut self,
+ prompt: &str,
+ default_name: Option<String>,
+ ) -> Option<String> {
+ let mut form = Form::new(prompt.to_owned());
+ form.add_field(FormInputField::DirectoryName {
+ prompt: prompt.to_string(),
+ default_name,
+ default: None,
+ value: None,
+ });
+ if !self.get_form_input(&mut form) {
+ return None;
+ }
+ form.get_field_with_name(prompt)
+ .and_then(|f| f.try_value_string())
+ }
+}
+
+pub struct InteractionHandlerTask {
+ ctxt: *mut c_void,
+ task: Option<
+ unsafe extern "C" fn(
+ taskCtxt: *mut c_void,
+ progress: Option<
+ unsafe extern "C" fn(progressCtxt: *mut c_void, cur: usize, max: usize) -> bool,
+ >,
+ progressCtxt: *mut c_void,
+ ),
+ >,
+}
+
+impl InteractionHandlerTask {
+ pub fn task<P: FnMut(usize, usize) -> bool>(&mut self, progress: &mut P) {
+ let Some(task) = self.task else {
+ // Assuming a nullptr task mean nothing need to be done
+ return;
+ };
+
+ let progress_ctxt = progress as *mut P as *mut c_void;
+ ffi_wrap!("custom_interation_run_progress_dialog", unsafe {
+ task(
+ self.ctxt,
+ Some(cb_custom_interation_handler_task::<P>),
+ progress_ctxt,
+ )
+ })
+ }
+}
+
+unsafe extern "C" fn cb_custom_interation_handler_task<P: FnMut(usize, usize) -> bool>(
+ ctxt: *mut c_void,
+ cur: usize,
+ max: usize,
+) -> bool {
+ let ctxt = ctxt as *mut P;
+ (*ctxt)(cur, max)
+}
+
+unsafe extern "C" fn cb_show_plain_text_report<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ view: *mut BNBinaryView,
+ title: *const c_char,
+ contents: *const c_char,
+) {
+ let ctxt = ctxt as *mut R;
+ let title = raw_to_string(title).unwrap();
+ let contents = raw_to_string(contents).unwrap();
+ (*ctxt).show_plain_text_report(&BinaryView::from_raw(view), &title, &contents)
+}
+
+unsafe extern "C" fn cb_show_markdown_report<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ view: *mut BNBinaryView,
+ title: *const c_char,
+ contents: *const c_char,
+ plaintext: *const c_char,
+) {
+ let ctxt = ctxt as *mut R;
+ let title = raw_to_string(title).unwrap();
+ let contents = raw_to_string(contents).unwrap();
+ let plaintext = raw_to_string(plaintext).unwrap();
+ (*ctxt).show_markdown_report(&BinaryView::from_raw(view), &title, &contents, &plaintext)
+}
+
+unsafe extern "C" fn cb_show_html_report<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ view: *mut BNBinaryView,
+ title: *const c_char,
+ contents: *const c_char,
+ plaintext: *const c_char,
+) {
+ let ctxt = ctxt as *mut R;
+ let title = raw_to_string(title).unwrap();
+ let contents = raw_to_string(contents).unwrap();
+ let plaintext = raw_to_string(plaintext).unwrap();
+ (*ctxt).show_html_report(&BinaryView::from_raw(view), &title, &contents, &plaintext)
+}
+
+unsafe extern "C" fn cb_show_graph_report<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ view: *mut BNBinaryView,
+ title: *const c_char,
+ graph: *mut BNFlowGraph,
+) {
+ let ctxt = ctxt as *mut R;
+ let title = raw_to_string(title).unwrap();
+ (*ctxt).show_graph_report(
+ &BinaryView::from_raw(view),
+ &title,
+ &FlowGraph::from_raw(graph),
+ )
+}
+
+unsafe extern "C" fn cb_show_report_collection<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ title: *const c_char,
+ report: *mut BNReportCollection,
+) {
+ let ctxt = ctxt as *mut R;
+ let title = raw_to_string(title).unwrap();
+ (*ctxt).show_report_collection(
+ &title,
+ &ReportCollection::from_raw(ptr::NonNull::new(report).unwrap()),
+ )
+}
+
+unsafe extern "C" fn cb_get_text_line_input<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ result_ffi: *mut *mut c_char,
+ prompt: *const c_char,
+ title: *const c_char,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let prompt = raw_to_string(prompt).unwrap();
+ let title = raw_to_string(title).unwrap();
+ let result = (*ctxt).get_text_line_input(&prompt, &title);
+ if let Some(result) = result {
+ unsafe { *result_ffi = BnString::into_raw(BnString::new(result)) };
+ true
+ } else {
+ unsafe { *result_ffi = ptr::null_mut() };
+ false
+ }
+}
+
+unsafe extern "C" fn cb_get_integer_input<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ result_ffi: *mut i64,
+ prompt: *const c_char,
+ title: *const c_char,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let prompt = raw_to_string(prompt).unwrap();
+ let title = raw_to_string(title).unwrap();
+ let result = (*ctxt).get_integer_input(&prompt, &title);
+ if let Some(result) = result {
+ unsafe { *result_ffi = result };
+ true
+ } else {
+ unsafe { *result_ffi = 0 };
+ false
+ }
+}
+
+unsafe extern "C" fn cb_get_address_input<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ result_ffi: *mut u64,
+ prompt: *const c_char,
+ title: *const c_char,
+ view: *mut BNBinaryView,
+ current_addr: u64,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let prompt = raw_to_string(prompt).unwrap();
+ let title = raw_to_string(title).unwrap();
+ let view = (!view.is_null()).then(|| BinaryView::from_raw(view));
+ let result = (*ctxt).get_address_input(&prompt, &title, view.as_ref(), current_addr);
+ if let Some(result) = result {
+ unsafe { *result_ffi = result };
+ true
+ } else {
+ unsafe { *result_ffi = 0 };
+ false
+ }
+}
+
+unsafe extern "C" fn cb_get_choice_input<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ result_ffi: *mut usize,
+ prompt: *const c_char,
+ title: *const c_char,
+ choices: *mut *const c_char,
+ count: usize,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let prompt = raw_to_string(prompt).unwrap();
+ let title = raw_to_string(title).unwrap();
+ let choices = unsafe { core::slice::from_raw_parts(choices, count) };
+ // SAFETY: BnString and *const c_char are transparent
+ let choices = unsafe { core::mem::transmute::<&[*const c_char], &[BnString]>(choices) };
+ let choices: Vec<String> = choices
+ .iter()
+ .map(|x| x.to_string_lossy().to_string())
+ .collect();
+ let result = (*ctxt).get_choice_input(&prompt, &title, choices);
+ if let Some(result) = result {
+ unsafe { *result_ffi = result };
+ true
+ } else {
+ unsafe { *result_ffi = 0 };
+ false
+ }
+}
+
+unsafe extern "C" fn cb_get_large_choice_input<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ result_ffi: *mut usize,
+ prompt: *const c_char,
+ title: *const c_char,
+ choices: *mut *const c_char,
+ count: usize,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let prompt = raw_to_string(prompt).unwrap();
+ let title = raw_to_string(title).unwrap();
+ let choices = unsafe { core::slice::from_raw_parts(choices, count) };
+ // SAFETY: BnString and *const c_char are transparent
+ let choices = unsafe { core::mem::transmute::<&[*const c_char], &[BnString]>(choices) };
+ let choices: Vec<String> = choices
+ .iter()
+ .map(|x| x.to_string_lossy().to_string())
+ .collect();
+ let result = (*ctxt).get_large_choice_input(&prompt, &title, choices);
+ if let Some(result) = result {
+ unsafe { *result_ffi = result };
+ true
+ } else {
+ unsafe { *result_ffi = 0 };
+ false
+ }
+}
+
+unsafe extern "C" fn cb_get_open_file_name_input<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ result_ffi: *mut *mut c_char,
+ prompt: *const c_char,
+ ext: *const c_char,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let prompt = raw_to_string(prompt).unwrap();
+ let ext = (!ext.is_null()).then(|| unsafe { CStr::from_ptr(ext) });
+ let result =
+ (*ctxt).get_open_file_name_input(&prompt, ext.map(|x| x.to_string_lossy().to_string()));
+ if let Some(result) = result {
+ unsafe { *result_ffi = BnString::into_raw(BnString::new(result)) };
+ true
+ } else {
+ unsafe { *result_ffi = ptr::null_mut() };
+ false
+ }
+}
+
+unsafe extern "C" fn cb_get_save_file_name_input<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ result_ffi: *mut *mut c_char,
+ prompt: *const c_char,
+ ext: *const c_char,
+ default_name: *const c_char,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let prompt = raw_to_string(prompt).unwrap();
+ let ext = raw_to_string(ext);
+ let default_name = raw_to_string(default_name);
+ let result = (*ctxt).get_save_file_name_input(&prompt, ext, default_name);
+ if let Some(result) = result {
+ unsafe { *result_ffi = BnString::into_raw(BnString::new(result)) };
+ true
+ } else {
+ unsafe { *result_ffi = ptr::null_mut() };
+ false
+ }
+}
+
+unsafe extern "C" fn cb_get_directory_name_input<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ result_ffi: *mut *mut c_char,
+ prompt: *const c_char,
+ default_name: *const c_char,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let prompt = raw_to_string(prompt).unwrap();
+ let default_name = raw_to_string(default_name);
+ let result = (*ctxt).get_directory_name_input(&prompt, default_name);
+ if let Some(result) = result {
+ unsafe { *result_ffi = BnString::into_raw(BnString::new(result)) };
+ true
+ } else {
+ unsafe { *result_ffi = ptr::null_mut() };
+ false
+ }
+}
+
+unsafe extern "C" fn cb_get_form_input<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ fields: *mut BNFormInputField,
+ count: usize,
+ title: *const c_char,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let raw_fields = unsafe { core::slice::from_raw_parts_mut(fields, count) };
+ let fields: Vec<_> = raw_fields
+ .iter_mut()
+ .map(|x| FormInputField::from_raw(x))
+ .collect();
+ let title = raw_to_string(title).unwrap();
+ let mut form = Form::new_with_fields(title, fields);
+ let results = (*ctxt).get_form_input(&mut form);
+ // Update the fields with the new values. Freeing the old ones.
+ raw_fields
+ .iter_mut()
+ .enumerate()
+ .for_each(|(idx, raw_field)| {
+ FormInputField::free_raw(*raw_field);
+ *raw_field = form.fields[idx].into_raw();
+ });
+ results
+}
+
+unsafe extern "C" fn cb_show_message_box<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ title: *const c_char,
+ text: *const c_char,
+ buttons: BNMessageBoxButtonSet,
+ icon: BNMessageBoxIcon,
+) -> BNMessageBoxButtonResult {
+ let ctxt = ctxt as *mut R;
+ let title = raw_to_string(title).unwrap();
+ let text = raw_to_string(text).unwrap();
+ (*ctxt).show_message_box(&title, &text, buttons, icon)
+}
+
+unsafe extern "C" fn cb_open_url<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ url: *const c_char,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let url = raw_to_string(url).unwrap();
+ (*ctxt).open_url(&url)
+}
+
+unsafe extern "C" fn cb_run_progress_dialog<R: InteractionHandler>(
+ ctxt: *mut c_void,
+ title: *const c_char,
+ can_cancel: bool,
+ task: Option<
+ unsafe extern "C" fn(
+ *mut c_void,
+ Option<unsafe extern "C" fn(*mut c_void, usize, usize) -> bool>,
+ *mut c_void,
+ ),
+ >,
+ task_ctxt: *mut c_void,
+) -> bool {
+ let ctxt = ctxt as *mut R;
+ let title = raw_to_string(title).unwrap();
+ let task = InteractionHandlerTask {
+ ctxt: task_ctxt,
+ task,
+ };
+ (*ctxt).run_progress_dialog(&title, can_cancel, &task)
+}
diff --git a/rust/src/interaction/report.rs b/rust/src/interaction/report.rs
new file mode 100644
index 00000000..a20d8f1f
--- /dev/null
+++ b/rust/src/interaction/report.rs
@@ -0,0 +1,311 @@
+use std::ffi::c_char;
+use std::ptr::NonNull;
+
+use binaryninjacore_sys::*;
+
+use crate::binary_view::BinaryView;
+use crate::flowgraph::FlowGraph;
+use crate::rc::{Ref, RefCountable};
+use crate::string::{BnString, IntoCStr};
+
+pub type ReportType = BNReportType;
+
+#[repr(transparent)]
+pub struct ReportCollection {
+ handle: NonNull<BNReportCollection>,
+}
+
+impl ReportCollection {
+ pub(crate) unsafe fn from_raw(handle: NonNull<BNReportCollection>) -> Self {
+ Self { handle }
+ }
+
+ pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNReportCollection>) -> Ref<Self> {
+ unsafe { Ref::new(Self { handle }) }
+ }
+
+ pub fn new() -> Ref<Self> {
+ let raw = unsafe { BNCreateReportCollection() };
+ unsafe { Self::ref_from_raw(NonNull::new(raw).unwrap()) }
+ }
+
+ pub fn show(&self, title: &str) {
+ let title = title.to_cstr();
+ unsafe { BNShowReportCollection(title.as_ptr(), self.handle.as_ptr()) }
+ }
+
+ pub fn count(&self) -> usize {
+ unsafe { BNGetReportCollectionCount(self.handle.as_ptr()) }
+ }
+
+ fn report_type(&self, i: usize) -> ReportType {
+ unsafe { BNGetReportType(self.handle.as_ptr(), i) }
+ }
+
+ pub fn get(&self, i: usize) -> Report<'_> {
+ Report::new(self, i)
+ }
+
+ fn view(&self, i: usize) -> Ref<BinaryView> {
+ let raw = unsafe { BNGetReportView(self.handle.as_ptr(), i) };
+ unsafe { BinaryView::ref_from_raw(raw) }
+ }
+
+ fn title(&self, i: usize) -> String {
+ let raw = unsafe { BNGetReportTitle(self.handle.as_ptr(), i) };
+ unsafe { BnString::into_string(raw) }
+ }
+
+ fn contents(&self, i: usize) -> String {
+ let raw = unsafe { BNGetReportContents(self.handle.as_ptr(), i) };
+ unsafe { BnString::into_string(raw) }
+ }
+
+ fn plain_text(&self, i: usize) -> String {
+ let raw = unsafe { BNGetReportPlainText(self.handle.as_ptr(), i) };
+ unsafe { BnString::into_string(raw) }
+ }
+
+ fn flow_graph(&self, i: usize) -> Ref<FlowGraph> {
+ let raw = unsafe { BNGetReportFlowGraph(self.handle.as_ptr(), i) };
+ unsafe { FlowGraph::ref_from_raw(raw) }
+ }
+
+ pub fn add_text(&self, view: &BinaryView, title: &str, contents: &str) {
+ let title = title.to_cstr();
+ let contents = contents.to_cstr();
+ unsafe {
+ BNAddPlainTextReportToCollection(
+ self.handle.as_ptr(),
+ view.handle,
+ title.as_ref().as_ptr() as *const c_char,
+ contents.as_ref().as_ptr() as *const c_char,
+ )
+ }
+ }
+
+ pub fn add_markdown(&self, view: &BinaryView, title: &str, contents: &str, plaintext: &str) {
+ let title = title.to_cstr();
+ let contents = contents.to_cstr();
+ let plaintext = plaintext.to_cstr();
+ unsafe {
+ BNAddMarkdownReportToCollection(
+ self.handle.as_ptr(),
+ view.handle,
+ title.as_ref().as_ptr() as *const c_char,
+ contents.as_ref().as_ptr() as *const c_char,
+ plaintext.as_ref().as_ptr() as *const c_char,
+ )
+ }
+ }
+
+ pub fn add_html(&self, view: &BinaryView, title: &str, contents: &str, plaintext: &str) {
+ let title = title.to_cstr();
+ let contents = contents.to_cstr();
+ let plaintext = plaintext.to_cstr();
+ unsafe {
+ BNAddHTMLReportToCollection(
+ self.handle.as_ptr(),
+ view.handle,
+ title.as_ref().as_ptr() as *const c_char,
+ contents.as_ref().as_ptr() as *const c_char,
+ plaintext.as_ref().as_ptr() as *const c_char,
+ )
+ }
+ }
+
+ pub fn add_graph(&self, view: &BinaryView, title: &str, graph: &FlowGraph) {
+ let title = title.to_cstr();
+ unsafe {
+ BNAddGraphReportToCollection(
+ self.handle.as_ptr(),
+ view.handle,
+ title.as_ref().as_ptr() as *const c_char,
+ graph.handle,
+ )
+ }
+ }
+
+ fn update_report_flow_graph(&self, i: usize, graph: &FlowGraph) {
+ unsafe { BNUpdateReportFlowGraph(self.handle.as_ptr(), i, graph.handle) }
+ }
+
+ pub fn iter(&self) -> ReportCollectionIter<'_> {
+ ReportCollectionIter::new(self)
+ }
+}
+
+unsafe impl RefCountable for ReportCollection {
+ unsafe fn inc_ref(handle: &Self) -> Ref<Self> {
+ let raw = unsafe { BNNewReportCollectionReference(handle.handle.as_ptr()) };
+ unsafe { Self::ref_from_raw(NonNull::new(raw).unwrap()) }
+ }
+
+ unsafe fn dec_ref(handle: &Self) {
+ unsafe { BNFreeReportCollection(handle.handle.as_ptr()) }
+ }
+}
+
+impl ToOwned for ReportCollection {
+ type Owned = Ref<Self>;
+
+ fn to_owned(&self) -> Self::Owned {
+ unsafe { RefCountable::inc_ref(self) }
+ }
+}
+
+impl<'a> IntoIterator for &'a ReportCollection {
+ type Item = Report<'a>;
+ type IntoIter = ReportCollectionIter<'a>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+pub enum Report<'a> {
+ PlainText(ReportPlainText<'a>),
+ Markdown(ReportMarkdown<'a>),
+ Html(ReportHtml<'a>),
+ FlowGraph(ReportFlowGraph<'a>),
+}
+
+impl<'a> Report<'a> {
+ fn new(collection: &'a ReportCollection, index: usize) -> Self {
+ let inner = ReportInner { collection, index };
+ match inner.type_() {
+ ReportType::PlainTextReportType => Report::PlainText(ReportPlainText(inner)),
+ ReportType::MarkdownReportType => Report::Markdown(ReportMarkdown(inner)),
+ ReportType::HTMLReportType => Report::Html(ReportHtml(inner)),
+ ReportType::FlowGraphReportType => Report::FlowGraph(ReportFlowGraph(inner)),
+ }
+ }
+
+ fn _inner(&self) -> &ReportInner<'a> {
+ match self {
+ Report::PlainText(ReportPlainText(x))
+ | Report::Markdown(ReportMarkdown(x))
+ | Report::Html(ReportHtml(x))
+ | Report::FlowGraph(ReportFlowGraph(x)) => x,
+ }
+ }
+
+ pub fn view(&self) -> Ref<BinaryView> {
+ self._inner().view()
+ }
+
+ pub fn title(&self) -> String {
+ self._inner().title()
+ }
+}
+
+pub struct ReportPlainText<'a>(ReportInner<'a>);
+
+impl ReportPlainText<'_> {
+ pub fn contents(&self) -> String {
+ self.0.contents()
+ }
+}
+
+pub struct ReportMarkdown<'a>(ReportInner<'a>);
+
+impl ReportMarkdown<'_> {
+ pub fn contents(&self) -> String {
+ self.0.contents()
+ }
+
+ pub fn plaintext(&self) -> String {
+ self.0.plain_text()
+ }
+}
+
+pub struct ReportHtml<'a>(ReportInner<'a>);
+
+impl ReportHtml<'_> {
+ pub fn contents(&self) -> String {
+ self.0.contents()
+ }
+
+ pub fn plaintext(&self) -> String {
+ self.0.plain_text()
+ }
+}
+
+pub struct ReportFlowGraph<'a>(ReportInner<'a>);
+
+impl ReportFlowGraph<'_> {
+ pub fn flow_graph(&self) -> Ref<FlowGraph> {
+ self.0.flow_graph()
+ }
+
+ pub fn update_report_flow_graph(&self, graph: &FlowGraph) {
+ self.0.update_report_flow_graph(graph)
+ }
+}
+
+struct ReportInner<'a> {
+ collection: &'a ReportCollection,
+ index: usize,
+}
+
+impl ReportInner<'_> {
+ fn type_(&self) -> ReportType {
+ self.collection.report_type(self.index)
+ }
+
+ fn view(&self) -> Ref<BinaryView> {
+ self.collection.view(self.index)
+ }
+
+ fn title(&self) -> String {
+ self.collection.title(self.index)
+ }
+
+ fn contents(&self) -> String {
+ self.collection.contents(self.index)
+ }
+
+ fn plain_text(&self) -> String {
+ self.collection.plain_text(self.index)
+ }
+
+ fn flow_graph(&self) -> Ref<FlowGraph> {
+ self.collection.flow_graph(self.index)
+ }
+
+ fn update_report_flow_graph(&self, graph: &FlowGraph) {
+ self.collection.update_report_flow_graph(self.index, graph)
+ }
+}
+
+pub struct ReportCollectionIter<'a> {
+ report: &'a ReportCollection,
+ current_index: usize,
+ count: usize,
+}
+
+impl<'a> ReportCollectionIter<'a> {
+ pub fn new(report: &'a ReportCollection) -> Self {
+ Self {
+ report,
+ current_index: 0,
+ count: report.count(),
+ }
+ }
+
+ pub fn collection(&self) -> &ReportCollection {
+ self.report
+ }
+}
+
+impl<'a> Iterator for ReportCollectionIter<'a> {
+ type Item = Report<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ (self.current_index < self.count).then(|| {
+ let result = Report::new(self.report, self.current_index);
+ self.current_index += 1;
+ result
+ })
+ }
+}
diff --git a/rust/src/language_representation.rs b/rust/src/language_representation.rs
index 50e7b710..536203fe 100644
--- a/rust/src/language_representation.rs
+++ b/rust/src/language_representation.rs
@@ -13,7 +13,7 @@ use crate::high_level_il::token_emitter::HighLevelILTokenEmitter;
use crate::high_level_il::{HighLevelILFunction, HighLevelInstructionIndex};
use crate::line_formatter::CoreLineFormatter;
use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Ref, RefCountable};
-use crate::string::{BnStrCompatible, BnString};
+use crate::string::{BnString, IntoCStr};
use crate::type_parser::CoreTypeParser;
use crate::type_printer::CoreTypePrinter;
@@ -27,10 +27,9 @@ pub type SymbolDisplayResult = BNSymbolDisplayResult;
pub fn register_language_representation_function_type<
C: LanguageRepresentationFunctionType,
F: FnOnce(CoreLanguageRepresentationFunctionType) -> C,
- B: BnStrCompatible,
>(
creator: F,
- name: B,
+ name: &str,
) -> CoreLanguageRepresentationFunctionType {
let custom = Box::leak(Box::new(MaybeUninit::uninit()));
let mut callbacks = BNCustomLanguageRepresentationFunctionType {
@@ -43,13 +42,9 @@ pub fn register_language_representation_function_type<
getFunctionTypeTokens: Some(cb_get_function_type_tokens::<C>),
freeLines: Some(cb_free_lines),
};
- let name = name.into_bytes_with_nul();
- let core = unsafe {
- BNRegisterLanguageRepresentationFunctionType(
- name.as_ref().as_ptr() as *const c_char,
- &mut callbacks,
- )
- };
+ let name = name.to_cstr();
+ let core =
+ unsafe { BNRegisterLanguageRepresentationFunctionType(name.as_ptr(), &mut callbacks) };
let core =
unsafe { CoreLanguageRepresentationFunctionType::from_raw(NonNull::new(core).unwrap()) };
custom.write(creator(core));
@@ -138,11 +133,9 @@ impl CoreLanguageRepresentationFunctionType {
self.handle.as_ptr()
}
- pub fn from_name<S: BnStrCompatible>(name: S) -> Option<Self> {
- let name = name.into_bytes_with_nul();
- let result = unsafe {
- BNGetLanguageRepresentationFunctionTypeByName(name.as_ref().as_ptr() as *const c_char)
- };
+ pub fn from_name(name: &str) -> Option<Self> {
+ let name = name.to_cstr();
+ let result = unsafe { BNGetLanguageRepresentationFunctionTypeByName(name.as_ptr()) };
NonNull::new(result).map(|handle| unsafe { Self::from_raw(handle) })
}
diff --git a/rust/src/line_formatter.rs b/rust/src/line_formatter.rs
index 5dc07c5c..50f1900c 100644
--- a/rust/src/line_formatter.rs
+++ b/rust/src/line_formatter.rs
@@ -1,4 +1,4 @@
-use std::ffi::{c_char, c_void};
+use std::ffi::c_void;
use std::ptr::NonNull;
use binaryninjacore_sys::*;
@@ -6,22 +6,18 @@ use binaryninjacore_sys::*;
use crate::disassembly::DisassemblyTextLine;
use crate::high_level_il::HighLevelILFunction;
use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Ref};
-use crate::string::{raw_to_string, BnStrCompatible, BnString};
+use crate::string::{raw_to_string, BnString, IntoCStr};
/// Register a [`LineFormatter`] with the API.
-pub fn register_line_formatter<C: LineFormatter, B: BnStrCompatible>(
- name: B,
- formatter: C,
-) -> CoreLineFormatter {
+pub fn register_line_formatter<C: LineFormatter>(name: &str, formatter: C) -> CoreLineFormatter {
let custom = Box::leak(Box::new(formatter));
let mut callbacks = BNCustomLineFormatter {
context: custom as *mut C as *mut c_void,
formatLines: Some(cb_format_lines::<C>),
freeLines: Some(cb_free_lines),
};
- let name = name.into_bytes_with_nul();
- let handle =
- unsafe { BNRegisterLineFormatter(name.as_ref().as_ptr() as *const c_char, &mut callbacks) };
+ let name = name.to_cstr();
+ let handle = unsafe { BNRegisterLineFormatter(name.as_ptr(), &mut callbacks) };
CoreLineFormatter::from_raw(NonNull::new(handle).unwrap())
}
@@ -54,10 +50,9 @@ impl CoreLineFormatter {
unsafe { Array::new(result, count, ()) }
}
- pub fn from_name<S: BnStrCompatible>(name: S) -> Option<CoreLineFormatter> {
- let name_raw = name.into_bytes_with_nul();
- let result =
- unsafe { BNGetLineFormatterByName(name_raw.as_ref().as_ptr() as *const c_char) };
+ pub fn from_name(name: &str) -> Option<CoreLineFormatter> {
+ let name_raw = name.to_cstr();
+ let result = unsafe { BNGetLineFormatterByName(name_raw.as_ptr()) };
NonNull::new(result).map(Self::from_raw)
}
diff --git a/rust/src/string.rs b/rust/src/string.rs
index a670320b..97c31270 100644
--- a/rust/src/string.rs
+++ b/rust/src/string.rs
@@ -34,12 +34,16 @@ pub(crate) fn raw_to_string(ptr: *const c_char) -> Option<String> {
}
}
-// TODO: Make this pass in an iterator over something more generic...
-pub(crate) fn strings_to_string_list(strings: &[String]) -> *mut *mut c_char {
+pub(crate) fn strings_to_string_list<I, S>(strings: I) -> *mut *mut c_char
+where
+ I: IntoIterator<Item = S>,
+ // TODO make `S: BnStrCompatible,`
+ S: AsRef<str>,
+{
use binaryninjacore_sys::BNAllocStringList;
let bn_str_list = strings
- .iter()
- .map(|s| BnString::new(s.as_str()))
+ .into_iter()
+ .map(|s| BnString::new(s.as_ref()))
.collect::<Vec<_>>();
let mut raw_str_list = bn_str_list.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
unsafe { BNAllocStringList(raw_str_list.as_mut_ptr(), raw_str_list.len()) }
diff --git a/rust/tests/interaction.rs b/rust/tests/interaction.rs
new file mode 100644
index 00000000..eea906ec
--- /dev/null
+++ b/rust/tests/interaction.rs
@@ -0,0 +1,169 @@
+use std::path::PathBuf;
+
+use binaryninja::binary_view::BinaryView;
+use binaryninja::flowgraph::FlowGraph;
+use binaryninja::headless::Session;
+use binaryninja::interaction::form::{Form, FormInputField};
+use binaryninja::interaction::handler::{
+ register_interaction_handler, InteractionHandler, InteractionHandlerTask,
+};
+use binaryninja::interaction::report::{Report, ReportCollection};
+use binaryninja::interaction::{MessageBoxButtonResult, MessageBoxButtonSet, MessageBoxIcon};
+
+struct MyInteractionHandler;
+
+impl InteractionHandler for MyInteractionHandler {
+ fn show_message_box(
+ &mut self,
+ _title: &str,
+ _text: &str,
+ _buttons: MessageBoxButtonSet,
+ _icon: MessageBoxIcon,
+ ) -> MessageBoxButtonResult {
+ todo!()
+ }
+
+ fn open_url(&mut self, _url: &str) -> bool {
+ todo!()
+ }
+
+ fn run_progress_dialog(
+ &mut self,
+ _title: &str,
+ _can_cancel: bool,
+ _task: &InteractionHandlerTask,
+ ) -> bool {
+ todo!()
+ }
+
+ fn show_plain_text_report(&mut self, _view: &BinaryView, _title: &str, _contents: &str) {
+ todo!()
+ }
+
+ fn show_graph_report(&mut self, _view: &BinaryView, _title: &str, _graph: &FlowGraph) {
+ todo!()
+ }
+
+ fn show_report_collection(&mut self, title: &str, reports: &ReportCollection) {
+ assert_eq!(title, "show_report_collection_title");
+ for (i, report) in reports.iter().enumerate() {
+ assert_eq!(report.title(), format!("title_report_{i}"));
+ match (i, report) {
+ (0, Report::PlainText(x)) => {
+ assert_eq!(x.contents().as_str(), "contents");
+ }
+ (1, Report::Markdown(x)) => {
+ assert_eq!(x.contents().as_str(), "# contents");
+ assert_eq!(x.plaintext().as_str(), "markdown_plain_text");
+ }
+ (2, Report::Html(x)) => {
+ assert_eq!(x.contents().as_str(), "<html>contents</html>");
+ assert_eq!(x.plaintext().as_str(), "html_plain_text");
+ }
+ (3, Report::FlowGraph(x)) => {
+ assert_eq!(x.flow_graph().get_node_count(), 0);
+ }
+ _ => unreachable!(),
+ }
+ }
+ }
+
+ fn get_form_input(&mut self, form: &mut Form) -> bool {
+ if form.fields.len() != 1 {
+ return false;
+ }
+
+ match &mut form.fields[0] {
+ FormInputField::Integer { ref mut value, .. } => {
+ *value = 1337;
+ true
+ }
+ FormInputField::Address {
+ ref mut value,
+ default,
+ ..
+ } => {
+ *value = default.unwrap_or(0) + 0x10;
+ true
+ }
+ FormInputField::DirectoryName {
+ ref mut value,
+ default,
+ default_name,
+ ..
+ } => {
+ let new_value = format!(
+ "example{}{}",
+ default.clone().unwrap_or_default(),
+ default_name.clone().unwrap_or_default()
+ );
+ *value = Some(new_value);
+ true
+ }
+ _ => false,
+ }
+ }
+}
+
+#[test]
+fn test_get_integer() {
+ register_interaction_handler(MyInteractionHandler {});
+ let output = binaryninja::interaction::get_integer_input("get_int", "get_int_prompt");
+ assert_eq!(output, Some(1337));
+}
+
+#[test]
+fn test_get_directory() {
+ register_interaction_handler(MyInteractionHandler {});
+ let output = binaryninja::interaction::get_directory_name_input("get_dir", "");
+ assert_eq!(
+ output.as_ref().map(|x| x.to_str().unwrap()),
+ Some("example")
+ );
+}
+
+#[test]
+fn test_get_directory_default() {
+ register_interaction_handler(MyInteractionHandler {});
+
+ let mut my_form = Form::new("get_dir_default");
+ my_form.add_field(FormInputField::DirectoryName {
+ prompt: "get_dir_default".to_string(),
+ default_name: Some("_default_name".to_string()),
+ default: Some("_default".to_string()),
+ value: None,
+ });
+
+ assert_eq!(my_form.prompt(), true);
+ assert_eq!(
+ my_form.fields[0].try_value_string(),
+ Some("example_default_default_name".to_string())
+ )
+}
+
+#[test]
+fn test_get_address() {
+ register_interaction_handler(MyInteractionHandler {});
+ let output = binaryninja::interaction::get_address_input("address", "Address Prompt");
+ assert_eq!(output, Some(0x10));
+}
+
+#[test]
+fn test_show_report_collection() {
+ let _session = Session::new().expect("Failed to initialize session");
+ let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap();
+ let view = binaryninja::load(out_dir.join("atox.obj")).expect("Failed to create view");
+
+ register_interaction_handler(MyInteractionHandler {});
+ let collection = ReportCollection::new();
+ collection.add_text(&view, "title_report_0", "contents");
+ collection.add_markdown(&view, "title_report_1", "# contents", "markdown_plain_text");
+ collection.add_html(
+ &view,
+ "title_report_2",
+ "<html>contents</html>",
+ "html_plain_text",
+ );
+ collection.add_graph(&view, "title_report_3", &FlowGraph::new());
+ collection.show("show_report_collection_title");
+}