binaryninja/workflow/
activity.rs

1use std::{
2    ffi::{c_void, CString},
3    ptr::NonNull,
4};
5
6use binaryninjacore_sys::*;
7use serde_derive::{Deserialize, Serialize};
8
9use crate::{
10    rc::{Ref, RefCountable},
11    string::{BnString, IntoCStr},
12    workflow::AnalysisContext,
13};
14
15// TODO: This needs to be made into a trait similar to that of `Command`.
16/// An `Activity` represents a fundamental unit of work within a workflow. It encapsulates
17/// a specific analysis step or action as a callback function, which is augmented by a configuration.
18/// The configuration defines the activity's metadata, eligibility criteria, and execution semantics,
19/// allowing it to seamlessly integrate into the workflow system.
20///
21/// ```
22/// use binaryninja::workflow::{activity, Activity, AnalysisContext};
23///
24/// fn activity_callback(context: &AnalysisContext) {
25///     // Perform custom analysis using data provided in the context.
26/// }
27///
28/// let config = activity::Config::action(
29///     "example.analysis.analyzeFunction",
30///     "Analyze functions",
31///     "This activity performs custom analysis on each function"
32/// ).eligibility(activity::Eligibility::auto());
33/// let activity = Activity::new_with_action(config, activity_callback);
34///
35/// // Register the activity in a `Workflow`.
36/// ```
37///
38/// See [Activity Fundamentals](https://docs.binary.ninja/dev/workflows.html#activity-fundamentals) for more information.
39#[repr(transparent)]
40pub struct Activity {
41    pub(crate) handle: NonNull<BNActivity>,
42}
43
44impl Activity {
45    #[allow(unused)]
46    pub(crate) unsafe fn from_raw(handle: NonNull<BNActivity>) -> Self {
47        Self { handle }
48    }
49
50    pub(crate) unsafe fn ref_from_raw(handle: NonNull<BNActivity>) -> Ref<Self> {
51        Ref::new(Self { handle })
52    }
53
54    pub fn new(config: impl AsConfig) -> Ref<Self> {
55        unsafe extern "C" fn cb_action_nop(_: *mut c_void, _: *mut BNAnalysisContext) {}
56        let config = config.as_config();
57        let result =
58            unsafe { BNCreateActivity(config.as_ptr(), std::ptr::null_mut(), Some(cb_action_nop)) };
59        unsafe { Activity::ref_from_raw(NonNull::new(result).unwrap()) }
60    }
61
62    pub fn new_with_action<F>(config: impl AsConfig, mut action: F) -> Ref<Self>
63    where
64        F: FnMut(&AnalysisContext),
65    {
66        unsafe extern "C" fn cb_action<F: FnMut(&AnalysisContext)>(
67            ctxt: *mut c_void,
68            analysis: *mut BNAnalysisContext,
69        ) {
70            let ctxt = &mut *(ctxt as *mut F);
71            if let Some(analysis) = NonNull::new(analysis) {
72                ctxt(&AnalysisContext::from_raw(analysis))
73            }
74        }
75        let config = config.as_config();
76        let result = unsafe {
77            BNCreateActivity(
78                config.as_ptr(),
79                &mut action as *mut F as *mut c_void,
80                Some(cb_action::<F>),
81            )
82        };
83        unsafe { Activity::ref_from_raw(NonNull::new(result).unwrap()) }
84    }
85
86    pub fn name(&self) -> String {
87        let result = unsafe { BNActivityGetName(self.handle.as_ptr()) };
88        assert!(!result.is_null());
89        unsafe { BnString::into_string(result) }
90    }
91}
92
93impl ToOwned for Activity {
94    type Owned = Ref<Self>;
95
96    fn to_owned(&self) -> Self::Owned {
97        unsafe { RefCountable::inc_ref(self) }
98    }
99}
100
101unsafe impl RefCountable for Activity {
102    unsafe fn inc_ref(handle: &Self) -> Ref<Self> {
103        Ref::new(Self {
104            handle: NonNull::new(BNNewActivityReference(handle.handle.as_ptr()))
105                .expect("valid handle"),
106        })
107    }
108
109    unsafe fn dec_ref(handle: &Self) {
110        BNFreeActivity(handle.handle.as_ptr());
111    }
112}
113
114pub trait AsConfig {
115    fn as_config(&self) -> CString;
116}
117
118impl AsConfig for &str {
119    fn as_config(&self) -> std::ffi::CString {
120        self.to_cstr()
121    }
122}
123
124/// The configuration for an `Activity`, defining its metadata, eligibility criteria, and execution semantics.
125#[must_use]
126#[derive(Deserialize, Serialize, Debug)]
127pub struct Config {
128    /// A unique identifier for the activity.
129    pub name: String,
130
131    /// A human-readable title for the activity.
132    pub title: String,
133
134    /// A brief description of the activity's purpose and functionality.
135    pub description: String,
136
137    /// The role of the activity within the workflow, determining its behavior and interaction with other activities.
138    #[serde(default)]
139    pub role: Role,
140
141    /// Names by which this activity has previously been known.
142    #[serde(skip_serializing_if = "Vec::is_empty", default)]
143    pub aliases: Vec<String>,
144
145    /// The conditions that determine when the activity should execute.
146    #[serde(default)]
147    pub eligibility: Eligibility,
148
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub dependencies: Option<Dependencies>,
151}
152
153#[derive(Deserialize, Serialize, Debug)]
154pub struct Dependencies {
155    #[serde(skip_serializing_if = "Vec::is_empty", default)]
156    pub downstream: Vec<String>,
157}
158
159impl Config {
160    /// Creates a new instance with role [`Role::Action`] and the specified name, title, and description.
161    pub fn action(
162        name: impl Into<String>,
163        title: impl Into<String>,
164        description: impl Into<String>,
165    ) -> Self {
166        Self {
167            name: name.into(),
168            title: title.into(),
169            description: description.into(),
170            role: Role::Action,
171            aliases: Vec::new(),
172            eligibility: Eligibility::default(),
173            dependencies: None,
174        }
175    }
176
177    /// Sets the [`aliases`](field@Config::aliases) field, which contains names by which this activity has previously been known.
178    pub fn aliases<I, S>(mut self, aliases: I) -> Self
179    where
180        I: IntoIterator<Item = S>,
181        S: Into<String>,
182    {
183        self.aliases = aliases.into_iter().map(|s| s.into()).collect();
184        self
185    }
186
187    /// Sets the [`eligibility`](field@Config::eligibility) field, which defines the conditions under which this activity is eligible for execution.
188    pub fn eligibility(mut self, eligibility: Eligibility) -> Self {
189        self.eligibility = eligibility;
190        self
191    }
192
193    /// Sets the [`dependencies`](field@Config::dependencies) field to specify dependencies that should be triggered after this activity completes.
194    pub fn downstream_dependencies<I, S>(mut self, dependencies: I) -> Self
195    where
196        I: IntoIterator<Item = S>,
197        S: Into<String>,
198    {
199        self.dependencies = Some(Dependencies {
200            downstream: dependencies.into_iter().map(|s| s.into()).collect(),
201        });
202        self
203    }
204}
205
206impl AsConfig for &Config {
207    fn as_config(&self) -> CString {
208        serde_json::to_string(self)
209            .expect("Failed to serialize Config")
210            .to_cstr()
211    }
212}
213
214impl AsConfig for Config {
215    fn as_config(&self) -> CString {
216        (&self).as_config()
217    }
218}
219
220/// Defines the behavior of the activity in the workflow.
221///
222/// NOTE: Activities with the subflow role are only permitted in module workflows.
223/// Subflows are not supported within function workflows.
224#[derive(Deserialize, Serialize, Debug)]
225#[serde(rename_all = "camelCase")]
226#[derive(Default)]
227pub enum Role {
228    /// The default role; performs a specific task.
229    #[default]
230    Action,
231
232    /// Contains child activities and uses an eligibility handler to determine which child activities to execute.
233    /// This enables the ability to have a dynamic and reactive execution pipeline.
234    Selector,
235
236    /// Creates a new task context and asynchronously processes its workflow sub-graph on a new thread within
237    /// the workflow machine. The subflow executes asynchronously from the requestor, allowing the original
238    /// thread to return immediately. Within this context, multiple task actions can be enqueued, enabling
239    /// extensive parallel processing. After completing its workflow sub-graph, it enters a stall state,
240    /// waiting for all its asynchronous task actions to complete.
241    Subflow,
242
243    /// Asynchronously processes the workflow graph on a new thread within the workflow machine.
244    /// `Task` activities enable the pipeline to execute asynchronously from its requestor. `Task` activities
245    /// require a task context to be present; if no task context exists, they execute immediately in the
246    /// current thread.
247    Task,
248
249    Sequence,
250    Listener,
251}
252
253/// The conditions that determine when an activity should execute.
254#[must_use]
255#[derive(Deserialize, Serialize, Debug)]
256#[serde(rename_all = "camelCase")]
257pub struct Eligibility {
258    /// An object that automatically generates a boolean control setting and corresponding predicate.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub auto: Option<Auto>,
261
262    /// Indicates whether the activity should run only once across all file/analysis sessions.
263    /// Once the activity runs, its state is saved persistently, and it will not run again unless
264    /// explicitly reset. This is useful for activities that only need to be performed exactly once,
265    /// such as initial setup tasks.
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub run_once: Option<bool>,
268
269    /// Indicates whether the activity should run only once per session. Its state is not
270    /// persisted, so it will run again in a new session. This is useful for activities
271    /// that should be performed once per analysis session, such as initialization steps
272    /// specific to a particular execution context.
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub run_once_per_session: Option<bool>,
275
276    /// Indicates if a subflow is eligible for re-execution based on its eligibility logic.
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub continuation: Option<bool>,
279
280    /// Objects that define the condition that must be met for the activity to be eligible to run.
281    #[serde(skip_serializing_if = "Vec::is_empty")]
282    pub predicates: Vec<Predicate>,
283
284    /// Logical operator that defines how multiple predicates are combined.
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub logical_operator: Option<PredicateLogicalOperator>,
287}
288
289impl Eligibility {
290    /// Creates a new instance without an automatically generated boolean control setting.
291    /// The activity is eligible to run by default.
292    pub fn without_setting() -> Self {
293        Eligibility {
294            auto: None,
295            run_once: None,
296            run_once_per_session: None,
297            continuation: None,
298            predicates: vec![],
299            logical_operator: None,
300        }
301    }
302
303    /// Creates a new instance with an automatically generated boolean control setting and corresponding predicate.
304    /// The setting is enabled by default.
305    pub fn auto() -> Self {
306        Eligibility {
307            auto: Some(Auto::new()),
308            run_once: None,
309            run_once_per_session: None,
310            continuation: None,
311            predicates: vec![],
312            logical_operator: None,
313        }
314    }
315
316    /// Creates a new instance with an automatically generated boolean control setting and corresponding predicate.
317    /// The setting has the value `value` by default.
318    pub fn auto_with_default(value: bool) -> Self {
319        Eligibility {
320            auto: Some(Auto::new().default(value)),
321            run_once: None,
322            run_once_per_session: None,
323            continuation: None,
324            predicates: vec![],
325            logical_operator: None,
326        }
327    }
328
329    /// Sets the [`run_once`](field@Eligibility::run_once) field, indicating whether the activity should run only once across all file/analysis sessions.
330    pub fn run_once(mut self, value: bool) -> Self {
331        self.run_once = Some(value);
332        self
333    }
334
335    /// Sets the [`run_once_per_session`](field@Eligibility::run_once_per_session) field, indicating whether the activity should run only once per session.
336    pub fn run_once_per_session(mut self, value: bool) -> Self {
337        self.run_once_per_session = Some(value);
338        self
339    }
340
341    /// Sets the [`continuation`](field@Eligibility::continuation) field, indicating whether a subflow is eligible for re-execution based on its eligibility logic.
342    pub fn continuation(mut self, value: bool) -> Self {
343        self.continuation = Some(value);
344        self
345    }
346
347    /// Sets the predicate that must be satisfied for the activity to be eligible to run.
348    pub fn predicate(mut self, predicate: impl Into<Predicate>) -> Self {
349        self.predicates = vec![predicate.into()];
350        self
351    }
352
353    /// Sets the predicates that must be satisfied for the activity to be eligible to run.
354    /// If multiple predicates are provided, they are combined using a logical OR.
355    pub fn matching_any_predicate(mut self, predicates: &[Predicate]) -> Self {
356        self.predicates = predicates.to_vec();
357        self.logical_operator = Some(PredicateLogicalOperator::Or);
358        self
359    }
360
361    /// Sets the predicates that must be satisfied for the activity to be eligible to run.
362    /// If multiple predicates are provided, they are combined using a logical AND.
363    pub fn matching_all_predicates(mut self, predicates: &[Predicate]) -> Self {
364        self.predicates = predicates.to_vec();
365        self.logical_operator = Some(PredicateLogicalOperator::And);
366        self
367    }
368}
369
370impl Default for Eligibility {
371    fn default() -> Self {
372        Self::auto()
373    }
374}
375
376/// Represents the request for an automatically generated boolean control setting and corresponding predicate.
377#[must_use]
378#[derive(Deserialize, Serialize, Debug, Default)]
379pub struct Auto {
380    /// The default value for the setting. If `None`, the setting is enabled by default.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub default: Option<bool>,
383}
384
385impl Auto {
386    /// Creates a new `Auto` instance that represents a setting that is enabled by default.
387    pub fn new() -> Self {
388        Self { default: None }
389    }
390
391    /// Sets the `default` value for the setting.
392    pub fn default(mut self, value: bool) -> Self {
393        self.default = Some(value);
394        self
395    }
396}
397
398/// A predicate that can be used to determine the eligibility of an activity.
399///
400/// See [`ViewType`] and [`Setting`] for specific predicates that can be used.
401#[must_use]
402#[derive(Deserialize, Serialize, Debug, Clone)]
403pub struct Predicate {
404    #[serde(flatten)]
405    predicate_type: PredicateType,
406    operator: Operator,
407    value: serde_json::Value,
408}
409
410/// A predicate that checks the type of the [`BinaryView`](crate::binary_view::BinaryView).
411#[must_use]
412pub enum ViewType {
413    In(Vec<String>),
414    NotIn(Vec<String>),
415}
416
417impl ViewType {
418    /// Creates a new predicate that checks if the type of the [`BinaryView`](crate::binary_view::BinaryView)
419    /// _is_ in the provided list.
420    pub fn in_<I, S>(values: I) -> Self
421    where
422        I: IntoIterator<Item = S>,
423        S: AsRef<str>,
424    {
425        ViewType::In(values.into_iter().map(|s| s.as_ref().to_string()).collect())
426    }
427
428    /// Creates a new predicate that checks if the type of the [`BinaryView`](crate::binary_view::BinaryView)
429    /// _is not_ in the provided list.
430    pub fn not_in<I, S>(values: I) -> Self
431    where
432        I: IntoIterator<Item = S>,
433        S: AsRef<str>,
434    {
435        ViewType::NotIn(values.into_iter().map(|s| s.as_ref().to_string()).collect())
436    }
437}
438
439impl From<ViewType> for Predicate {
440    fn from(predicate: ViewType) -> Self {
441        match predicate {
442            ViewType::In(value) => Predicate {
443                predicate_type: PredicateType::ViewType,
444                operator: Operator::In,
445                value: serde_json::json!(value),
446            },
447            ViewType::NotIn(value) => Predicate {
448                predicate_type: PredicateType::ViewType,
449                operator: Operator::NotIn,
450                value: serde_json::json!(value),
451            },
452        }
453    }
454}
455
456/// A predicate that checks the platform of the [`BinaryView`](crate::binary_view::BinaryView).
457#[must_use]
458pub enum Platform {
459    In(Vec<String>),
460    NotIn(Vec<String>),
461}
462
463impl Platform {
464    /// Creates a new predicate that checks if the platform of the [`BinaryView`](crate::binary_view::BinaryView)
465    /// _is_ in the provided list.
466    pub fn in_<I, S>(values: I) -> Self
467    where
468        I: IntoIterator<Item = S>,
469        S: AsRef<str>,
470    {
471        Platform::In(values.into_iter().map(|s| s.as_ref().to_string()).collect())
472    }
473
474    /// Creates a new predicate that checks if the platform of the [`BinaryView`](crate::binary_view::BinaryView)
475    /// _is not_ in the provided list.
476    pub fn not_in<I, S>(values: I) -> Self
477    where
478        I: IntoIterator<Item = S>,
479        S: AsRef<str>,
480    {
481        Platform::NotIn(values.into_iter().map(|s| s.as_ref().to_string()).collect())
482    }
483}
484
485impl From<Platform> for Predicate {
486    fn from(predicate: Platform) -> Self {
487        match predicate {
488            Platform::In(value) => Predicate {
489                predicate_type: PredicateType::Platform,
490                operator: Operator::In,
491                value: serde_json::json!(value),
492            },
493            Platform::NotIn(value) => Predicate {
494                predicate_type: PredicateType::Platform,
495                operator: Operator::NotIn,
496                value: serde_json::json!(value),
497            },
498        }
499    }
500}
501
502/// A predicate that evaluates the value of a specific setting.
503#[must_use]
504pub struct Setting {
505    identifier: String,
506    operator: Operator,
507    value: serde_json::Value,
508}
509
510impl Setting {
511    /// Creates a new predicate that evaluates the value of a specific setting against `value` using `operator`.
512    pub fn new(
513        identifier: impl Into<String>,
514        operator: Operator,
515        value: impl serde::Serialize,
516    ) -> Self {
517        Self {
518            identifier: identifier.into(),
519            operator,
520            value: serde_json::json!(value),
521        }
522    }
523
524    /// Creates a new predicate that checks if the value of the setting is equal to `value`.
525    pub fn eq(identifier: impl Into<String>, value: impl serde::Serialize) -> Self {
526        Self::new(identifier, Operator::Eq, value)
527    }
528
529    /// Creates a new predicate that checks if the value of the setting is not equal to `value`.
530    pub fn ne(identifier: impl Into<String>, value: impl serde::Serialize) -> Self {
531        Self::new(identifier, Operator::Ne, value)
532    }
533
534    /// Creates a new predicate that checks if the value of the setting is less than `value`.
535    pub fn lt(identifier: impl Into<String>, value: impl serde::Serialize) -> Self {
536        Self::new(identifier, Operator::Lt, value)
537    }
538
539    /// Creates a new predicate that checks if the value of the setting is less than or equal to `value`.
540    pub fn lte(identifier: impl Into<String>, value: impl serde::Serialize) -> Self {
541        Self::new(identifier, Operator::Lte, value)
542    }
543
544    /// Creates a new predicate that checks if the value of the setting is greater than `value`.
545    pub fn gt(identifier: impl Into<String>, value: impl serde::Serialize) -> Self {
546        Self::new(identifier, Operator::Gt, value)
547    }
548
549    /// Creates a new predicate that checks if the value of the setting is greater than or equal to `value`.
550    pub fn gte(identifier: impl Into<String>, value: impl serde::Serialize) -> Self {
551        Self::new(identifier, Operator::Gte, value)
552    }
553
554    /// Creates a new predicate that checks if the value of the setting is in the provided list.
555    pub fn in_(identifier: impl Into<String>, value: impl serde::Serialize) -> Self {
556        Self::new(identifier, Operator::In, value)
557    }
558
559    /// Creates a new predicate that checks if the value of the setting is not in the provided list.
560    pub fn not_in(identifier: impl Into<String>, value: impl serde::Serialize) -> Self {
561        Self::new(identifier, Operator::NotIn, value)
562    }
563}
564
565impl From<Setting> for Predicate {
566    fn from(setting: Setting) -> Self {
567        Predicate {
568            predicate_type: PredicateType::Setting {
569                identifier: setting.identifier,
570            },
571            operator: setting.operator,
572            value: setting.value,
573        }
574    }
575}
576
577#[derive(Deserialize, Serialize, Debug, Clone)]
578#[serde(rename_all = "camelCase", tag = "type")]
579enum PredicateType {
580    Setting { identifier: String },
581    ViewType,
582    Platform,
583}
584
585#[derive(Deserialize, Serialize, Debug, Copy, Clone)]
586pub enum Operator {
587    #[serde(rename = "==")]
588    Eq,
589    #[serde(rename = "!=")]
590    Ne,
591    #[serde(rename = "<")]
592    Lt,
593    #[serde(rename = "<=")]
594    Lte,
595    #[serde(rename = ">")]
596    Gt,
597    #[serde(rename = ">=")]
598    Gte,
599    #[serde(rename = "in")]
600    In,
601    #[serde(rename = "not in")]
602    NotIn,
603}
604
605#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
606#[serde(rename_all = "camelCase")]
607pub enum PredicateLogicalOperator {
608    And,
609    Or,
610}