summaryrefslogtreecommitdiff
path: root/rust/src/interaction
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/src/interaction
parent4a4a8488269922269b20a12a7b71272b925b9f18 (diff)
[Rust] Implement custom interactive handler
Diffstat (limited to 'rust/src/interaction')
-rw-r--r--rust/src/interaction/form.rs411
-rw-r--r--rust/src/interaction/handler.rs598
-rw-r--r--rust/src/interaction/report.rs311
3 files changed, 1320 insertions, 0 deletions
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
+ })
+ }
+}