diff options
| -rw-r--r-- | plugins/dwarf/dwarf_export/src/lib.rs | 180 | ||||
| -rw-r--r-- | rust/src/binary_view.rs | 40 | ||||
| -rw-r--r-- | rust/src/function.rs | 16 | ||||
| -rw-r--r-- | rust/src/interaction.rs | 433 | ||||
| -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 | ||||
| -rw-r--r-- | rust/src/language_representation.rs | 23 | ||||
| -rw-r--r-- | rust/src/line_formatter.rs | 21 | ||||
| -rw-r--r-- | rust/src/string.rs | 12 | ||||
| -rw-r--r-- | rust/tests/interaction.rs | 169 |
11 files changed, 1695 insertions, 519 deletions
diff --git a/plugins/dwarf/dwarf_export/src/lib.rs b/plugins/dwarf/dwarf_export/src/lib.rs index d1754d29..283a60ab 100644 --- a/plugins/dwarf/dwarf_export/src/lib.rs +++ b/plugins/dwarf/dwarf_export/src/lib.rs @@ -1,27 +1,26 @@ mod edit_distance; -use gimli::{ - constants, - write::{ - Address, AttributeValue, DwarfUnit, EndianVec, Expression, Range, RangeList, Sections, - UnitEntryId, - }, -}; -use object::{write, Architecture, BinaryFormat, SectionKind}; -use std::fs; - +use binaryninja::interaction::form::{Form, FormInputField}; use binaryninja::logger::Logger; use binaryninja::{ binary_view::{BinaryView, BinaryViewBase, BinaryViewExt}, command::{register_command, Command}, confidence::Conf, - interaction, - interaction::{FormResponses, FormResponses::Index}, rc::Ref, symbol::SymbolType, types::{MemberAccess, StructureType, Type, TypeClass}, }; +use gimli::{ + constants, + write::{ + Address, AttributeValue, DwarfUnit, EndianVec, Expression, Range, RangeList, Sections, + UnitEntryId, + }, +}; use log::{error, info, LevelFilter}; +use object::{write, Architecture, BinaryFormat, SectionKind}; +use std::fs; +use std::path::{Path, PathBuf}; fn export_type( name: String, @@ -598,10 +597,11 @@ fn export_data_vars( } } -fn present_form(bv_arch: &str) -> Vec<FormResponses> { - // TODO : Verify inputs (like save location) so that we can fail early - // TODO : Add Language field - // TODO : Choose to export types/functions/etc +// TODO : Verify inputs (like save location) so that we can fail early +// TODO : Add Language field +// TODO : Choose to export types/functions/etc +// TODO: Add actual / better support for formats other than elf? +fn create_export_form(bv_arch: &str) -> Form { let archs = [ "Unknown", "AArch64", @@ -627,80 +627,37 @@ fn present_form(bv_arch: &str) -> Vec<FormResponses> { "Wasm32", "Xtensa", ]; - interaction::FormInputBuilder::new() - .save_file_field( - "Save Location", - Some("Debug Files (*.dwo *.debug);;All Files (*)"), - None, - None, - ) - .choice_field( - "Architecture", - &archs, - archs - .iter() - .enumerate() - .min_by(|&(_, arch_name_1), &(_, arch_name_2)| { - edit_distance::distance(bv_arch, arch_name_1) - .cmp(&edit_distance::distance(bv_arch, arch_name_2)) - }) - .map(|(index, _)| index), - ) - // Add actual / better support for formats other than elf? - // .choice_field( - // "Container Format", - // &["Coff", "Elf", "MachO", "Pe", "Wasm", "Xcoff"], - // None, - // ) - .get_form_input("Export as DWARF") + + let mut form = Form::new("Export as DWARF"); + form.add_field(FormInputField::SaveFileName { + prompt: "Save Location".to_string(), + extension: Some("Debug Files (*.dwo *.debug);;All Files (*)".to_string()), + default_name: None, + default: None, + value: None, + }); + form.add_field(FormInputField::Choice { + prompt: "Architecture".to_string(), + choices: archs.iter().map(|arch| arch.to_string()).collect(), + default: archs + .iter() + .enumerate() + .min_by(|&(_, arch_name_1), &(_, arch_name_2)| { + edit_distance::distance(bv_arch, arch_name_1) + .cmp(&edit_distance::distance(bv_arch, arch_name_2)) + }) + .map(|(index, _)| index), + value: 0, + }); + form } fn write_dwarf<T: gimli::Endianity>( - responses: Vec<FormResponses>, + file_path: &Path, + arch: Architecture, endian: T, dwarf: &mut DwarfUnit, ) { - if responses.len() < 2 { - return; - } - - let arch = match responses[1] { - Index(0) => Architecture::Unknown, - Index(1) => Architecture::Aarch64, - Index(2) => Architecture::Aarch64_Ilp32, - Index(3) => Architecture::Arm, - Index(4) => Architecture::Avr, - Index(5) => Architecture::Bpf, - Index(6) => Architecture::I386, - Index(7) => Architecture::X86_64, - Index(8) => Architecture::X86_64_X32, - Index(9) => Architecture::Hexagon, - Index(10) => Architecture::LoongArch64, - Index(11) => Architecture::Mips, - Index(12) => Architecture::Mips64, - Index(13) => Architecture::Msp430, - Index(14) => Architecture::PowerPc, - Index(15) => Architecture::PowerPc64, - Index(16) => Architecture::Riscv32, - Index(17) => Architecture::Riscv64, - Index(18) => Architecture::S390x, - Index(19) => Architecture::Sbf, - Index(20) => Architecture::Sparc64, - Index(21) => Architecture::Wasm32, - Index(22) => Architecture::Xtensa, - _ => Architecture::Unknown, - }; - - // let format = match responses[2] { - // Index(0) => BinaryFormat::Coff, - // Index(1) => BinaryFormat::Elf, - // Index(2) => BinaryFormat::MachO, - // Index(3) => BinaryFormat::Pe, - // Index(4) => BinaryFormat::Wasm, - // Index(5) => BinaryFormat::Xcoff, - // _ => BinaryFormat::Elf, - // }; - // TODO : Look in to other options (mangling, flags, etc (see Object::new)) let mut out_object = write::Object::new( BinaryFormat::Elf, @@ -738,16 +695,14 @@ fn write_dwarf<T: gimli::Endianity>( }) .unwrap(); - if let interaction::FormResponses::String(filename) = &responses[0] { - if let Ok(out_data) = out_object.write() { - if let Err(err) = fs::write(filename, out_data) { - error!("Failed to write DWARF file: {}", err); - } else { - info!("Successfully saved as DWARF to `{}`", filename); - } + if let Ok(out_data) = out_object.write() { + if let Err(err) = fs::write(file_path, out_data) { + error!("Failed to write DWARF file: {}", err); } else { - error!("Failed to write DWARF with requested settings"); + info!("Successfully saved as DWARF to `{:?}`", file_path); } + } else { + error!("Failed to write DWARF with requested settings"); } } @@ -757,7 +712,42 @@ fn export_dwarf(bv: &BinaryView) { } else { String::from("Unknown") }; - let responses = present_form(arch_name.as_str()); + let mut export_form = create_export_form(arch_name.as_str()); + if !export_form.prompt() { + return; + } + + let arch_field = export_form.get_field_with_name("Architecture").unwrap(); + let arch_field_idx = arch_field.try_value_index().unwrap_or_default(); + let arch = match arch_field_idx { + 0 => Architecture::Unknown, + 1 => Architecture::Aarch64, + 2 => Architecture::Aarch64_Ilp32, + 3 => Architecture::Arm, + 4 => Architecture::Avr, + 5 => Architecture::Bpf, + 6 => Architecture::I386, + 7 => Architecture::X86_64, + 8 => Architecture::X86_64_X32, + 9 => Architecture::Hexagon, + 10 => Architecture::LoongArch64, + 11 => Architecture::Mips, + 12 => Architecture::Mips64, + 13 => Architecture::Msp430, + 14 => Architecture::PowerPc, + 15 => Architecture::PowerPc64, + 16 => Architecture::Riscv32, + 17 => Architecture::Riscv64, + 18 => Architecture::S390x, + 19 => Architecture::Sbf, + 20 => Architecture::Sparc64, + 21 => Architecture::Wasm32, + 22 => Architecture::Xtensa, + _ => Architecture::Unknown, + }; + + let save_loc_field = export_form.get_field_with_name("Save Location").unwrap(); + let save_loc_path = PathBuf::from(save_loc_field.try_value_string().unwrap_or_default()); let encoding = gimli::Encoding { format: gimli::Format::Dwarf32, @@ -782,9 +772,9 @@ fn export_dwarf(bv: &BinaryView) { // TODO: Sections? Segments? if bv.default_endianness() == binaryninja::Endianness::LittleEndian { - write_dwarf(responses, gimli::LittleEndian, &mut dwarf); + write_dwarf(&save_loc_path, arch, gimli::LittleEndian, &mut dwarf); } else { - write_dwarf(responses, gimli::BigEndian, &mut dwarf); + write_dwarf(&save_loc_path, arch, gimli::BigEndian, &mut dwarf); }; } 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"); +} |
