summaryrefslogtreecommitdiff
path: root/rust/src/interaction/form.rs
diff options
context:
space:
mode:
authorrbran <git@rubens.io>2025-04-23 19:24:45 +0000
committerMason Reed <35282038+emesare@users.noreply.github.com>2025-05-12 17:45:24 -0400
commitc2d90f2e5a010546d0e1f1deeb1a6d8531ea4d4f (patch)
tree20a6c6268943154fdda6a4d6884f2f29d9d4de50 /rust/src/interaction/form.rs
parent4a4a8488269922269b20a12a7b71272b925b9f18 (diff)
[Rust] Implement custom interactive handler
Diffstat (limited to 'rust/src/interaction/form.rs')
-rw-r--r--rust/src/interaction/form.rs411
1 files changed, 411 insertions, 0 deletions
diff --git a/rust/src/interaction/form.rs b/rust/src/interaction/form.rs
new file mode 100644
index 00000000..06799681
--- /dev/null
+++ b/rust/src/interaction/form.rs
@@ -0,0 +1,411 @@
+use std::ffi::c_char;
+
+use binaryninjacore_sys::*;
+
+use crate::binary_view::BinaryView;
+use crate::rc::{Array, Ref};
+use crate::string::{raw_to_string, strings_to_string_list, BnString, IntoCStr};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Form {
+ pub title: String,
+ pub fields: Vec<FormInputField>,
+}
+
+impl Form {
+ pub fn new(title: impl Into<String>) -> Self {
+ Self::new_with_fields(title, vec![])
+ }
+
+ pub fn new_with_fields(title: impl Into<String>, fields: Vec<FormInputField>) -> Self {
+ Self {
+ title: title.into(),
+ fields,
+ }
+ }
+
+ pub fn add_field(&mut self, field: FormInputField) {
+ self.fields.push(field);
+ }
+
+ /// Prompt the user (or interaction handler) for the form input.
+ ///
+ /// Updates the field's values and returns whether the form was accepted or not.
+ pub fn prompt(&mut self) -> bool {
+ let title = self.title.clone().to_cstr();
+ let mut raw_fields = self
+ .fields
+ .iter()
+ .map(FormInputField::into_raw)
+ .collect::<Vec<_>>();
+ let success =
+ unsafe { BNGetFormInput(raw_fields.as_mut_ptr(), raw_fields.len(), title.as_ptr()) };
+ // Update the fields with the new field values.
+ self.fields = raw_fields
+ .into_iter()
+ .map(FormInputField::from_owned_raw)
+ .collect();
+ success
+ }
+
+ pub fn get_field_with_name(&self, name: &str) -> Option<&FormInputField> {
+ self.fields
+ .iter()
+ .find(|field| field.try_prompt() == Some(name.to_string()))
+ }
+}
+
+/// A field within a form.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum FormInputField {
+ Label {
+ prompt: String,
+ },
+ Separator,
+ TextLine {
+ prompt: String,
+ default: Option<String>,
+ value: Option<String>,
+ },
+ MultilineText {
+ prompt: String,
+ default: Option<String>,
+ value: Option<String>,
+ },
+ Integer {
+ prompt: String,
+ default: Option<i64>,
+ value: i64,
+ },
+ Address {
+ prompt: String,
+ view: Option<Ref<BinaryView>>,
+ current_address: u64,
+ default: Option<u64>,
+ value: u64,
+ },
+ Choice {
+ prompt: String,
+ choices: Vec<String>,
+ default: Option<usize>,
+ value: usize,
+ },
+ OpenFileName {
+ prompt: String,
+ /// File extension to filter on.
+ extension: Option<String>,
+ default: Option<String>,
+ value: Option<String>,
+ },
+ SaveFileName {
+ prompt: String,
+ /// File extension to filter on.
+ extension: Option<String>,
+ /// Default file name to fill.
+ default_name: Option<String>,
+ default: Option<String>,
+ value: Option<String>,
+ },
+ DirectoryName {
+ prompt: String,
+ default_name: Option<String>,
+ default: Option<String>,
+ value: Option<String>,
+ },
+}
+
+impl FormInputField {
+ pub fn from_raw(value: &BNFormInputField) -> Self {
+ let prompt = raw_to_string(value.prompt).unwrap_or_default();
+ let view = match value.view.is_null() {
+ false => Some(unsafe { BinaryView::from_raw(value.view) }.to_owned()),
+ true => None,
+ };
+ let name_default = value
+ .hasDefault
+ .then_some(raw_to_string(value.defaultName).unwrap_or_default());
+ let string_default = value
+ .hasDefault
+ .then_some(raw_to_string(value.stringDefault).unwrap_or_default());
+ let int_default = value.hasDefault.then_some(value.intDefault);
+ let address_default = value.hasDefault.then_some(value.addressDefault);
+ let index_default = value.hasDefault.then_some(value.indexDefault);
+ let extension = raw_to_string(value.ext);
+ let current_address = value.currentAddress;
+ let string_result = raw_to_string(value.stringResult);
+ let int_result = value.intResult;
+ let address_result = value.addressResult;
+ let index_result = value.indexResult;
+ match value.type_ {
+ BNFormInputFieldType::LabelFormField => Self::Label { prompt },
+ BNFormInputFieldType::SeparatorFormField => Self::Separator,
+ BNFormInputFieldType::TextLineFormField => Self::TextLine {
+ prompt,
+ default: string_default,
+ value: string_result,
+ },
+ BNFormInputFieldType::MultilineTextFormField => Self::MultilineText {
+ prompt,
+ default: string_default,
+ value: string_result,
+ },
+ BNFormInputFieldType::IntegerFormField => Self::Integer {
+ prompt,
+ default: int_default,
+ value: int_result,
+ },
+ BNFormInputFieldType::AddressFormField => Self::Address {
+ prompt,
+ view,
+ current_address,
+ default: address_default,
+ value: address_result,
+ },
+ BNFormInputFieldType::ChoiceFormField => Self::Choice {
+ prompt,
+ choices: vec![],
+ default: index_default,
+ value: index_result,
+ },
+ BNFormInputFieldType::OpenFileNameFormField => Self::OpenFileName {
+ prompt,
+ extension,
+ default: string_default,
+ value: string_result,
+ },
+ BNFormInputFieldType::SaveFileNameFormField => Self::SaveFileName {
+ prompt,
+ extension,
+ default_name: name_default,
+ default: string_default,
+ value: string_result,
+ },
+ BNFormInputFieldType::DirectoryNameFormField => Self::DirectoryName {
+ prompt,
+ default_name: name_default,
+ default: string_default,
+ value: string_result,
+ },
+ }
+ }
+
+ pub fn from_owned_raw(value: BNFormInputField) -> Self {
+ let owned = Self::from_raw(&value);
+ Self::free_raw(value);
+ owned
+ }
+
+ pub fn into_raw(&self) -> BNFormInputField {
+ let bn_prompt = BnString::new(self.try_prompt().unwrap_or_default());
+ let bn_extension = BnString::new(self.try_extension().unwrap_or_default());
+ let bn_default_string = BnString::new(self.try_default_string().unwrap_or_default());
+ let bn_default_name = BnString::new(self.try_default_name().unwrap_or_default());
+ let bn_value_string = BnString::new(self.try_value_string().unwrap_or_default());
+ // Expected to be freed by [`FormInputField::free_raw`].
+ BNFormInputField {
+ type_: self.as_type(),
+ prompt: BnString::into_raw(bn_prompt),
+ view: match self.try_view() {
+ None => std::ptr::null_mut(),
+ Some(view) => unsafe { Ref::into_raw(view) }.handle,
+ },
+ currentAddress: self.try_current_address().unwrap_or_default(),
+ choices: match self.try_choices() {
+ None => std::ptr::null_mut(),
+ Some(choices) => strings_to_string_list(choices.as_slice()) as *mut *const c_char,
+ },
+ // NOTE: `count` is the length of the `choices` array.
+ count: self.try_choices().unwrap_or_default().len(),
+ ext: BnString::into_raw(bn_extension),
+ defaultName: BnString::into_raw(bn_default_name),
+ intResult: self.try_value_int().unwrap_or_default(),
+ addressResult: self.try_value_address().unwrap_or_default(),
+ stringResult: BnString::into_raw(bn_value_string),
+ indexResult: self.try_value_index().unwrap_or_default(),
+ hasDefault: self.try_has_default().unwrap_or_default(),
+ intDefault: self.try_default_int().unwrap_or_default(),
+ addressDefault: self.try_default_address().unwrap_or_default(),
+ stringDefault: BnString::into_raw(bn_default_string),
+ indexDefault: self.try_default_index().unwrap_or_default(),
+ }
+ }
+
+ pub fn free_raw(value: BNFormInputField) {
+ unsafe {
+ BnString::free_raw(value.defaultName as *mut c_char);
+ BnString::free_raw(value.prompt as *mut c_char);
+ BnString::free_raw(value.ext as *mut c_char);
+ BnString::free_raw(value.stringDefault as *mut c_char);
+ BnString::free_raw(value.stringResult);
+ // TODO: Would like access to a `Array::free_raw` or something.
+ Array::<BnString>::new(value.choices as *mut *mut c_char, value.count, ());
+ // Free the view ref if provided.
+ if !value.view.is_null() {
+ BinaryView::ref_from_raw(value.view);
+ }
+ }
+ }
+
+ pub fn as_type(&self) -> BNFormInputFieldType {
+ match self {
+ FormInputField::Label { .. } => BNFormInputFieldType::LabelFormField,
+ FormInputField::Separator => BNFormInputFieldType::SeparatorFormField,
+ FormInputField::TextLine { .. } => BNFormInputFieldType::TextLineFormField,
+ FormInputField::MultilineText { .. } => BNFormInputFieldType::MultilineTextFormField,
+ FormInputField::Integer { .. } => BNFormInputFieldType::IntegerFormField,
+ FormInputField::Address { .. } => BNFormInputFieldType::AddressFormField,
+ FormInputField::Choice { .. } => BNFormInputFieldType::ChoiceFormField,
+ FormInputField::OpenFileName { .. } => BNFormInputFieldType::OpenFileNameFormField,
+ FormInputField::SaveFileName { .. } => BNFormInputFieldType::SaveFileNameFormField,
+ FormInputField::DirectoryName { .. } => BNFormInputFieldType::DirectoryNameFormField,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::prompt`] field.
+ pub fn try_prompt(&self) -> Option<String> {
+ match self {
+ FormInputField::Label { prompt, .. } => Some(prompt.clone()),
+ FormInputField::Separator => None,
+ FormInputField::TextLine { prompt, .. } => Some(prompt.clone()),
+ FormInputField::MultilineText { prompt, .. } => Some(prompt.clone()),
+ FormInputField::Integer { prompt, .. } => Some(prompt.clone()),
+ FormInputField::Address { prompt, .. } => Some(prompt.clone()),
+ FormInputField::Choice { prompt, .. } => Some(prompt.clone()),
+ FormInputField::OpenFileName { prompt, .. } => Some(prompt.clone()),
+ FormInputField::SaveFileName { prompt, .. } => Some(prompt.clone()),
+ FormInputField::DirectoryName { prompt, .. } => Some(prompt.clone()),
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::view`] field.
+ pub fn try_view(&self) -> Option<Ref<BinaryView>> {
+ match self {
+ FormInputField::Address { view, .. } => view.clone(),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::currentAddress`] field.
+ pub fn try_current_address(&self) -> Option<u64> {
+ match self {
+ FormInputField::Address {
+ current_address, ..
+ } => Some(*current_address),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::choices`] field.
+ pub fn try_choices(&self) -> Option<Vec<String>> {
+ match self {
+ FormInputField::Choice { choices, .. } => Some(choices.clone()),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::ext`] field.
+ pub fn try_extension(&self) -> Option<String> {
+ match self {
+ Self::SaveFileName { extension, .. } => extension.clone(),
+ Self::OpenFileName { extension, .. } => extension.clone(),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::hasDefault`] field.
+ pub fn try_has_default(&self) -> Option<bool> {
+ match self {
+ FormInputField::Label { .. } => None,
+ FormInputField::Separator => None,
+ FormInputField::TextLine { default, .. } => Some(default.is_some()),
+ FormInputField::MultilineText { default, .. } => Some(default.is_some()),
+ FormInputField::Integer { default, .. } => Some(default.is_some()),
+ FormInputField::Address { default, .. } => Some(default.is_some()),
+ FormInputField::Choice { default, .. } => Some(default.is_some()),
+ FormInputField::OpenFileName { default, .. } => Some(default.is_some()),
+ FormInputField::SaveFileName { default, .. } => Some(default.is_some()),
+ FormInputField::DirectoryName { default, .. } => Some(default.is_some()),
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::defaultName`] field.
+ pub fn try_default_name(&self) -> Option<String> {
+ match self {
+ Self::SaveFileName { default_name, .. } => default_name.clone(),
+ Self::DirectoryName { default_name, .. } => default_name.clone(),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::intDefault`] field.
+ pub fn try_default_int(&self) -> Option<i64> {
+ match self {
+ FormInputField::Integer { default, .. } => *default,
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::addressDefault`] field.
+ pub fn try_default_address(&self) -> Option<u64> {
+ match self {
+ FormInputField::Address { default, .. } => *default,
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::stringDefault`] field.
+ pub fn try_default_string(&self) -> Option<String> {
+ match self {
+ FormInputField::TextLine { default, .. } => default.clone(),
+ FormInputField::MultilineText { default, .. } => default.clone(),
+ FormInputField::OpenFileName { default, .. } => default.clone(),
+ FormInputField::SaveFileName { default, .. } => default.clone(),
+ FormInputField::DirectoryName { default, .. } => default.clone(),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::indexDefault`] field.
+ pub fn try_default_index(&self) -> Option<usize> {
+ match self {
+ FormInputField::Choice { default, .. } => *default,
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::intResult`] field.
+ pub fn try_value_int(&self) -> Option<i64> {
+ match self {
+ FormInputField::Integer { value, .. } => Some(*value),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::addressResult`] field.
+ pub fn try_value_address(&self) -> Option<u64> {
+ match self {
+ FormInputField::Address { value, .. } => Some(*value),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::stringResult`] field.
+ pub fn try_value_string(&self) -> Option<String> {
+ match self {
+ FormInputField::TextLine { value, .. } => value.clone(),
+ FormInputField::MultilineText { value, .. } => value.clone(),
+ FormInputField::OpenFileName { value, .. } => value.clone(),
+ FormInputField::SaveFileName { value, .. } => value.clone(),
+ FormInputField::DirectoryName { value, .. } => value.clone(),
+ _ => None,
+ }
+ }
+
+ /// Mapping to the [`BNFormInputField::indexResult`] field.
+ pub fn try_value_index(&self) -> Option<usize> {
+ match self {
+ FormInputField::Choice { value, .. } => Some(*value),
+ _ => None,
+ }
+ }
+}