binaryninja/interaction/
form.rs

1use std::ffi::c_char;
2
3use binaryninjacore_sys::*;
4
5use crate::binary_view::BinaryView;
6use crate::rc::{Array, Ref};
7use crate::string::{raw_to_string, strings_to_string_list, BnString, IntoCStr};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Form {
11    pub title: String,
12    pub fields: Vec<FormInputField>,
13}
14
15impl Form {
16    pub fn new(title: impl Into<String>) -> Self {
17        Self::new_with_fields(title, vec![])
18    }
19
20    pub fn new_with_fields(title: impl Into<String>, fields: Vec<FormInputField>) -> Self {
21        Self {
22            title: title.into(),
23            fields,
24        }
25    }
26
27    pub fn add_field(&mut self, field: FormInputField) {
28        self.fields.push(field);
29    }
30
31    /// Prompt the user (or interaction handler) for the form input.
32    ///
33    /// Updates the field's values and returns whether the form was accepted or not.
34    pub fn prompt(&mut self) -> bool {
35        let title = self.title.clone().to_cstr();
36        let mut raw_fields = self
37            .fields
38            .iter()
39            .map(FormInputField::into_raw)
40            .collect::<Vec<_>>();
41        let success =
42            unsafe { BNGetFormInput(raw_fields.as_mut_ptr(), raw_fields.len(), title.as_ptr()) };
43        // Update the fields with the new field values.
44        self.fields = raw_fields
45            .into_iter()
46            .map(FormInputField::from_owned_raw)
47            .collect();
48        success
49    }
50
51    pub fn get_field_with_name(&self, name: &str) -> Option<&FormInputField> {
52        self.fields
53            .iter()
54            .find(|field| field.try_prompt() == Some(name.to_string()))
55    }
56}
57
58/// A field within a form.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum FormInputField {
61    Label {
62        prompt: String,
63    },
64    Separator,
65    TextLine {
66        prompt: String,
67        default: Option<String>,
68        value: Option<String>,
69    },
70    MultilineText {
71        prompt: String,
72        default: Option<String>,
73        value: Option<String>,
74    },
75    Integer {
76        prompt: String,
77        default: Option<i64>,
78        value: i64,
79    },
80    Address {
81        prompt: String,
82        view: Option<Ref<BinaryView>>,
83        current_address: u64,
84        default: Option<u64>,
85        value: u64,
86    },
87    Choice {
88        prompt: String,
89        choices: Vec<String>,
90        default: Option<usize>,
91        value: usize,
92    },
93    OpenFileName {
94        prompt: String,
95        /// File extension to filter on.
96        extension: Option<String>,
97        default: Option<String>,
98        value: Option<String>,
99    },
100    SaveFileName {
101        prompt: String,
102        /// File extension to filter on.
103        extension: Option<String>,
104        default: Option<String>,
105        value: Option<String>,
106    },
107    DirectoryName {
108        prompt: String,
109        default: Option<String>,
110        value: Option<String>,
111    },
112    Checkbox {
113        prompt: String,
114        default: Option<bool>,
115        value: bool,
116    },
117}
118
119impl FormInputField {
120    pub fn from_raw(value: &BNFormInputField) -> Self {
121        let prompt = raw_to_string(value.prompt).unwrap_or_default();
122        let view = match value.view.is_null() {
123            false => Some(unsafe { BinaryView::from_raw(value.view) }.to_owned()),
124            true => None,
125        };
126        let name_default = value
127            .hasDefault
128            .then_some(raw_to_string(value.defaultName).unwrap_or_default());
129        let string_default = value
130            .hasDefault
131            .then_some(raw_to_string(value.stringDefault).unwrap_or_default());
132        let int_default = value.hasDefault.then_some(value.intDefault);
133        let address_default = value.hasDefault.then_some(value.addressDefault);
134        let index_default = value.hasDefault.then_some(value.indexDefault);
135        let bool_default = int_default.map(|i| i != 0);
136        let extension = raw_to_string(value.ext);
137        let current_address = value.currentAddress;
138        let string_result = raw_to_string(value.stringResult);
139        let int_result = value.intResult;
140        let address_result = value.addressResult;
141        let index_result = value.indexResult;
142        match value.type_ {
143            BNFormInputFieldType::LabelFormField => Self::Label { prompt },
144            BNFormInputFieldType::SeparatorFormField => Self::Separator,
145            BNFormInputFieldType::TextLineFormField => Self::TextLine {
146                prompt,
147                default: string_default,
148                value: string_result,
149            },
150            BNFormInputFieldType::MultilineTextFormField => Self::MultilineText {
151                prompt,
152                default: string_default,
153                value: string_result,
154            },
155            BNFormInputFieldType::IntegerFormField => Self::Integer {
156                prompt,
157                default: int_default,
158                value: int_result,
159            },
160            BNFormInputFieldType::AddressFormField => Self::Address {
161                prompt,
162                view,
163                current_address,
164                default: address_default,
165                value: address_result,
166            },
167            BNFormInputFieldType::ChoiceFormField => Self::Choice {
168                prompt,
169                choices: vec![],
170                default: index_default,
171                value: index_result,
172            },
173            BNFormInputFieldType::OpenFileNameFormField => Self::OpenFileName {
174                prompt,
175                extension,
176                default: string_default,
177                value: string_result,
178            },
179            BNFormInputFieldType::SaveFileNameFormField => Self::SaveFileName {
180                prompt,
181                extension,
182                default: name_default,
183                value: string_result,
184            },
185            BNFormInputFieldType::DirectoryNameFormField => Self::DirectoryName {
186                prompt,
187                default: name_default,
188                value: string_result,
189            },
190            BNFormInputFieldType::CheckboxFormField => Self::Checkbox {
191                prompt,
192                default: bool_default,
193                value: value.intResult != 0,
194            },
195        }
196    }
197
198    pub fn from_owned_raw(value: BNFormInputField) -> Self {
199        let owned = Self::from_raw(&value);
200        Self::free_raw(value);
201        owned
202    }
203
204    pub fn into_raw(&self) -> BNFormInputField {
205        let bn_prompt = BnString::new(self.try_prompt().unwrap_or_default());
206        let bn_extension = BnString::new(self.try_extension().unwrap_or_default());
207        let bn_default_string = BnString::new(self.try_default_string().unwrap_or_default());
208        let bn_default_name = BnString::new(self.try_default_name().unwrap_or_default());
209        let bn_value_string = BnString::new(self.try_value_string().unwrap_or_default());
210        // Expected to be freed by [`FormInputField::free_raw`].
211        BNFormInputField {
212            type_: self.as_type(),
213            prompt: BnString::into_raw(bn_prompt),
214            view: match self.try_view() {
215                None => std::ptr::null_mut(),
216                Some(view) => unsafe { Ref::into_raw(view) }.handle,
217            },
218            currentAddress: self.try_current_address().unwrap_or_default(),
219            choices: match self.try_choices() {
220                None => std::ptr::null_mut(),
221                Some(choices) => strings_to_string_list(choices.as_slice()) as *mut *const c_char,
222            },
223            // NOTE: `count` is the length of the `choices` array.
224            count: self.try_choices().unwrap_or_default().len(),
225            ext: BnString::into_raw(bn_extension),
226            defaultName: BnString::into_raw(bn_default_name),
227            intResult: self.try_value_int().unwrap_or_default(),
228            addressResult: self.try_value_address().unwrap_or_default(),
229            stringResult: BnString::into_raw(bn_value_string),
230            indexResult: self.try_value_index().unwrap_or_default(),
231            hasDefault: self.try_has_default().unwrap_or_default(),
232            intDefault: self.try_default_int().unwrap_or_default(),
233            addressDefault: self.try_default_address().unwrap_or_default(),
234            stringDefault: BnString::into_raw(bn_default_string),
235            indexDefault: self.try_default_index().unwrap_or_default(),
236        }
237    }
238
239    pub fn free_raw(value: BNFormInputField) {
240        unsafe {
241            BnString::free_raw(value.defaultName as *mut c_char);
242            BnString::free_raw(value.prompt as *mut c_char);
243            BnString::free_raw(value.ext as *mut c_char);
244            BnString::free_raw(value.stringDefault as *mut c_char);
245            BnString::free_raw(value.stringResult);
246            // TODO: Would like access to a `Array::free_raw` or something.
247            Array::<BnString>::new(value.choices as *mut *mut c_char, value.count, ());
248            // Free the view ref if provided.
249            if !value.view.is_null() {
250                BinaryView::ref_from_raw(value.view);
251            }
252        }
253    }
254
255    pub fn as_type(&self) -> BNFormInputFieldType {
256        match self {
257            FormInputField::Label { .. } => BNFormInputFieldType::LabelFormField,
258            FormInputField::Separator => BNFormInputFieldType::SeparatorFormField,
259            FormInputField::TextLine { .. } => BNFormInputFieldType::TextLineFormField,
260            FormInputField::MultilineText { .. } => BNFormInputFieldType::MultilineTextFormField,
261            FormInputField::Integer { .. } => BNFormInputFieldType::IntegerFormField,
262            FormInputField::Address { .. } => BNFormInputFieldType::AddressFormField,
263            FormInputField::Choice { .. } => BNFormInputFieldType::ChoiceFormField,
264            FormInputField::OpenFileName { .. } => BNFormInputFieldType::OpenFileNameFormField,
265            FormInputField::SaveFileName { .. } => BNFormInputFieldType::SaveFileNameFormField,
266            FormInputField::DirectoryName { .. } => BNFormInputFieldType::DirectoryNameFormField,
267            FormInputField::Checkbox { .. } => BNFormInputFieldType::CheckboxFormField,
268        }
269    }
270
271    /// Mapping to the [`BNFormInputField::prompt`] field.
272    pub fn try_prompt(&self) -> Option<String> {
273        match self {
274            FormInputField::Label { prompt, .. } => Some(prompt.clone()),
275            FormInputField::Separator => None,
276            FormInputField::TextLine { prompt, .. } => Some(prompt.clone()),
277            FormInputField::MultilineText { prompt, .. } => Some(prompt.clone()),
278            FormInputField::Integer { prompt, .. } => Some(prompt.clone()),
279            FormInputField::Address { prompt, .. } => Some(prompt.clone()),
280            FormInputField::Choice { prompt, .. } => Some(prompt.clone()),
281            FormInputField::OpenFileName { prompt, .. } => Some(prompt.clone()),
282            FormInputField::SaveFileName { prompt, .. } => Some(prompt.clone()),
283            FormInputField::DirectoryName { prompt, .. } => Some(prompt.clone()),
284            FormInputField::Checkbox { prompt, .. } => Some(prompt.clone()),
285        }
286    }
287
288    /// Mapping to the [`BNFormInputField::view`] field.
289    pub fn try_view(&self) -> Option<Ref<BinaryView>> {
290        match self {
291            FormInputField::Address { view, .. } => view.clone(),
292            _ => None,
293        }
294    }
295
296    /// Mapping to the [`BNFormInputField::currentAddress`] field.
297    pub fn try_current_address(&self) -> Option<u64> {
298        match self {
299            FormInputField::Address {
300                current_address, ..
301            } => Some(*current_address),
302            _ => None,
303        }
304    }
305
306    /// Mapping to the [`BNFormInputField::choices`] field.
307    pub fn try_choices(&self) -> Option<Vec<String>> {
308        match self {
309            FormInputField::Choice { choices, .. } => Some(choices.clone()),
310            _ => None,
311        }
312    }
313
314    /// Mapping to the [`BNFormInputField::ext`] field.
315    pub fn try_extension(&self) -> Option<String> {
316        match self {
317            Self::SaveFileName { extension, .. } => extension.clone(),
318            Self::OpenFileName { extension, .. } => extension.clone(),
319            _ => None,
320        }
321    }
322
323    /// Mapping to the [`BNFormInputField::hasDefault`] field.
324    pub fn try_has_default(&self) -> Option<bool> {
325        match self {
326            FormInputField::Label { .. } => None,
327            FormInputField::Separator => None,
328            FormInputField::TextLine { default, .. } => Some(default.is_some()),
329            FormInputField::MultilineText { default, .. } => Some(default.is_some()),
330            FormInputField::Integer { default, .. } => Some(default.is_some()),
331            FormInputField::Address { default, .. } => Some(default.is_some()),
332            FormInputField::Choice { default, .. } => Some(default.is_some()),
333            FormInputField::OpenFileName { default, .. } => Some(default.is_some()),
334            FormInputField::SaveFileName { default, .. } => Some(default.is_some()),
335            FormInputField::DirectoryName { default, .. } => Some(default.is_some()),
336            FormInputField::Checkbox { default, .. } => Some(default.is_some()),
337        }
338    }
339
340    /// Mapping to the [`BNFormInputField::defaultName`] field.
341    pub fn try_default_name(&self) -> Option<String> {
342        match self {
343            Self::SaveFileName { default, .. } => default.clone(),
344            Self::DirectoryName { default, .. } => default.clone(),
345            _ => None,
346        }
347    }
348
349    /// Mapping to the [`BNFormInputField::intDefault`] field.
350    pub fn try_default_int(&self) -> Option<i64> {
351        match self {
352            FormInputField::Integer { default, .. } => *default,
353            FormInputField::Checkbox { default, .. } => default.map(|b| b as i64),
354            _ => None,
355        }
356    }
357
358    /// Mapping to the [`BNFormInputField::addressDefault`] field.
359    pub fn try_default_address(&self) -> Option<u64> {
360        match self {
361            FormInputField::Address { default, .. } => *default,
362            _ => None,
363        }
364    }
365
366    /// Mapping to the [`BNFormInputField::stringDefault`] field.
367    pub fn try_default_string(&self) -> Option<String> {
368        match self {
369            FormInputField::TextLine { default, .. } => default.clone(),
370            FormInputField::MultilineText { default, .. } => default.clone(),
371            FormInputField::OpenFileName { default, .. } => default.clone(),
372            FormInputField::SaveFileName { default, .. } => default.clone(),
373            FormInputField::DirectoryName { default, .. } => default.clone(),
374            _ => None,
375        }
376    }
377
378    /// Mapping to the [`BNFormInputField::indexDefault`] field.
379    pub fn try_default_index(&self) -> Option<usize> {
380        match self {
381            FormInputField::Choice { default, .. } => *default,
382            _ => None,
383        }
384    }
385
386    /// Mapping to the [`BNFormInputField::intResult`] field.
387    pub fn try_value_int(&self) -> Option<i64> {
388        match self {
389            FormInputField::Integer { value, .. } => Some(*value),
390            FormInputField::Checkbox { value, .. } => Some(*value as i64),
391            _ => None,
392        }
393    }
394
395    /// Mapping to the [`BNFormInputField::addressResult`] field.
396    pub fn try_value_address(&self) -> Option<u64> {
397        match self {
398            FormInputField::Address { value, .. } => Some(*value),
399            _ => None,
400        }
401    }
402
403    /// Mapping to the [`BNFormInputField::stringResult`] field.
404    pub fn try_value_string(&self) -> Option<String> {
405        match self {
406            FormInputField::TextLine { value, .. } => value.clone(),
407            FormInputField::MultilineText { value, .. } => value.clone(),
408            FormInputField::OpenFileName { value, .. } => value.clone(),
409            FormInputField::SaveFileName { value, .. } => value.clone(),
410            FormInputField::DirectoryName { value, .. } => value.clone(),
411            _ => None,
412        }
413    }
414
415    /// Mapping to the [`BNFormInputField::indexResult`] field.
416    pub fn try_value_index(&self) -> Option<usize> {
417        match self {
418            FormInputField::Choice { value, .. } => Some(*value),
419            _ => None,
420        }
421    }
422}