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