diff --git a/plugins/dwarf/dwarf_export/src/lib.rs b/plugins/dwarf/dwarf_export/src/lib.rs index bc159825f..577469b1a 100644 --- a/plugins/dwarf/dwarf_export/src/lib.rs +++ b/plugins/dwarf/dwarf_export/src/lib.rs @@ -628,14 +628,14 @@ fn present_form(bv_arch: &str) -> Vec { "Wasm32", "Xtensa", ]; - interaction::FormInputBuilder::new() - .save_file_field( + let mut form = [ + interaction::FormInput::save_file_field::<_, _, &str, &str>( "Save Location", Some("Debug Files (*.dwo *.debug);;All Files (*)"), None, None, - ) - .choice_field( + ), + interaction::FormInput::choice_field( "Architecture", &archs, archs @@ -646,14 +646,15 @@ fn present_form(bv_arch: &str) -> Vec { .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") + //interaction::FormInput::choice_field( + // "Container Format", + // &["Coff", "Elf", "MachO", "Pe", "Wasm", "Xcoff"], + // None, + //), + ]; + interaction::get_form_input("Export as DWARF", &mut form) } fn write_dwarf( diff --git a/rust/src/binary_view.rs b/rust/src/binary_view.rs index db4c92d74..c33758ce2 100644 --- a/rust/src/binary_view.rs +++ b/rust/src/binary_view.rs @@ -1109,6 +1109,60 @@ pub trait BinaryViewExt: BinaryViewBase { unsafe { BNApplyDebugInfo(self.as_ref().handle, debug_info.handle) } } + fn show_plaintext_report( + &self, + title: S1, + plaintext: S2, + ) { + let title = title.into_bytes_with_nul(); + let plaintext = plaintext.into_bytes_with_nul(); + 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: S1, + contents: S2, + plaintext: S3, + ) { + let title = title.into_bytes_with_nul(); + let contents = contents.into_bytes_with_nul(); + let plaintext = plaintext.into_bytes_with_nul(); + 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: S1, + contents: S2, + plaintext: S3, + ) { + let title = title.into_bytes_with_nul(); + let contents = contents.into_bytes_with_nul(); + let plaintext = plaintext.into_bytes_with_nul(); + 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: S, graph: &FlowGraph) { let raw_name = raw_name.into_bytes_with_nul(); unsafe { diff --git a/rust/src/interaction.rs b/rust/src/interaction.rs index 1546bc5ae..397670574 100644 --- a/rust/src/interaction.rs +++ b/rust/src/interaction.rs @@ -16,13 +16,31 @@ use binaryninjacore_sys::*; +use core::ffi; use std::ffi::{c_char, c_void, CStr}; use std::path::PathBuf; +use std::ptr; use crate::binary_view::BinaryView; -use crate::rc::Ref; +use crate::flowgraph::FlowGraph; +use crate::rc::{Array, Ref, RefCountable}; use crate::string::{BnStrCompatible, BnString}; +pub type ReportType = BNReportType; +pub type MessageBoxButtonSet = BNMessageBoxButtonSet; +pub type MessageBoxIcon = BNMessageBoxIcon; +pub type MessageBoxButtonResult = BNMessageBoxButtonResult; + +pub fn show_report_collection(title: B, reports: &ReportCollection) { + let title = title.into_bytes_with_nul(); + unsafe { + BNShowReportCollection( + title.as_ref().as_ptr() as *const ffi::c_char, + reports.as_raw(), + ) + } +} + pub fn get_text_line_input(prompt: &str, title: &str) -> Option { let mut value: *mut c_char = std::ptr::null_mut(); @@ -78,6 +96,56 @@ pub fn get_address_input(prompt: &str, title: &str) -> Option { Some(value) } +pub fn get_choice_input( + prompt: S1, + title: S2, + choices: &[&str], +) -> Option { + let prompt = prompt.into_bytes_with_nul(); + let title = title.into_bytes_with_nul(); + let mut choices_inner: Vec = choices.iter().copied().map(BnString::new).collect(); + // SAFETY BnString and *const ffi::c_char are transparent + let choices: &mut [*const ffi::c_char] = unsafe { + core::mem::transmute::<&mut [BnString], &mut [*const ffi::c_char]>(&mut choices_inner[..]) + }; + let mut result = 0; + let succ = unsafe { + BNGetChoiceInput( + &mut result, + prompt.as_ref().as_ptr() as *const ffi::c_char, + title.as_ref().as_ptr() as *const ffi::c_char, + choices.as_mut_ptr(), + choices.len(), + ) + }; + succ.then_some(result) +} + +pub fn get_large_choice_input( + prompt: S1, + title: S2, + choices: &[&str], +) -> Option { + let prompt = prompt.into_bytes_with_nul(); + let title = title.into_bytes_with_nul(); + let mut choices_inner: Vec = choices.iter().copied().map(BnString::new).collect(); + // SAFETY BnString and *const ffi::c_char are transparent + let choices: &mut [*const ffi::c_char] = unsafe { + core::mem::transmute::<&mut [BnString], &mut [*const ffi::c_char]>(&mut choices_inner[..]) + }; + let mut result = 0; + let succ = unsafe { + BNGetLargeChoiceInput( + &mut result, + prompt.as_ref().as_ptr() as *const ffi::c_char, + title.as_ref().as_ptr() as *const ffi::c_char, + choices.as_mut_ptr(), + choices.len(), + ) + }; + succ.then_some(result) +} + pub fn get_open_filename_input(prompt: &str, extension: &str) -> Option { let mut value: *mut c_char = std::ptr::null_mut(); @@ -137,9 +205,69 @@ pub fn get_directory_name_input(prompt: &str, default_name: &str) -> Option "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(title: &str, fields: &mut [FormInput]) -> Vec { + // SAFETY BNFormInputField and FormInputField are transparent + let succ = unsafe { + BNGetFormInput( + fields.as_mut_ptr() as *mut BNFormInputField, + fields.len(), + title.into_bytes_with_nul().as_ptr() as *const _, + ) + }; + // I'm assuming there is no need to drop the result if false is returned + if !succ { + return vec![]; + } + + let result = fields.iter().map(FormInput::result).collect(); + unsafe { BNFreeFormInputResults(fields.as_mut_ptr() as *mut BNFormInputField, fields.len()) }; + result +} + pub fn show_message_box( title: &str, text: &str, @@ -156,6 +284,7 @@ pub fn show_message_box( } } +#[derive(Debug, Clone)] pub enum FormResponses { None, String(String), @@ -164,427 +293,1397 @@ pub enum FormResponses { Index(usize), } -enum FormData { - Label { - _text: BnString, - }, - Text { - _prompt: BnString, - _default: Option, - }, - Choice { - _prompt: BnString, - _choices: Vec, - _raw: Vec<*const c_char>, - }, - File { - _prompt: BnString, - _ext: BnString, - _default: Option, - }, - FileSave { - _prompt: BnString, - _ext: BnString, - _default_name: BnString, - _default: Option, - }, -} - -pub struct FormInputBuilder { - fields: Vec, - data: Vec, -} - -impl FormInputBuilder { - pub fn new() -> Self { - Self { - fields: vec![], - data: vec![], - } +struct TaskContext Result<(), ()>>)>(F); + +pub fn run_progress_dialog Result<(), ()>>)>( + title: &str, + can_cancel: bool, + task: F, +) -> Result<(), ()> { + let mut ctxt = TaskContext::(task); + + unsafe extern "C" fn cb_task Result<(), ()>>)>( + ctxt: *mut c_void, + progress: Option bool>, + progress_ctxt: *mut c_void, + ) { + ffi_wrap!("run_progress_dialog", { + let context = ctxt as *mut TaskContext; + let progress_fn = Box::new(move |cur: usize, max: usize| -> Result<(), ()> { + match progress { + Some(func) => { + if (func)(progress_ctxt, cur, max) { + Ok(()) + } else { + Err(()) + } + } + None => Ok(()), + } + }); + ((*context).0)(progress_fn); + }) } - /// Form Field: Text output - pub fn label_field(mut self, text: &str) -> Self { - let text = BnString::new(text); + if unsafe { + BNRunProgressDialog( + title.into_bytes_with_nul().as_ptr() as *mut _, + can_cancel, + Some(cb_task::), + &mut ctxt as *mut _ as *mut c_void, + ) + } { + Ok(()) + } else { + Err(()) + } +} - let mut result = unsafe { std::mem::zeroed::() }; - result.type_ = BNFormInputFieldType::LabelFormField; - result.hasDefault = false; - result.prompt = text.as_ref().as_ptr() as *const c_char; - self.fields.push(result); +pub trait CustomInteractionHandler: Sync + Send + 'static { + fn show_plain_text_report(&mut self, view: &BinaryView, title: &str, contents: &str); + fn show_markdown_report( + &mut self, + view: &BinaryView, + title: &str, + contents: &str, + plaintext: &str, + ); + fn show_html_report(&mut self, view: &BinaryView, title: &str, contents: &str, plaintext: &str); + fn show_graph_report(&mut self, view: &BinaryView, title: &str, graph: &FlowGraph); + fn show_report_collection(&mut self, title: &str, reports: &ReportCollection); + fn get_text_line_input(&mut self, prompt: &str, title: &str) -> Option { + let mut result = + self.get_form_input(&[FormInput::text_field::<_, &str>(prompt, None)], title)?; + let [FormResponses::String(result)] = &mut result[..] else { + panic!("Invalid result from CustomIteractionHandler::get_form_input") + }; + Some(core::mem::take(result)) + } + fn get_integer_input(&mut self, prompt: &str, title: &str) -> Option { + let result = self.get_form_input(&[FormInput::integer_field(prompt, None)], title)?; + let [FormResponses::Integer(result)] = &result[..] else { + panic!("Invalid result from CustomIteractionHandler::get_form_input") + }; + Some(*result) + } + fn get_address_input( + &mut self, + prompt: &str, + title: &str, + view: Option<&BinaryView>, + current_addr: u64, + ) -> Option { + let result = self.get_form_input( + &[FormInput::address_field(prompt, view, current_addr, None)], + title, + )?; + let [FormResponses::Address(result)] = &result[..] else { + panic!("Invalid result from CustomIteractionHandler::get_form_input") + }; + Some(*result) + } + fn get_choice_input(&mut self, prompt: &str, title: &str, choices: &[&str]) -> Option { + let result = + self.get_form_input(&[FormInput::choice_field(prompt, choices, None)], title)?; + let [FormResponses::Index(result)] = &result[..] else { + panic!("Invalid result from CustomIteractionHandler::get_form_input") + }; + Some(*result) + } + fn get_large_choice_input( + &mut self, + prompt: &str, + title: &str, + choices: &[&str], + ) -> Option { + self.get_choice_input(prompt, title, choices) + } + fn get_open_file_name_input(&mut self, prompt: &str, ext: Option<&str>) -> Option { + let mut result = self.get_form_input( + &[FormInput::open_file_field::<_, _, &str>(prompt, ext, None)], + "Select a file", + )?; + let [FormResponses::String(result)] = &mut result[..] else { + panic!("Invalid result from CustomIteractionHandler::get_form_input") + }; + Some(core::mem::take(result)) + } + fn get_save_file_name_input( + &mut self, + prompt: &str, + ext: Option<&str>, + default_name: Option<&str>, + ) -> Option { + let mut result = self.get_form_input( + &[FormInput::save_file_field::<_, _, _, &str>( + prompt, + ext, + default_name, + None, + )], + "Select a file", + )?; + let [FormResponses::String(result)] = &mut result[..] else { + panic!("Invalid result from CustomIteractionHandler::get_form_input") + }; + Some(core::mem::take(result)) + } + fn get_directory_name_input( + &mut self, + prompt: &str, + default_name: Option<&str>, + ) -> Option { + let mut result = self.get_form_input( + &[FormInput::directory_name_field::<_, _, &str>( + prompt, + default_name, + None, + )], + "Select a directory", + )?; + let [FormResponses::String(result)] = &mut result[..] else { + panic!("Invalid result from CustomIteractionHandler::get_form_input") + }; + Some(core::mem::take(result)) + } + fn get_form_input(&mut self, fields: &[FormInput], title: &str) -> Option>; + 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: &CustomInterationHandlerTask, + ) -> bool; +} - self.data.push(FormData::Label { _text: text }); - self +pub fn register_custom_interaction_handler(custom: R) { + let leak_custom = Box::leak(Box::new(custom)); + let mut callbacks = BNInteractionHandlerCallbacks { + context: leak_custom as *mut R as *mut ffi::c_void, + showPlainTextReport: Some(show_plain_text_report_ffi::), + showMarkdownReport: Some(show_markdown_report_ffi::), + showHTMLReport: Some(show_html_report_ffi::), + showGraphReport: Some(show_graph_report_ffi::), + showReportCollection: Some(show_report_collection_ffi::), + getTextLineInput: Some(get_text_line_input_ffi::), + getIntegerInput: Some(get_integer_input_ffi::), + getAddressInput: Some(get_address_input_ffi::), + getChoiceInput: Some(get_choice_input_ffi::), + getLargeChoiceInput: Some(get_large_choice_input_ffi::), + getOpenFileNameInput: Some(get_open_file_name_input_ffi::), + getSaveFileNameInput: Some(get_save_file_name_input_ffi::), + getDirectoryNameInput: Some(get_directory_name_input_ffi::), + getFormInput: Some(get_form_input_ffi::), + showMessageBox: Some(show_message_box_ffi::), + openUrl: Some(open_url_ffi::), + runProgressDialog: Some(run_progress_dialog_ffi::), + }; + unsafe { BNRegisterInteractionHandler(&mut callbacks) } +} + +unsafe extern "C" fn show_plain_text_report_ffi( + ctxt: *mut ffi::c_void, + view: *mut BNBinaryView, + title: *const ffi::c_char, + contents: *const ffi::c_char, +) { + let ctxt = ctxt as *mut R; + let title = unsafe { CStr::from_ptr(title) }; + let contents = unsafe { CStr::from_ptr(contents) }; + (*ctxt).show_plain_text_report( + &BinaryView::from_raw(view), + title.to_str().unwrap(), + contents.to_str().unwrap(), + ) +} + +unsafe extern "C" fn show_markdown_report_ffi( + ctxt: *mut ffi::c_void, + view: *mut BNBinaryView, + title: *const ffi::c_char, + contents: *const ffi::c_char, + plaintext: *const ffi::c_char, +) { + let ctxt = ctxt as *mut R; + let title = unsafe { CStr::from_ptr(title) }; + let contents = unsafe { CStr::from_ptr(contents) }; + let plaintext = unsafe { CStr::from_ptr(plaintext) }; + (*ctxt).show_markdown_report( + &BinaryView::from_raw(view), + title.to_str().unwrap(), + contents.to_str().unwrap(), + plaintext.to_str().unwrap(), + ) +} + +unsafe extern "C" fn show_html_report_ffi( + ctxt: *mut ffi::c_void, + view: *mut BNBinaryView, + title: *const ffi::c_char, + contents: *const ffi::c_char, + plaintext: *const ffi::c_char, +) { + let ctxt = ctxt as *mut R; + let title = unsafe { CStr::from_ptr(title) }; + let contents = unsafe { CStr::from_ptr(contents) }; + let plaintext = unsafe { CStr::from_ptr(plaintext) }; + (*ctxt).show_html_report( + &BinaryView::from_raw(view), + title.to_str().unwrap(), + contents.to_str().unwrap(), + plaintext.to_str().unwrap(), + ) +} + +unsafe extern "C" fn show_graph_report_ffi( + ctxt: *mut ffi::c_void, + view: *mut BNBinaryView, + title: *const ffi::c_char, + graph: *mut BNFlowGraph, +) { + let ctxt = ctxt as *mut R; + let title = unsafe { CStr::from_ptr(title) }; + (*ctxt).show_graph_report( + &BinaryView::from_raw(view), + title.to_str().unwrap(), + &FlowGraph::from_raw(graph), + ) +} + +unsafe extern "C" fn show_report_collection_ffi( + ctxt: *mut ffi::c_void, + title: *const ffi::c_char, + report: *mut BNReportCollection, +) { + let ctxt = ctxt as *mut R; + let title = unsafe { CStr::from_ptr(title) }; + (*ctxt).show_report_collection( + title.to_str().unwrap(), + &ReportCollection::from_raw(ptr::NonNull::new(report).unwrap()), + ) +} + +unsafe extern "C" fn get_text_line_input_ffi( + ctxt: *mut ffi::c_void, + result_ffi: *mut *mut ffi::c_char, + prompt: *const ffi::c_char, + title: *const ffi::c_char, +) -> bool { + let ctxt = ctxt as *mut R; + let prompt = unsafe { CStr::from_ptr(prompt) }; + let title = unsafe { CStr::from_ptr(title) }; + let result = (*ctxt).get_text_line_input(prompt.to_str().unwrap(), title.to_str().unwrap()); + if let Some(result) = result { + unsafe { *result_ffi = BnString::into_raw(BnString::new(result)) }; + true + } else { + unsafe { *result_ffi = ptr::null_mut() }; + false } +} - /// Form Field: Vertical spacing - pub fn separator_field(mut self) -> Self { - let mut result = unsafe { std::mem::zeroed::() }; - result.type_ = BNFormInputFieldType::SeparatorFormField; - result.hasDefault = false; - self.fields.push(result); - self +unsafe extern "C" fn get_integer_input_ffi( + ctxt: *mut ffi::c_void, + result_ffi: *mut i64, + prompt: *const ffi::c_char, + title: *const ffi::c_char, +) -> bool { + let ctxt = ctxt as *mut R; + let prompt = unsafe { CStr::from_ptr(prompt) }; + let title = unsafe { CStr::from_ptr(title) }; + let result = (*ctxt).get_integer_input(prompt.to_str().unwrap(), title.to_str().unwrap()); + if let Some(result) = result { + unsafe { *result_ffi = result }; + true + } else { + unsafe { *result_ffi = 0 }; + false } +} - /// 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::() }; - result.type_ = BNFormInputFieldType::TextLineFormField; - result.prompt = prompt.as_ref().as_ptr() as *const c_char; - result.hasDefault = default.is_some(); - if let Some(ref default) = default { - result.stringDefault = default.as_ref().as_ptr() as *const c_char; +unsafe extern "C" fn get_address_input_ffi( + ctxt: *mut ffi::c_void, + result_ffi: *mut u64, + prompt: *const ffi::c_char, + title: *const ffi::c_char, + view: *mut BNBinaryView, + current_addr: u64, +) -> bool { + let ctxt = ctxt as *mut R; + let prompt = unsafe { CStr::from_ptr(prompt) }; + let title = unsafe { CStr::from_ptr(title) }; + let view = (!view.is_null()).then(|| BinaryView::from_raw(view)); + let result = (*ctxt).get_address_input( + prompt.to_str().unwrap(), + title.to_str().unwrap(), + 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 get_choice_input_ffi( + ctxt: *mut ffi::c_void, + result_ffi: *mut usize, + prompt: *const ffi::c_char, + title: *const ffi::c_char, + choices: *mut *const ffi::c_char, + count: usize, +) -> bool { + let ctxt = ctxt as *mut R; + let prompt = unsafe { CStr::from_ptr(prompt) }; + let title = unsafe { CStr::from_ptr(title) }; + let choices = unsafe { core::slice::from_raw_parts(choices, count) }; + // SAFETY: BnString and *const ffi::c_char are transparent + let choices = unsafe { core::mem::transmute::<&[*const ffi::c_char], &[BnString]>(choices) }; + let choices: Vec<&str> = choices.iter().map(|x| x.to_str().unwrap()).collect(); + let result = + (*ctxt).get_choice_input(prompt.to_str().unwrap(), title.to_str().unwrap(), &choices); + if let Some(result) = result { + unsafe { *result_ffi = result }; + true + } else { + unsafe { *result_ffi = 0 }; + false + } +} + +unsafe extern "C" fn get_large_choice_input_ffi( + ctxt: *mut ffi::c_void, + result_ffi: *mut usize, + prompt: *const ffi::c_char, + title: *const ffi::c_char, + choices: *mut *const ffi::c_char, + count: usize, +) -> bool { + let ctxt = ctxt as *mut R; + let prompt = unsafe { CStr::from_ptr(prompt) }; + let title = unsafe { CStr::from_ptr(title) }; + let choices = unsafe { core::slice::from_raw_parts(choices, count) }; + // SAFETY: BnString and *const ffi::c_char are transparent + let choices = unsafe { core::mem::transmute::<&[*const ffi::c_char], &[BnString]>(choices) }; + let choices: Vec<&str> = choices.iter().map(|x| x.to_str().unwrap()).collect(); + let result = + (*ctxt).get_large_choice_input(prompt.to_str().unwrap(), title.to_str().unwrap(), &choices); + if let Some(result) = result { + unsafe { *result_ffi = result }; + true + } else { + unsafe { *result_ffi = 0 }; + false + } +} + +unsafe extern "C" fn get_open_file_name_input_ffi( + ctxt: *mut ffi::c_void, + result_ffi: *mut *mut ffi::c_char, + prompt: *const ffi::c_char, + ext: *const ffi::c_char, +) -> bool { + let ctxt = ctxt as *mut R; + let prompt = unsafe { CStr::from_ptr(prompt) }; + let ext = (!ext.is_null()).then(|| unsafe { CStr::from_ptr(ext) }); + let result = (*ctxt) + .get_open_file_name_input(prompt.to_str().unwrap(), ext.map(|x| x.to_str().unwrap())); + 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 get_save_file_name_input_ffi( + ctxt: *mut ffi::c_void, + result_ffi: *mut *mut ffi::c_char, + prompt: *const ffi::c_char, + ext: *const ffi::c_char, + default_name: *const ffi::c_char, +) -> bool { + let ctxt = ctxt as *mut R; + let prompt = unsafe { CStr::from_ptr(prompt) }; + let ext = (!ext.is_null()).then(|| unsafe { CStr::from_ptr(ext) }); + let default_name = (!default_name.is_null()).then(|| unsafe { CStr::from_ptr(default_name) }); + let result = (*ctxt).get_save_file_name_input( + prompt.to_str().unwrap(), + ext.map(|x| x.to_str().unwrap()), + default_name.map(|x| x.to_str().unwrap()), + ); + 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 get_directory_name_input_ffi( + ctxt: *mut ffi::c_void, + result_ffi: *mut *mut ffi::c_char, + prompt: *const ffi::c_char, + default_name: *const ffi::c_char, +) -> bool { + let ctxt = ctxt as *mut R; + let prompt = unsafe { CStr::from_ptr(prompt) }; + let default_name = (!default_name.is_null()).then(|| unsafe { CStr::from_ptr(default_name) }); + let result = (*ctxt).get_directory_name_input( + prompt.to_str().unwrap(), + default_name.map(|x| x.to_str().unwrap()), + ); + 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 get_form_input_ffi( + ctxt: *mut ffi::c_void, + fields: *mut BNFormInputField, + count: usize, + title: *const ffi::c_char, +) -> bool { + let ctxt = ctxt as *mut R; + let fields = unsafe { core::slice::from_raw_parts_mut(fields, count) }; + // SAFETY BNFormInputField and FormInput are transparent + let fields = + unsafe { core::mem::transmute::<&mut [BNFormInputField], &mut [FormInput]>(fields) }; + let title = unsafe { CStr::from_ptr(title) }; + let results = (*ctxt).get_form_input(fields, title.to_str().unwrap()); + let Some(results) = results else { + return false; + }; + // modify the fields, so they include the results + for (field, result) in fields.iter_mut().zip(results) { + use BNFormInputFieldType::*; + use FormResponses::*; + match (field.0.type_, result) { + (LabelFormField, None) | (SeparatorFormField, None) => {} + (IntegerFormField, Integer(i)) => field.0.intResult = i, + (AddressFormField, Address(a)) => field.0.addressResult = a, + (ChoiceFormField, Index(idx)) => field.0.indexResult = idx, + (TextLineFormField, String(s)) + | (MultilineTextFormField, String(s)) + | (OpenFileNameFormField, String(s)) + | (SaveFileNameFormField, String(s)) + | (DirectoryNameFormField, String(s)) => { + field.0.stringResult = BnString::into_raw(BnString::new(s)) + } + (type_, result) => panic!("Unexpected result for type {type_:?} -> {result:?}"), } - self.fields.push(result); + } + true +} + +unsafe extern "C" fn show_message_box_ffi( + ctxt: *mut ffi::c_void, + title: *const ffi::c_char, + text: *const ffi::c_char, + buttons: BNMessageBoxButtonSet, + icon: BNMessageBoxIcon, +) -> BNMessageBoxButtonResult { + let ctxt = ctxt as *mut R; + let title = unsafe { CStr::from_ptr(title) }; + let text = unsafe { CStr::from_ptr(text) }; + (*ctxt).show_message_box( + title.to_str().unwrap(), + text.to_str().unwrap(), + buttons, + icon, + ) +} + +unsafe extern "C" fn open_url_ffi( + ctxt: *mut ffi::c_void, + url: *const ffi::c_char, +) -> bool { + let ctxt = ctxt as *mut R; + let url = unsafe { CStr::from_ptr(url) }; + (*ctxt).open_url(url.to_str().unwrap()) +} + +unsafe extern "C" fn run_progress_dialog_ffi( + ctxt: *mut ffi::c_void, + title: *const ffi::c_char, + can_cancel: bool, + task: Option< + unsafe extern "C" fn( + *mut ffi::c_void, + Option bool>, + *mut ffi::c_void, + ), + >, + task_ctxt: *mut ffi::c_void, +) -> bool { + let ctxt = ctxt as *mut R; + let title = unsafe { CStr::from_ptr(title) }; + let task = CustomInterationHandlerTask { + ctxt: task_ctxt, + task, + }; + (*ctxt).run_progress_dialog(title.to_str().unwrap(), can_cancel, &task) +} + +pub struct CustomInterationHandlerTask { + ctxt: *mut ffi::c_void, + task: Option< + unsafe extern "C" fn( + taskCtxt: *mut ffi::c_void, + progress: Option< + unsafe extern "C" fn( + progressCtxt: *mut ffi::c_void, + cur: usize, + max: usize, + ) -> bool, + >, + progressCtxt: *mut ffi::c_void, + ), + >, +} - self.data.push(FormData::Text { - _prompt: prompt, - _default: default, - }); - self +impl CustomInterationHandlerTask { + pub fn task 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 ffi::c_void; + ffi_wrap!("custom_interation_run_progress_dialog", unsafe { + task( + self.ctxt, + Some(custom_interation_handler_task_ffi::

), + progress_ctxt, + ) + }) } +} - /// 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::() }; - result.type_ = BNFormInputFieldType::MultilineTextFormField; - result.prompt = prompt.as_ref().as_ptr() as *const c_char; - result.hasDefault = default.is_some(); - if let Some(ref default) = default { - result.stringDefault = default.as_ref().as_ptr() as *const c_char; +unsafe extern "C" fn custom_interation_handler_task_ffi bool>( + ctxt: *mut ffi::c_void, + cur: usize, + max: usize, +) -> bool { + let ctxt = ctxt as *mut P; + (*ctxt)(cur, max) +} + +#[repr(transparent)] +pub struct ReportCollection { + handle: ptr::NonNull, +} + +unsafe impl RefCountable for ReportCollection { + unsafe fn inc_ref(handle: &Self) -> Ref { + let raw = unsafe { BNNewReportCollectionReference(handle.handle.as_ptr()) }; + unsafe { Self::ref_from_raw(ptr::NonNull::new(raw).unwrap()) } + } + + unsafe fn dec_ref(handle: &Self) { + unsafe { BNFreeReportCollection(handle.handle.as_ptr()) } + } +} + +impl ToOwned for ReportCollection { + type Owned = Ref; + + fn to_owned(&self) -> Self::Owned { + unsafe { ::inc_ref(self) } + } +} + +impl ReportCollection { + pub(crate) fn as_raw(&self) -> *mut BNReportCollection { + self.handle.as_ptr() + } + + pub(crate) unsafe fn from_raw(handle: ptr::NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: ptr::NonNull) -> Ref { + unsafe { Ref::new(Self { handle }) } + } + + pub fn new() -> Ref { + let raw = unsafe { BNCreateReportCollection() }; + unsafe { Self::ref_from_raw(ptr::NonNull::new(raw).unwrap()) } + } + + pub fn count(&self) -> usize { + unsafe { BNGetReportCollectionCount(self.as_raw()) } + } + + fn type_(&self, i: usize) -> ReportType { + unsafe { BNGetReportType(self.as_raw(), i) } + } + + pub fn get(&self, i: usize) -> Report<'_> { + Report::new(self, i) + } + + fn view(&self, i: usize) -> Ref { + // TODO is this owned? or just a reference? + let raw = unsafe { BNGetReportView(self.as_raw(), i) }; + unsafe { BinaryView::ref_from_raw(raw) } + } + + fn title(&self, i: usize) -> BnString { + // TODO is this owned? or just a reference? Assuming owned because it + // returns a `*mut ffi::c_char` + let raw = unsafe { BNGetReportTitle(self.as_raw(), i) }; + unsafe { BnString::from_raw(raw) } + } + + fn contents(&self, i: usize) -> BnString { + // TODO is this owned? or just a reference? Assuming owned because it + // returns a `*mut ffi::c_char` + let raw = unsafe { BNGetReportContents(self.as_raw(), i) }; + unsafe { BnString::from_raw(raw) } + } + + fn plain_text(&self, i: usize) -> BnString { + // TODO is this owned? or just a reference? Assuming owned because it + // returns a `*mut ffi::c_char` + let raw = unsafe { BNGetReportPlainText(self.as_raw(), i) }; + unsafe { BnString::from_raw(raw) } + } + + fn flow_graph(&self, i: usize) -> Ref { + // TODO is this owned? or just a reference? + let raw = unsafe { BNGetReportFlowGraph(self.as_raw(), i) }; + unsafe { FlowGraph::ref_from_raw(raw) } + } + + pub fn add_text( + &self, + view: &BinaryView, + title: B1, + contents: B2, + ) { + let title = title.into_bytes_with_nul(); + let contents = contents.into_bytes_with_nul(); + unsafe { + BNAddPlainTextReportToCollection( + self.as_raw(), + view.handle, + title.as_ref().as_ptr() as *const ffi::c_char, + contents.as_ref().as_ptr() as *const ffi::c_char, + ) } - self.fields.push(result); + } - self.data.push(FormData::Text { - _prompt: prompt, - _default: default, - }); - self + pub fn add_markdown( + &self, + view: &BinaryView, + title: B1, + contents: B2, + plaintext: B3, + ) { + let title = title.into_bytes_with_nul(); + let contents = contents.into_bytes_with_nul(); + let plaintext = plaintext.into_bytes_with_nul(); + unsafe { + BNAddMarkdownReportToCollection( + self.as_raw(), + view.handle, + title.as_ref().as_ptr() as *const ffi::c_char, + contents.as_ref().as_ptr() as *const ffi::c_char, + plaintext.as_ref().as_ptr() as *const ffi::c_char, + ) + } } - /// Form Field: Prompt for an integer - pub fn integer_field(mut self, prompt: &str, default: Option) -> Self { - let prompt = BnString::new(prompt); - - let mut result = unsafe { std::mem::zeroed::() }; - result.type_ = BNFormInputFieldType::IntegerFormField; - result.prompt = prompt.as_ref().as_ptr() as *const c_char; - result.hasDefault = default.is_some(); - if let Some(default) = default { - result.intDefault = default; + pub fn add_html( + &self, + view: &BinaryView, + title: B1, + contents: B2, + plaintext: B3, + ) { + let title = title.into_bytes_with_nul(); + let contents = contents.into_bytes_with_nul(); + let plaintext = plaintext.into_bytes_with_nul(); + unsafe { + BNAddHTMLReportToCollection( + self.as_raw(), + view.handle, + title.as_ref().as_ptr() as *const ffi::c_char, + contents.as_ref().as_ptr() as *const ffi::c_char, + plaintext.as_ref().as_ptr() as *const ffi::c_char, + ) } - self.fields.push(result); + } - self.data.push(FormData::Label { _text: prompt }); - self + pub fn add_graph(&self, view: &BinaryView, title: B, graph: &FlowGraph) { + let title = title.into_bytes_with_nul(); + unsafe { + BNAddGraphReportToCollection( + self.as_raw(), + view.handle, + title.as_ref().as_ptr() as *const ffi::c_char, + graph.handle, + ) + } } - /// Form Field: Prompt for an address - pub fn address_field( - mut self, - prompt: &str, - view: Option>, - current_address: Option, - default: Option, - ) -> Self { - let prompt = BnString::new(prompt); - - let mut result = unsafe { std::mem::zeroed::() }; - result.type_ = BNFormInputFieldType::AddressFormField; - result.prompt = prompt.as_ref().as_ptr() as *const c_char; - 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; + fn update_report_flow_graph(&self, i: usize, graph: &FlowGraph) { + unsafe { BNUpdateReportFlowGraph(self.as_raw(), i, graph.handle) } + } + + pub fn iter(&self) -> ReportCollectionIter<'_> { + ReportCollectionIter::new(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)), } - result.currentAddress = current_address.unwrap_or(0); - result.hasDefault = default.is_some(); - if let Some(default) = default { - result.addressDefault = default; + } + + fn _inner(&self) -> &ReportInner<'a> { + match self { + Report::PlainText(ReportPlainText(x)) + | Report::Markdown(ReportMarkdown(x)) + | Report::Html(ReportHtml(x)) + | Report::FlowGraph(ReportFlowGraph(x)) => x, } - self.fields.push(result); + } - self.data.push(FormData::Label { _text: prompt }); - self + pub fn view(&self) -> Ref { + self._inner().view() } - /// Form Field: Prompt for a choice from provided options - pub fn choice_field(mut self, prompt: &str, choices: &[&str], default: Option) -> Self { - let prompt = BnString::new(prompt); - let choices: Vec = choices.iter().map(|&s| BnString::new(s)).collect(); - - let mut result = unsafe { std::mem::zeroed::() }; - result.type_ = BNFormInputFieldType::ChoiceFormField; - result.prompt = prompt.as_ref().as_ptr() as *const c_char; - let mut raw_choices: Vec<*const c_char> = choices - .iter() - .map(|c| c.as_ref().as_ptr() as *const c_char) - .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; + pub fn title(&self) -> BnString { + self._inner().title() + } +} + +pub struct ReportPlainText<'a>(ReportInner<'a>); +impl ReportPlainText<'_> { + pub fn contents(&self) -> BnString { + self.0.contents() + } +} + +pub struct ReportMarkdown<'a>(ReportInner<'a>); +impl ReportMarkdown<'_> { + pub fn contents(&self) -> BnString { + self.0.contents() + } + + pub fn plaintext(&self) -> BnString { + self.0.plain_text() + } +} + +pub struct ReportHtml<'a>(ReportInner<'a>); +impl ReportHtml<'_> { + pub fn contents(&self) -> BnString { + self.0.contents() + } + + pub fn plaintext(&self) -> BnString { + self.0.plain_text() + } +} + +pub struct ReportFlowGraph<'a>(ReportInner<'a>); +impl ReportFlowGraph<'_> { + pub fn flow_graph(&self) -> Ref { + 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.type_(self.index) + } + + fn view(&self) -> Ref { + self.collection.view(self.index) + } + + fn title(&self) -> BnString { + self.collection.title(self.index) + } + + fn contents(&self) -> BnString { + self.collection.contents(self.index) + } + + fn plain_text(&self) -> BnString { + self.collection.plain_text(self.index) + } + + fn flow_graph(&self) -> Ref { + 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(), } - self.fields.push(result); + } - self.data.push(FormData::Choice { - _prompt: prompt, - _choices: choices, - _raw: raw_choices, - }); - self + pub fn collection(&self) -> &ReportCollection { + self.report } +} - /// 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::() }; - result.type_ = BNFormInputFieldType::OpenFileNameFormField; - result.prompt = prompt.as_ref().as_ptr() as *const c_char; - result.ext = ext.as_ref().as_ptr() as *const c_char; - result.hasDefault = default.is_some(); - if let Some(ref default) = default { - result.stringDefault = default.as_ref().as_ptr() as *const c_char; +impl<'a> Iterator for ReportCollectionIter<'a> { + type Item = Report<'a>; + + fn next(&mut self) -> Option { + (self.current_index < self.count).then(|| { + let result = Report::new(self.report, self.current_index); + self.current_index += 1; + result + }) + } +} + +// A Zero cost transmute compatible type with BNFormInputField +// NOTE: the result values WILL be leaked unless +// [BNFreeFormInputResults] is called after [FormInputField::result] +#[repr(transparent)] +pub struct FormInput(BNFormInputField); + +impl Drop for FormInput { + fn drop(&mut self) { + fn drop_string(string: *mut ffi::c_char) { + if !string.is_null() { + drop(unsafe { BnString::from_raw(string) }); + } } - self.fields.push(result); - self.data.push(FormData::File { - _prompt: prompt, - _ext: ext, - _default: default, - }); - self + let raw: &BNFormInputField = &self.0; + // NOTE there is a function BNFreeFormInputResults, but that + // only works with a list for BNFormInputField and only drop the result. + // I'm assuming this those the same as this function with drop_result true. + + use BNFormInputFieldType::*; + match raw.type_ { + SeparatorFormField => {} + LabelFormField => { + drop_string(raw.prompt as *mut ffi::c_char); + } + MultilineTextFormField | TextLineFormField => { + drop_string(raw.prompt as *mut ffi::c_char); + if raw.hasDefault { + drop_string(raw.stringDefault as *mut ffi::c_char); + } + } + IntegerFormField => { + drop_string(raw.prompt as *mut ffi::c_char); + } + AddressFormField => { + drop_string(raw.prompt as *mut ffi::c_char); + // NOTE the BinaryView in `raw.view` can be both owned or + // borrowed depending on the creation of FormInput. Currently + // the implementation uses borrow BinaryView, so we don't need + // to drop it. + } + ChoiceFormField => { + drop_string(raw.prompt as *mut ffi::c_char); + drop((!raw.choices.is_null()).then(|| unsafe { + Array::::new(raw.choices as *mut *mut ffi::c_char, raw.count, ()) + })); + } + OpenFileNameFormField => { + drop_string(raw.prompt as *mut ffi::c_char); + drop_string(raw.ext as *mut ffi::c_char); + if raw.hasDefault { + drop_string(raw.stringDefault as *mut ffi::c_char); + } + } + SaveFileNameFormField => { + drop_string(raw.prompt as *mut ffi::c_char); + drop_string(raw.ext as *mut ffi::c_char); + if raw.hasDefault { + drop_string(raw.stringDefault as *mut ffi::c_char); + } + drop_string(raw.defaultName as *mut ffi::c_char); + } + DirectoryNameFormField => { + drop_string(raw.prompt as *mut ffi::c_char); + if raw.hasDefault { + drop_string(raw.stringDefault as *mut ffi::c_char); + } + drop_string(raw.defaultName as *mut ffi::c_char); + } + } } +} - /// 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::() }; - result.type_ = BNFormInputFieldType::SaveFileNameFormField; - result.prompt = prompt.as_ref().as_ptr() as *const c_char; - result.ext = ext.as_ref().as_ptr() as *const c_char; - result.defaultName = default_name.as_ref().as_ptr() as *const c_char; - result.hasDefault = default.is_some(); - if let Some(ref default) = default { - result.stringDefault = default.as_ref().as_ptr() as *const c_char; +impl FormInput { + pub(crate) fn as_raw(&self) -> &BNFormInputField { + &self.0 + } + + // NOTE this don't free the result, you need to call BNFreeFormInputResults + // manually after + fn result(&self) -> FormResponses { + use BNFormInputFieldType::*; + match self.0.type_ { + LabelFormField | SeparatorFormField => FormResponses::None, + + TextLineFormField + | MultilineTextFormField + | OpenFileNameFormField + | SaveFileNameFormField + | DirectoryNameFormField => FormResponses::String(unsafe { + CStr::from_ptr(self.0.stringResult) + .to_str() + .unwrap() + .to_owned() + }), + + IntegerFormField => FormResponses::Integer(self.0.intResult), + AddressFormField => FormResponses::Address(self.0.addressResult), + ChoiceFormField => FormResponses::Index(self.0.indexResult), + } + } + + pub fn type_(&self) -> FormInputType<'_> { + use BNFormInputFieldType::*; + use FormInputType::*; + match self.0.type_ { + SeparatorFormField => Separator, + LabelFormField => Label(FormInputFieldLabel(self)), + TextLineFormField => TextLine(FormInputFieldText(self)), + MultilineTextFormField => MultilineText(FormInputFieldText(self)), + IntegerFormField => Integer(FormInputFieldInteger(self)), + AddressFormField => Address(FormInputFieldAddress(self)), + ChoiceFormField => Choice(FormInputFieldChoice(self)), + OpenFileNameFormField => OpenFileName(FormInputFieldOpenFile(self)), + SaveFileNameFormField => SaveFileName(FormInputFieldSaveFile(self)), + DirectoryNameFormField => DirectoryName(FormInputFieldDirectory(self)), } - self.fields.push(result); + } - self.data.push(FormData::FileSave { - _prompt: prompt, - _ext: ext, - _default_name: default_name, - _default: default, - }); - self + /// Form Field: Text output + pub fn label_field(text: S) -> Self { + Self(BNFormInputField { + type_: BNFormInputFieldType::LabelFormField, + prompt: BnString::into_raw(BnString::new(text)) as *const ffi::c_char, + ..Default::default() + }) } - /// Form Field: Prompt for directory name - pub fn directory_name_field( - mut self, - prompt: &str, - default_name: Option<&str>, - default: Option<&str>, + /// Form Field: Vertical spacing + pub fn separator_field() -> Self { + Self(BNFormInputField { + type_: BNFormInputFieldType::SeparatorFormField, + ..Default::default() + }) + } + + fn _inner_text_field( + prompt: S1, + default: Option, + type_: BNFormInputFieldType, + ) -> Self + where + S1: BnStrCompatible, + S2: BnStrCompatible, + { + Self(BNFormInputField { + type_, + prompt: BnString::into_raw(BnString::new(BnString::new(prompt))) as *const ffi::c_char, + hasDefault: default.is_some(), + stringDefault: default + .map(|d| BnString::into_raw(BnString::new(d))) + .unwrap_or(ptr::null_mut()), + ..Default::default() + }) + } + + /// Form Field: Prompt for a string value + pub fn text_field(prompt: S1, default: Option) -> Self + where + S1: BnStrCompatible, + S2: BnStrCompatible, + { + Self::_inner_text_field(prompt, default, BNFormInputFieldType::TextLineFormField) + } + + /// Form Field: Prompt for multi-line string value + pub fn multiline_field(prompt: S1, default: Option) -> Self + where + S1: BnStrCompatible, + S2: BnStrCompatible, + { + Self::_inner_text_field(prompt, default, BNFormInputFieldType::TextLineFormField) + } + + /// Form Field: Prompt for an integer + pub fn integer_field(prompt: S, default: Option) -> Self { + Self(BNFormInputField { + type_: BNFormInputFieldType::IntegerFormField, + prompt: BnString::into_raw(BnString::new(BnString::new(prompt))) as *const ffi::c_char, + hasDefault: default.is_some(), + intDefault: default.unwrap_or_default(), + ..Default::default() + }) + } + + /// Form Field: Prompt for an address + pub fn address_field( + prompt: S, + view: Option<&BinaryView>, + current_address: u64, + default: Option, ) -> 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::() }; - result.type_ = BNFormInputFieldType::DirectoryNameFormField; - result.prompt = prompt.as_ref().as_ptr() as *const c_char; - result.defaultName = default_name.as_ref().as_ptr() as *const c_char; - result.hasDefault = default.is_some(); - if let Some(ref default) = default { - result.stringDefault = default.as_ref().as_ptr() as *const c_char; - } - 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 { - if unsafe { - BNGetFormInput( - self.fields.as_mut_ptr(), - self.fields.len(), - title.into_bytes_with_nul().as_ptr() as *const _, - ) - } { - 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() - }) - } + Self(BNFormInputField { + type_: BNFormInputFieldType::AddressFormField, + prompt: BnString::into_raw(BnString::new(BnString::new(prompt))) as *const ffi::c_char, + view: view.map(|view| view.handle).unwrap_or(ptr::null_mut()), + currentAddress: current_address, + hasDefault: default.is_some(), + addressDefault: default.unwrap_or_default(), + ..Default::default() + }) + } - 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![] + /// Form Field: Prompt for a choice from provided options + pub fn choice_field( + prompt: S, + choices: &[&str], + default: Option, + ) -> Self { + Self(BNFormInputField { + type_: BNFormInputFieldType::ChoiceFormField, + prompt: BnString::into_raw(BnString::new(BnString::new(prompt))) as *const ffi::c_char, + choices: crate::string::strings_to_string_list(choices) as *mut *const ffi::c_char, + count: choices.len(), + hasDefault: default.is_some(), + indexDefault: default.unwrap_or_default(), + ..Default::default() + }) + } + + /// Form Field: Prompt for file to open + pub fn open_file_field(prompt: S1, ext: Option, default: Option) -> Self + where + S1: BnStrCompatible, + S2: BnStrCompatible, + S3: BnStrCompatible, + { + Self(BNFormInputField { + type_: BNFormInputFieldType::OpenFileNameFormField, + prompt: BnString::into_raw(BnString::new(BnString::new(prompt))) as *const ffi::c_char, + ext: ext + .map(|ext| BnString::into_raw(BnString::new(ext))) + .unwrap_or_else(|| BnString::into_raw(BnString::new(c""))), + hasDefault: default.is_some(), + stringDefault: default + .map(|default| BnString::into_raw(BnString::new(default))) + .unwrap_or(ptr::null_mut()), + ..Default::default() + }) + } + + /// Form Field: Prompt for file to save to + pub fn save_file_field( + prompt: S1, + ext: Option, + default_name: Option, + default: Option, + ) -> Self + where + S1: BnStrCompatible, + S2: BnStrCompatible, + S3: BnStrCompatible, + S4: BnStrCompatible, + { + Self(BNFormInputField { + type_: BNFormInputFieldType::SaveFileNameFormField, + prompt: BnString::into_raw(BnString::new(BnString::new(prompt))) as *const ffi::c_char, + ext: ext + .map(|ext| BnString::into_raw(BnString::new(ext))) + .unwrap_or_else(|| BnString::into_raw(BnString::new(c""))), + defaultName: default_name + .map(|name| BnString::into_raw(BnString::new(name))) + .unwrap_or_else(|| BnString::into_raw(BnString::new(c""))), + hasDefault: default.is_some(), + stringDefault: default + .map(|default| BnString::into_raw(BnString::new(default))) + .unwrap_or(ptr::null_mut()), + ..Default::default() + }) + } + + /// Form Field: Prompt for directory name + pub fn directory_name_field( + prompt: S1, + default_name: Option, + default: Option, + ) -> Self + where + S1: BnStrCompatible, + S2: BnStrCompatible, + S3: BnStrCompatible, + { + Self(BNFormInputField { + type_: BNFormInputFieldType::DirectoryNameFormField, + prompt: BnString::into_raw(BnString::new(BnString::new(prompt))) as *const ffi::c_char, + hasDefault: default.is_some(), + defaultName: default_name + .map(|name| BnString::into_raw(BnString::new(name))) + .unwrap_or_else(|| BnString::into_raw(BnString::new(c""))), + stringDefault: default + .map(|default| BnString::into_raw(BnString::new(default))) + .unwrap_or(ptr::null_mut()), + ..Default::default() + }) + } +} + +pub enum FormInputType<'a> { + Separator, + Label(FormInputFieldLabel<'a>), + TextLine(FormInputFieldText<'a>), + MultilineText(FormInputFieldText<'a>), + Integer(FormInputFieldInteger<'a>), + Address(FormInputFieldAddress<'a>), + Choice(FormInputFieldChoice<'a>), + OpenFileName(FormInputFieldOpenFile<'a>), + SaveFileName(FormInputFieldSaveFile<'a>), + DirectoryName(FormInputFieldDirectory<'a>), +} + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct FormInputFieldLabel<'a>(&'a FormInput); +impl FormInputFieldLabel<'_> { + pub fn prompt(&self) -> &str { + form_input_field_prompt(self.0.as_raw()) + } +} + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct FormInputFieldText<'a>(&'a FormInput); +impl FormInputFieldText<'_> { + pub fn prompt(&self) -> &str { + form_input_field_prompt(self.0.as_raw()) + } + + pub fn default(&self) -> Option<&str> { + form_input_field_default_string(self.0.as_raw()) + } +} + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct FormInputFieldInteger<'a>(&'a FormInput); +impl FormInputFieldInteger<'_> { + pub fn prompt(&self) -> &str { + form_input_field_prompt(self.0.as_raw()) + } + + pub fn default(&self) -> Option { + self.0 + .as_raw() + .hasDefault + .then_some(self.0.as_raw().intDefault) + } +} + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct FormInputFieldAddress<'a>(&'a FormInput); +impl FormInputFieldAddress<'_> { + pub fn prompt(&self) -> &str { + form_input_field_prompt(self.0.as_raw()) + } + + pub fn view(&self) -> Option { + (!self.0.as_raw().view.is_null()) + .then(|| unsafe { BinaryView::from_raw(self.0.as_raw().view) }) + } + + pub fn default(&self) -> Option { + self.0 + .as_raw() + .hasDefault + .then_some(self.0.as_raw().addressDefault) + } +} + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct FormInputFieldChoice<'a>(&'a FormInput); +impl FormInputFieldChoice<'_> { + pub fn prompt(&self) -> &str { + form_input_field_prompt(self.0.as_raw()) + } + + pub fn choices(&self) -> &[BnString] { + let ptr: *mut *const ffi::c_char = self.0.as_raw().choices; + let count = self.0.as_raw().count; + if ptr.is_null() { + return &[]; } + + // SAFETY BnString and *const ffi::c_char are transparent + unsafe { core::slice::from_raw_parts(ptr as *const BnString, count) } + } + + pub fn default(&self) -> Option { + self.0 + .as_raw() + .hasDefault + .then_some(self.0.as_raw().indexDefault) } } -impl Default for FormInputBuilder { - fn default() -> Self { - Self::new() +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct FormInputFieldOpenFile<'a>(&'a FormInput); +impl FormInputFieldOpenFile<'_> { + pub fn prompt(&self) -> &str { + form_input_field_prompt(self.0.as_raw()) + } + + pub fn ext(&self) -> Option<&str> { + form_input_field_ext(self.0.as_raw()) + } + + pub fn default(&self) -> Option<&str> { + form_input_field_default_string(self.0.as_raw()) } } -struct TaskContext Result<(), ()>>)>(F); +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct FormInputFieldSaveFile<'a>(&'a FormInput); +impl FormInputFieldSaveFile<'_> { + pub fn prompt(&self) -> &str { + form_input_field_prompt(self.0.as_raw()) + } -pub fn run_progress_dialog Result<(), ()>>)>( - title: &str, - can_cancel: bool, - task: F, -) -> Result<(), ()> { - let mut ctxt = TaskContext::(task); + pub fn ext(&self) -> Option<&str> { + form_input_field_ext(self.0.as_raw()) + } - unsafe extern "C" fn cb_task Result<(), ()>>)>( - ctxt: *mut c_void, - progress: Option bool>, - progress_ctxt: *mut c_void, - ) { - ffi_wrap!("run_progress_dialog", { - let context = ctxt as *mut TaskContext; - let progress_fn = Box::new(move |cur: usize, max: usize| -> Result<(), ()> { - match progress { - Some(func) => { - if (func)(progress_ctxt, cur, max) { - Ok(()) - } else { - Err(()) - } - } - None => Ok(()), - } - }); - ((*context).0)(progress_fn); - }) + pub fn default_name(&self) -> Option<&str> { + form_input_field_default_name(self.0.as_raw()) } - if unsafe { - BNRunProgressDialog( - title.into_bytes_with_nul().as_ptr() as *mut _, - can_cancel, - Some(cb_task::), - &mut ctxt as *mut _ as *mut c_void, - ) - } { - Ok(()) - } else { - Err(()) + pub fn default(&self) -> Option<&str> { + form_input_field_default_string(self.0.as_raw()) + } +} + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct FormInputFieldDirectory<'a>(&'a FormInput); +impl FormInputFieldDirectory<'_> { + pub fn prompt(&self) -> &str { + form_input_field_prompt(self.0.as_raw()) + } + + pub fn default_name(&self) -> Option<&str> { + form_input_field_default_name(self.0.as_raw()) } + + pub fn default(&self) -> Option<&str> { + form_input_field_default_string(self.0.as_raw()) + } +} + +fn form_input_field_prompt(raw: &BNFormInputField) -> &str { + debug_assert!(!raw.prompt.is_null()); + unsafe { CStr::from_ptr(raw.prompt) }.to_str().unwrap() +} + +fn form_input_field_ext(raw: &BNFormInputField) -> Option<&str> { + (!raw.ext.is_null()) + .then(|| { + let result = unsafe { CStr::from_ptr(raw.ext) }.to_str().unwrap(); + (!result.is_empty()).then_some(result) + }) + .flatten() +} + +fn form_input_field_default_name(raw: &BNFormInputField) -> Option<&str> { + raw.hasDefault + .then(|| { + let result = unsafe { CStr::from_ptr(raw.defaultName) }.to_str().unwrap(); + (!result.is_empty()).then_some(result) + }) + .flatten() +} + +fn form_input_field_default_string(raw: &BNFormInputField) -> Option<&str> { + raw.hasDefault + .then(|| unsafe { + (!raw.stringDefault.is_null()) + .then(|| CStr::from_ptr(raw.stringDefault).to_str().unwrap()) + }) + .flatten() } diff --git a/rust/src/string.rs b/rust/src/string.rs index f92248516..888b6b28b 100644 --- a/rust/src/string.rs +++ b/rust/src/string.rs @@ -33,12 +33,16 @@ pub(crate) fn raw_to_string(ptr: *const c_char) -> Option { } } -// 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(strings: I) -> *mut *mut c_char +where + I: IntoIterator, + // TODO make `S: BnStrCompatible,` + S: AsRef, +{ 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::>(); let mut raw_str_list = bn_str_list.iter().map(|s| s.as_ptr()).collect::>(); 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 000000000..40a49f3b8 --- /dev/null +++ b/rust/tests/interaction.rs @@ -0,0 +1,176 @@ +use std::path::PathBuf; + +use binaryninja::binary_view::BinaryView; +use binaryninja::flowgraph::FlowGraph; +use binaryninja::headless::Session; +use binaryninja::interaction::{ + register_custom_interaction_handler, CustomInteractionHandler, CustomInterationHandlerTask, + FormInput, FormResponses, MessageBoxButtonResult, MessageBoxButtonSet, MessageBoxIcon, Report, + ReportCollection, +}; + +struct MyInteractionHandler {} +impl CustomInteractionHandler for MyInteractionHandler { + fn show_plain_text_report(&mut self, _view: &BinaryView, _title: &str, _contents: &str) { + todo!() + } + + fn show_markdown_report( + &mut self, + _view: &BinaryView, + _title: &str, + _contents: &str, + _plaintext: &str, + ) { + todo!() + } + + fn show_html_report( + &mut self, + _view: &BinaryView, + _title: &str, + _contents: &str, + _plaintext: &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().as_str(), 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(), "contents"); + 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, fields: &[FormInput], _title: &str) -> Option> { + if fields.len() != 1 { + return None; + } + use binaryninja::interaction::FormInputType::*; + match fields[0].type_() { + Integer(_) => Some(vec![FormResponses::Integer(1337)]), + DirectoryName(dir) => Some(vec![FormResponses::String( + Some("example") + .into_iter() + .chain(dir.default()) + .chain(dir.default_name()) + .collect(), + )]), + Address(addr_form) => Some(vec![FormResponses::Address( + addr_form.default().unwrap_or(0) + 0x10, + )]), + _ => None, + } + } + + 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: &CustomInterationHandlerTask, + ) -> bool { + todo!() + } +} + +#[test] +fn test_get_integer() { + register_custom_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_custom_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_custom_interaction_handler(MyInteractionHandler {}); + let outputs = binaryninja::interaction::get_form_input( + "get_dir_default", + &mut [FormInput::directory_name_field( + "get_dir_default", + Some("_default_name"), + Some("_default"), + )], + ); + assert_eq!(outputs.len(), 1); + let FormResponses::String(output) = &outputs[0] else { + panic!(); + }; + assert_eq!(output, "example_default_default_name"); +} + +#[test] +fn test_get_address() { + register_custom_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::().unwrap(); + let view = binaryninja::load(out_dir.join("atox.obj")).expect("Failed to create view"); + + register_custom_interaction_handler(MyInteractionHandler {}); + let collection = ReportCollection::new(); + collection.add_text(&view, format!("title_report_0"), "contents"); + collection.add_markdown( + &view, + format!("title_report_1"), + "# contents", + "markdown_plain_text", + ); + collection.add_html( + &view, + format!("title_report_2"), + "contents", + "html_plain_text", + ); + collection.add_graph(&view, format!("title_report_3"), &FlowGraph::new()); + binaryninja::interaction::show_report_collection("show_report_collection_title", &collection); +}