diff options
| author | rbran <git@rubens.io> | 2025-04-23 19:24:45 +0000 |
|---|---|---|
| committer | Mason Reed <35282038+emesare@users.noreply.github.com> | 2025-05-12 17:45:24 -0400 |
| commit | c2d90f2e5a010546d0e1f1deeb1a6d8531ea4d4f (patch) | |
| tree | 20a6c6268943154fdda6a4d6884f2f29d9d4de50 /rust/src/interaction | |
| parent | 4a4a8488269922269b20a12a7b71272b925b9f18 (diff) | |
[Rust] Implement custom interactive handler
Diffstat (limited to 'rust/src/interaction')
| -rw-r--r-- | rust/src/interaction/form.rs | 411 | ||||
| -rw-r--r-- | rust/src/interaction/handler.rs | 598 | ||||
| -rw-r--r-- | rust/src/interaction/report.rs | 311 |
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 + }) + } +} |
