diff --git a/plugins/svd/src/lib.rs b/plugins/svd/src/lib.rs index b02b1ec2bd..4a611e3dd1 100644 --- a/plugins/svd/src/lib.rs +++ b/plugins/svd/src/lib.rs @@ -6,20 +6,9 @@ use crate::settings::LoadSettings; use binaryninja::binary_view::{BinaryView, BinaryViewBase, BinaryViewExt}; use binaryninja::command::Command; use binaryninja::logger::Logger; -use binaryninja::workflow::{Activity, AnalysisContext, Workflow}; +use binaryninja::workflow::{activity, Activity, AnalysisContext, Workflow}; use log::LevelFilter; -pub const LOADER_ACTIVITY_NAME: &str = "analysis.svd.loader"; -const LOADER_ACTIVITY_CONFIG: &str = r#"{ - "name": "analysis.svd.loader", - "title" : "SVD Loader", - "description": "This analysis step applies SVD info to the view...", - "eligibility": { - "auto": {}, - "runOnce": true - } -}"#; - struct LoadSVDFile; impl Command for LoadSVDFile { @@ -62,7 +51,10 @@ impl Command for LoadSVDFile { #[allow(non_snake_case)] #[cfg(not(feature = "demo"))] pub extern "C" fn CorePluginInit() -> bool { - plugin_init(); + if plugin_init().is_err() { + log::error!("Failed to initialize SVD plug-in"); + return false; + } true } @@ -70,11 +62,14 @@ pub extern "C" fn CorePluginInit() -> bool { #[allow(non_snake_case)] #[cfg(feature = "demo")] pub extern "C" fn SVDPluginInit() -> bool { - plugin_init(); + if plugin_init().is_err() { + log::error!("Failed to initialize SVD plug-in"); + return false; + } true } -fn plugin_init() { +fn plugin_init() -> Result<(), ()> { Logger::new("SVD").with_level(LevelFilter::Debug).init(); binaryninja::command::register_command( @@ -113,12 +108,16 @@ fn plugin_init() { }; // Register new workflow activity to load svd information. - let old_module_meta_workflow = Workflow::instance("core.module.metaAnalysis"); - let module_meta_workflow = old_module_meta_workflow.clone_to("core.module.metaAnalysis"); - let loader_activity = Activity::new_with_action(LOADER_ACTIVITY_CONFIG, loader_activity); - module_meta_workflow - .register_activity(&loader_activity) - .unwrap(); - module_meta_workflow.insert("core.module.loadDebugInfo", [LOADER_ACTIVITY_NAME]); - module_meta_workflow.register().unwrap(); + let loader_config = activity::Config::action( + "analysis.svd.loader", + "SVD Loader", + "This analysis step applies SVD info to the view...", + ) + .eligibility(activity::Eligibility::auto().run_once(true)); + let loader_activity = Activity::new_with_action(loader_config, loader_activity); + Workflow::cloned("core.module.metaAnalysis") + .ok_or(())? + .activity_before(&loader_activity, "core.module.loadDebugInfo")? + .register()?; + Ok(()) } diff --git a/plugins/warp/src/plugin.rs b/plugins/warp/src/plugin.rs index 3306801d92..ee778062a2 100644 --- a/plugins/warp/src/plugin.rs +++ b/plugins/warp/src/plugin.rs @@ -102,7 +102,10 @@ pub extern "C" fn CorePluginInit() -> bool { // Register our highlight render layer. HighlightRenderLayer::register(); - workflow::insert_workflow(); + if workflow::insert_workflow().is_err() { + log::error!("Failed to register WARP workflow"); + return false; + } // TODO: Make the retrieval of containers wait on this to be done. // TODO: We could also have a mechanism for lazily loading the files using the chunk header target. diff --git a/plugins/warp/src/plugin/workflow.rs b/plugins/warp/src/plugin/workflow.rs index 2a8f6287eb..735706cf8b 100644 --- a/plugins/warp/src/plugin/workflow.rs +++ b/plugins/warp/src/plugin/workflow.rs @@ -11,7 +11,7 @@ use binaryninja::background_task::BackgroundTask; use binaryninja::binary_view::{BinaryView, BinaryViewExt}; use binaryninja::command::Command; use binaryninja::settings::{QueryOptions, Settings}; -use binaryninja::workflow::{Activity, AnalysisContext, Workflow}; +use binaryninja::workflow::{activity, Activity, AnalysisContext, Workflow, WorkflowBuilder}; use itertools::Itertools; use std::collections::HashMap; use std::time::Instant; @@ -19,41 +19,7 @@ use warp::r#type::class::function::{Location, RegisterLocation, StackLocation}; use warp::signature::function::{Function, FunctionGUID}; use warp::target::Target; -pub const APPLY_ACTIVITY_NAME: &str = "analysis.warp.apply"; -const APPLY_ACTIVITY_CONFIG: &str = r#"{ - "name": "analysis.warp.apply", - "title" : "WARP Apply Matched", - "description": "This analysis step applies WARP info to matched functions...", - "eligibility": { - "auto": {}, - "runOnce": false - } -}"#; - -pub const MATCHER_ACTIVITY_NAME: &str = "analysis.warp.matcher"; -const MATCHER_ACTIVITY_CONFIG: &str = r#"{ - "name": "analysis.warp.matcher", - "title" : "WARP Matcher", - "description": "This analysis step attempts to find matching WARP functions after the initial analysis is complete...", - "eligibility": { - "auto": {}, - "runOnce": true - }, - "dependencies": { - "downstream": ["core.module.update"] - } -}"#; - pub const GUID_ACTIVITY_NAME: &str = "analysis.warp.guid"; -const GUID_ACTIVITY_CONFIG: &str = r#"{ - "name": "analysis.warp.guid", - "title" : "WARP GUID Generator", - "description": "This analysis step generates the GUID for all analyzed functions...", - "eligibility": { - "auto": {}, - "runOnce": false - } -}"#; pub struct RunMatcher; @@ -189,7 +155,7 @@ pub fn run_matcher(view: &BinaryView) { view.update_analysis(); } -pub fn insert_workflow() { +pub fn insert_workflow() -> Result<(), ()> { // TODO: Note: because of symbol persistence function symbol is applied in `insert_cached_function_match`. // TODO: Comments are also applied there, they are "user" like, persisted and make undo actions. // "Hey look, it's a plier" ~ Josh 2025 @@ -259,29 +225,51 @@ pub fn insert_workflow() { } }; - let guid_activity = Activity::new_with_action(GUID_ACTIVITY_CONFIG, guid_activity); - let apply_activity = Activity::new_with_action(APPLY_ACTIVITY_CONFIG, apply_activity); + let guid_config = activity::Config::action( + GUID_ACTIVITY_NAME, + "WARP GUID Generator", + "This analysis step generates the GUID for all analyzed functions...", + ) + .eligibility(activity::Eligibility::auto().run_once(false)); + let guid_activity = Activity::new_with_action(&guid_config, guid_activity); + + let apply_config = activity::Config::action( + "analysis.warp.apply", + "WARP Apply Matched", + "This analysis step applies WARP info to matched functions...", + ) + .eligibility(activity::Eligibility::auto().run_once(false)); + let apply_activity = Activity::new_with_action(&apply_config, apply_activity); - let add_function_activities = |workflow: &Workflow| { - let new_workflow = workflow.clone_to(&workflow.name()); - new_workflow.register_activity(&guid_activity).unwrap(); - new_workflow.register_activity(&apply_activity).unwrap(); - new_workflow.insert_after("core.function.runFunctionRecognizers", [GUID_ACTIVITY_NAME]); - new_workflow.insert_after("core.function.generateMediumLevelIL", [APPLY_ACTIVITY_NAME]); - new_workflow.register().unwrap(); + let add_function_activities = |workflow: Option| -> Result<(), ()> { + let Some(workflow) = workflow else { + return Ok(()); + }; + + workflow + .activity_after(&guid_activity, "core.function.runFunctionRecognizers")? + .activity_after(&apply_activity, "core.function.generateMediumLevelIL")? + .register()?; + Ok(()) }; - add_function_activities(&Workflow::instance("core.function.metaAnalysis")); + add_function_activities(Workflow::cloned("core.function.metaAnalysis"))?; // TODO: Remove this once the objectivec workflow is registered on the meta workflow. - add_function_activities(&Workflow::instance("core.function.objectiveC")); + add_function_activities(Workflow::cloned("core.function.objectiveC"))?; - let old_module_meta_workflow = Workflow::instance("core.module.metaAnalysis"); - let module_meta_workflow = old_module_meta_workflow.clone_to("core.module.metaAnalysis"); - let matcher_activity = Activity::new_with_action(MATCHER_ACTIVITY_CONFIG, matcher_activity); + let matcher_config = activity::Config::action( + "analysis.warp.matcher", + "WARP Matcher", + "This analysis step attempts to find matching WARP functions after the initial analysis is complete...", + ) + .eligibility(activity::Eligibility::auto().run_once(true)) // Matcher activity must have core.module.update as subactivity otherwise analysis will sometimes never retrigger. - module_meta_workflow - .register_activity(&matcher_activity) - .unwrap(); - module_meta_workflow.insert("core.module.finishUpdate", [MATCHER_ACTIVITY_NAME]); - module_meta_workflow.register().unwrap(); + .downstream_dependencies(["core.module.update"]); + let matcher_activity = Activity::new_with_action(&matcher_config, matcher_activity); + Workflow::cloned("core.module.metaAnalysis") + .ok_or(())? + .activity_before(&matcher_activity, "core.module.finishUpdate")? + .register()?; + + Ok(()) } diff --git a/rust/examples/workflow.rs b/rust/examples/workflow.rs index 4956057935..db0bf59986 100644 --- a/rust/examples/workflow.rs +++ b/rust/examples/workflow.rs @@ -52,7 +52,7 @@ pub fn main() { binaryninja::headless::Session::new().expect("Failed to initialize session"); println!("Registering workflow..."); - let old_meta_workflow = Workflow::instance("core.function.metaAnalysis"); + let old_meta_workflow = Workflow::get("core.function.metaAnalysis"); let meta_workflow = old_meta_workflow.clone_to("core.function.metaAnalysis"); let activity = Activity::new_with_action(RUST_ACTIVITY_CONFIG, example_activity); meta_workflow.register_activity(&activity).unwrap(); diff --git a/rust/src/workflow.rs b/rust/src/workflow.rs index 1049cc2f24..2c03a4829d 100644 --- a/rust/src/workflow.rs +++ b/rust/src/workflow.rs @@ -9,10 +9,13 @@ use crate::low_level_il::{LowLevelILMutableFunction, LowLevelILRegularFunction}; use crate::medium_level_il::MediumLevelILFunction; use crate::rc::{Array, CoreArrayProvider, CoreArrayProviderInner, Guard, Ref, RefCountable}; use crate::string::{BnString, IntoCStr}; -use std::ffi::{c_char, c_void}; +use std::ffi::c_char; use std::ptr; use std::ptr::NonNull; +pub mod activity; +pub use activity::Activity; + #[repr(transparent)] /// The AnalysisContext struct is used to represent the current state of /// analysis for a given function. It allows direct modification of IL and other @@ -145,83 +148,6 @@ unsafe impl RefCountable for AnalysisContext { } } -// TODO: This needs to be made into a trait similar to that of `Command`. -#[repr(transparent)] -pub struct Activity { - handle: NonNull, -} - -impl Activity { - #[allow(unused)] - pub(crate) unsafe fn from_raw(handle: NonNull) -> Self { - Self { handle } - } - - pub(crate) unsafe fn ref_from_raw(handle: NonNull) -> Ref { - Ref::new(Self { handle }) - } - - pub fn new(config: &str) -> Ref { - unsafe extern "C" fn cb_action_nop(_: *mut c_void, _: *mut BNAnalysisContext) {} - let config = config.to_cstr(); - let result = - unsafe { BNCreateActivity(config.as_ptr(), std::ptr::null_mut(), Some(cb_action_nop)) }; - unsafe { Activity::ref_from_raw(NonNull::new(result).unwrap()) } - } - - pub fn new_with_action(config: &str, mut action: F) -> Ref - where - F: FnMut(&AnalysisContext), - { - unsafe extern "C" fn cb_action( - ctxt: *mut c_void, - analysis: *mut BNAnalysisContext, - ) { - let ctxt = &mut *(ctxt as *mut F); - if let Some(analysis) = NonNull::new(analysis) { - ctxt(&AnalysisContext::from_raw(analysis)) - } - } - let config = config.to_cstr(); - let result = unsafe { - BNCreateActivity( - config.as_ptr(), - &mut action as *mut F as *mut c_void, - Some(cb_action::), - ) - }; - unsafe { Activity::ref_from_raw(NonNull::new(result).unwrap()) } - } - - pub fn name(&self) -> String { - let result = unsafe { BNActivityGetName(self.handle.as_ptr()) }; - assert!(!result.is_null()); - unsafe { BnString::into_string(result) } - } -} - -impl ToOwned for Activity { - type Owned = Ref; - - fn to_owned(&self) -> Self::Owned { - unsafe { RefCountable::inc_ref(self) } - } -} - -unsafe impl RefCountable for Activity { - unsafe fn inc_ref(handle: &Self) -> Ref { - Ref::new(Self { - handle: NonNull::new(BNNewActivityReference(handle.handle.as_ptr())) - .expect("valid handle"), - }) - } - - unsafe fn dec_ref(handle: &Self) { - BNFreeActivity(handle.handle.as_ptr()); - } -} - -// TODO: We need to hide the JSON here behind a sensible/typed API. #[repr(transparent)] pub struct Workflow { handle: NonNull, @@ -237,19 +163,22 @@ impl Workflow { } /// Create a new unregistered [Workflow] with no activities. + /// Returns a [WorkflowBuilder] that can be used to configure and register the new [Workflow]. /// /// To get a copy of an existing registered [Workflow] use [Workflow::clone_to]. - pub fn new(name: &str) -> Ref { + pub fn build(name: &str) -> WorkflowBuilder { let name = name.to_cstr(); let result = unsafe { BNCreateWorkflow(name.as_ptr()) }; - unsafe { Workflow::ref_from_raw(NonNull::new(result).unwrap()) } + WorkflowBuilder { + handle: unsafe { Workflow::ref_from_raw(NonNull::new(result).unwrap()) }, + } } /// Make a new unregistered [Workflow], copying all activities and the execution strategy. + /// Returns a [WorkflowBuilder] that can be used to configure and register the new [Workflow]. /// /// * `name` - the name for the new [Workflow] - #[must_use] - pub fn clone_to(&self, name: &str) -> Ref { + pub fn clone_to(&self, name: &str) -> WorkflowBuilder { self.clone_to_with_root(name, "") } @@ -257,11 +186,10 @@ impl Workflow { /// /// * `name` - the name for the new [Workflow] /// * `root_activity` - perform the clone operation with this activity as the root - #[must_use] - pub fn clone_to_with_root(&self, name: &str, root_activity: &str) -> Ref { + pub fn clone_to_with_root(&self, name: &str, root_activity: &str) -> WorkflowBuilder { let raw_name = name.to_cstr(); let activity = root_activity.to_cstr(); - unsafe { + let workflow = unsafe { Self::ref_from_raw( NonNull::new(BNWorkflowClone( self.handle.as_ptr(), @@ -270,13 +198,23 @@ impl Workflow { )) .unwrap(), ) - } + }; + WorkflowBuilder { handle: workflow } } - pub fn instance(name: &str) -> Ref { + /// Get an existing [Workflow] by name. + pub fn get(name: &str) -> Option> { + // TODO: BNWorkflowInstance has get-or-create semantics. There is currently no way to just get. let name = name.to_cstr(); let result = unsafe { BNWorkflowInstance(name.as_ptr()) }; - unsafe { Workflow::ref_from_raw(NonNull::new(result).unwrap()) } + let handle = NonNull::new(result)?; + Some(unsafe { Workflow::ref_from_raw(handle) }) + } + + /// Clone the existing [Workflow] named `name`. + /// Returns a [WorkflowBuilder] that can be used to configure and register the new [Workflow]. + pub fn cloned(name: &str) -> Option { + Self::get(name).map(|workflow| workflow.clone_to(name)) } /// List of all registered [Workflow]'s @@ -293,58 +231,6 @@ impl Workflow { unsafe { BnString::into_string(result) } } - /// Register this [Workflow], making it immutable and available for use. - pub fn register(&self) -> Result<(), ()> { - self.register_with_config("") - } - - /// Register this [Workflow], making it immutable and available for use. - /// - /// * `configuration` - a JSON representation of the workflow configuration - pub fn register_with_config(&self, config: &str) -> Result<(), ()> { - let config = config.to_cstr(); - if unsafe { BNRegisterWorkflow(self.handle.as_ptr(), config.as_ptr()) } { - Ok(()) - } else { - Err(()) - } - } - - /// Register an [Activity] with this Workflow. - /// - /// * `activity` - the [Activity] to register - pub fn register_activity(&self, activity: &Activity) -> Result, ()> { - self.register_activity_with_subactivities::>(activity, vec![]) - } - - /// Register an [Activity] with this Workflow. - /// - /// * `activity` - the [Activity] to register - /// * `subactivities` - the list of Activities to assign - pub fn register_activity_with_subactivities( - &self, - activity: &Activity, - subactivities: I, - ) -> Result, ()> - where - I: IntoIterator, - I::Item: IntoCStr, - { - let subactivities_raw: Vec<_> = subactivities.into_iter().map(|x| x.to_cstr()).collect(); - let mut subactivities_ptr: Vec<*const _> = - subactivities_raw.iter().map(|x| x.as_ptr()).collect(); - let result = unsafe { - BNWorkflowRegisterActivity( - self.handle.as_ptr(), - activity.handle.as_ptr(), - subactivities_ptr.as_mut_ptr(), - subactivities_ptr.len(), - ) - }; - let activity_ptr = NonNull::new(result).ok_or(())?; - unsafe { Ok(Activity::ref_from_raw(activity_ptr)) } - } - /// Determine if an Activity exists in this [Workflow]. pub fn contains(&self, activity: &str) -> bool { let activity = activity.to_cstr(); @@ -416,11 +302,166 @@ impl Workflow { unsafe { Array::new(result as *mut *mut c_char, count, ()) } } + /// Generate a FlowGraph object for the current [Workflow] and optionally show it in the UI. + /// + /// * `activity` - if specified, generate the Flowgraph using `activity` as the root + /// * `sequential` - whether to generate a **Composite** or **Sequential** style graph + pub fn graph(&self, activity: &str, sequential: Option) -> Option> { + let sequential = sequential.unwrap_or(false); + let activity = activity.to_cstr(); + let graph = + unsafe { BNWorkflowGetGraph(self.handle.as_ptr(), activity.as_ptr(), sequential) }; + if graph.is_null() { + return None; + } + Some(unsafe { FlowGraph::ref_from_raw(graph) }) + } + + /// Not yet implemented. + pub fn show_metrics(&self) { + unsafe { BNWorkflowShowReport(self.handle.as_ptr(), c"metrics".as_ptr()) } + } + + /// Show the Workflow topology in the UI. + pub fn show_topology(&self) { + unsafe { BNWorkflowShowReport(self.handle.as_ptr(), c"topology".as_ptr()) } + } + + /// Not yet implemented. + pub fn show_trace(&self) { + unsafe { BNWorkflowShowReport(self.handle.as_ptr(), c"trace".as_ptr()) } + } +} + +impl ToOwned for Workflow { + type Owned = Ref; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for Workflow { + unsafe fn inc_ref(handle: &Self) -> Ref { + Ref::new(Self { + handle: NonNull::new(BNNewWorkflowReference(handle.handle.as_ptr())) + .expect("valid handle"), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeWorkflow(handle.handle.as_ptr()); + } +} + +impl CoreArrayProvider for Workflow { + type Raw = *mut BNWorkflow; + type Context = (); + type Wrapped<'a> = Guard<'a, Workflow>; +} + +unsafe impl CoreArrayProviderInner for Workflow { + unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { + BNFreeWorkflowList(raw, count) + } + + unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, context: &'a Self::Context) -> Self::Wrapped<'a> { + Guard::new( + Workflow::from_raw(NonNull::new(*raw).expect("valid handle")), + context, + ) + } +} + +#[must_use = "Workflow is not registered until `register` is called"] +pub struct WorkflowBuilder { + handle: Ref, +} + +impl WorkflowBuilder { + fn raw_handle(&self) -> *mut BNWorkflow { + self.handle.handle.as_ptr() + } + + /// Register an [Activity] with this Workflow and insert it before the designated position. + /// + /// * `activity` - the [Activity] to register + /// * `sibling` - the activity to insert the new activity before + pub fn activity_before(self, activity: &Activity, sibling: &str) -> Result { + self.register_activity(activity)? + .insert(sibling, vec![activity.name()]) + } + + /// Register an [Activity] with this Workflow and insert it in the designated position. + /// + /// * `activity` - the [Activity] to register + /// * `sibling` - the activity to insert the new activity after + pub fn activity_after(self, activity: &Activity, sibling: &str) -> Result { + self.register_activity(activity)? + .insert_after(sibling, vec![activity.name()]) + } + + /// Register an [Activity] with this Workflow. + /// + /// * `activity` - the [Activity] to register + pub fn register_activity(self, activity: &Activity) -> Result { + self.register_activity_with_subactivities::>(activity, vec![]) + } + + /// Register an [Activity] with this Workflow. + /// + /// * `activity` - the [Activity] to register + /// * `subactivities` - the list of Activities to assign + pub fn register_activity_with_subactivities( + self, + activity: &Activity, + subactivities: I, + ) -> Result + where + I: IntoIterator, + I::Item: IntoCStr, + { + let subactivities_raw: Vec<_> = subactivities.into_iter().map(|x| x.to_cstr()).collect(); + let mut subactivities_ptr: Vec<*const _> = + subactivities_raw.iter().map(|x| x.as_ptr()).collect(); + let result = unsafe { + BNWorkflowRegisterActivity( + self.raw_handle(), + activity.handle.as_ptr(), + subactivities_ptr.as_mut_ptr(), + subactivities_ptr.len(), + ) + }; + let Some(activity_ptr) = NonNull::new(result) else { + return Err(()); + }; + let _ = unsafe { Activity::ref_from_raw(activity_ptr) }; + Ok(self) + } + + /// Register this [Workflow], making it immutable and available for use. + pub fn register(self) -> Result, ()> { + self.register_with_config("") + } + + /// Register this [Workflow], making it immutable and available for use. + /// + /// * `configuration` - a JSON representation of the workflow configuration + pub fn register_with_config(self, config: &str) -> Result, ()> { + // TODO: We need to hide the JSON here behind a sensible/typed API. + let config = config.to_cstr(); + if unsafe { BNRegisterWorkflow(self.raw_handle(), config.as_ptr()) } { + Ok(self.handle) + } else { + Err(()) + } + } + /// Assign the list of `activities` as the new set of children for the specified `activity`. /// /// * `activity` - the Activity node to assign children /// * `activities` - the list of Activities to assign - pub fn assign_subactivities(&self, activity: &str, activities: I) -> bool + pub fn subactivities(self, activity: &str, activities: I) -> Result where I: IntoIterator, I::Item: IntoCStr, @@ -428,26 +469,36 @@ impl Workflow { let activity = activity.to_cstr(); let input_list: Vec<_> = activities.into_iter().map(|a| a.to_cstr()).collect(); let mut input_list_ptr: Vec<*const _> = input_list.iter().map(|x| x.as_ptr()).collect(); - unsafe { + let result = unsafe { BNWorkflowAssignSubactivities( - self.handle.as_ptr(), + self.raw_handle(), activity.as_ptr(), input_list_ptr.as_mut_ptr(), input_list.len(), ) + }; + if result { + Ok(self) + } else { + Err(()) } } /// Remove all Activity nodes from this [Workflow]. - pub fn clear(&self) -> bool { - unsafe { BNWorkflowClear(self.handle.as_ptr()) } + pub fn clear(self) -> Result { + let result = unsafe { BNWorkflowClear(self.raw_handle()) }; + if result { + Ok(self) + } else { + Err(()) + } } /// Insert the list of `activities` before the specified `activity` and at the same level. /// /// * `activity` - the Activity node for which to insert `activities` before /// * `activities` - the list of Activities to insert - pub fn insert(&self, activity: &str, activities: I) -> bool + pub fn insert(self, activity: &str, activities: I) -> Result where I: IntoIterator, I::Item: IntoCStr, @@ -455,13 +506,18 @@ impl Workflow { let activity = activity.to_cstr(); let input_list: Vec<_> = activities.into_iter().map(|a| a.to_cstr()).collect(); let mut input_list_ptr: Vec<*const _> = input_list.iter().map(|x| x.as_ptr()).collect(); - unsafe { + let result = unsafe { BNWorkflowInsert( - self.handle.as_ptr(), + self.raw_handle(), activity.as_ptr(), input_list_ptr.as_mut_ptr(), input_list.len(), ) + }; + if result { + Ok(self) + } else { + Err(()) } } @@ -469,7 +525,7 @@ impl Workflow { /// /// * `activity` - the Activity node for which to insert `activities` after /// * `activities` - the list of Activities to insert - pub fn insert_after(&self, activity: &str, activities: I) -> bool + pub fn insert_after(self, activity: &str, activities: I) -> Result where I: IntoIterator, I::Item: IntoCStr, @@ -477,105 +533,46 @@ impl Workflow { let activity = activity.to_cstr(); let input_list: Vec<_> = activities.into_iter().map(|a| a.to_cstr()).collect(); let mut input_list_ptr: Vec<*const _> = input_list.iter().map(|x| x.as_ptr()).collect(); - unsafe { + let result = unsafe { BNWorkflowInsertAfter( - self.handle.as_ptr(), + self.raw_handle(), activity.as_ptr(), input_list_ptr.as_mut_ptr(), input_list.len(), ) + }; + if result { + Ok(self) + } else { + Err(()) } } /// Remove the specified `activity` - pub fn remove(&self, activity: &str) -> bool { + pub fn remove(self, activity: &str) -> Result { let activity = activity.to_cstr(); - unsafe { BNWorkflowRemove(self.handle.as_ptr(), activity.as_ptr()) } + let result = unsafe { BNWorkflowRemove(self.raw_handle(), activity.as_ptr()) }; + if result { + Ok(self) + } else { + Err(()) + } } /// Replace the specified `activity`. /// /// * `activity` - the Activity to replace /// * `new_activity` - the replacement Activity - pub fn replace(&self, activity: &str, new_activity: &str) -> bool { + pub fn replace(self, activity: &str, new_activity: &str) -> Result { let activity = activity.to_cstr(); let new_activity = new_activity.to_cstr(); - unsafe { - BNWorkflowReplace( - self.handle.as_ptr(), - activity.as_ptr(), - new_activity.as_ptr(), - ) - } - } - - /// Generate a FlowGraph object for the current [Workflow] and optionally show it in the UI. - /// - /// * `activity` - if specified, generate the Flowgraph using `activity` as the root - /// * `sequential` - whether to generate a **Composite** or **Sequential** style graph - pub fn graph(&self, activity: &str, sequential: Option) -> Option> { - let sequential = sequential.unwrap_or(false); - let activity = activity.to_cstr(); - let graph = - unsafe { BNWorkflowGetGraph(self.handle.as_ptr(), activity.as_ptr(), sequential) }; - if graph.is_null() { - return None; + let result = unsafe { + BNWorkflowReplace(self.raw_handle(), activity.as_ptr(), new_activity.as_ptr()) + }; + if result { + Ok(self) + } else { + Err(()) } - Some(unsafe { FlowGraph::ref_from_raw(graph) }) - } - - /// Not yet implemented. - pub fn show_metrics(&self) { - unsafe { BNWorkflowShowReport(self.handle.as_ptr(), c"metrics".as_ptr()) } - } - - /// Show the Workflow topology in the UI. - pub fn show_topology(&self) { - unsafe { BNWorkflowShowReport(self.handle.as_ptr(), c"topology".as_ptr()) } - } - - /// Not yet implemented. - pub fn show_trace(&self) { - unsafe { BNWorkflowShowReport(self.handle.as_ptr(), c"trace".as_ptr()) } - } -} - -impl ToOwned for Workflow { - type Owned = Ref; - - fn to_owned(&self) -> Self::Owned { - unsafe { RefCountable::inc_ref(self) } - } -} - -unsafe impl RefCountable for Workflow { - unsafe fn inc_ref(handle: &Self) -> Ref { - Ref::new(Self { - handle: NonNull::new(BNNewWorkflowReference(handle.handle.as_ptr())) - .expect("valid handle"), - }) - } - - unsafe fn dec_ref(handle: &Self) { - BNFreeWorkflow(handle.handle.as_ptr()); - } -} - -impl CoreArrayProvider for Workflow { - type Raw = *mut BNWorkflow; - type Context = (); - type Wrapped<'a> = Guard<'a, Workflow>; -} - -unsafe impl CoreArrayProviderInner for Workflow { - unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) { - BNFreeWorkflowList(raw, count) - } - - unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, context: &'a Self::Context) -> Self::Wrapped<'a> { - Guard::new( - Workflow::from_raw(NonNull::new(*raw).expect("valid handle")), - context, - ) } } diff --git a/rust/src/workflow/activity.rs b/rust/src/workflow/activity.rs new file mode 100644 index 0000000000..25f7bdf59c --- /dev/null +++ b/rust/src/workflow/activity.rs @@ -0,0 +1,550 @@ +use std::{ + ffi::{c_void, CString}, + ptr::NonNull, +}; + +use binaryninjacore_sys::*; +use serde_derive::{Deserialize, Serialize}; + +use crate::{ + rc::{Ref, RefCountable}, + string::{BnString, IntoCStr}, + workflow::AnalysisContext, +}; + +// TODO: This needs to be made into a trait similar to that of `Command`. +/// An `Activity` represents a fundamental unit of work within a workflow. It encapsulates +/// a specific analysis step or action as a callback function, which is augmented by a configuration. +/// The configuration defines the activity's metadata, eligibility criteria, and execution semantics, +/// allowing it to seamlessly integrate into the workflow system. +/// +/// ``` +/// use binaryninja::workflow::{activity, Activity, AnalysisContext}; +/// +/// fn activity_callback(context: &AnalysisContext) { +/// // Perform custom analysis using data provided in the context. +/// } +/// +/// let config = activity::Config::action( +/// "example.analysis.analyzeFunction", +/// "Analyze functions", +/// "This activity performs custom analysis on each function" +/// ).eligibility(activity::Eligibility::auto()); +/// let activity = Activity::new_with_action(config, activity_callback); +/// +/// // Register the activity in a `Workflow`. +/// ``` +/// +/// See [Activity Fundamentals](https://docs.binary.ninja/dev/workflows.html#activity-fundamentals) for more information. +#[repr(transparent)] +pub struct Activity { + pub(crate) handle: NonNull, +} + +impl Activity { + #[allow(unused)] + pub(crate) unsafe fn from_raw(handle: NonNull) -> Self { + Self { handle } + } + + pub(crate) unsafe fn ref_from_raw(handle: NonNull) -> Ref { + Ref::new(Self { handle }) + } + + pub fn new(config: impl AsConfig) -> Ref { + unsafe extern "C" fn cb_action_nop(_: *mut c_void, _: *mut BNAnalysisContext) {} + let config = config.as_config(); + let result = + unsafe { BNCreateActivity(config.as_ptr(), std::ptr::null_mut(), Some(cb_action_nop)) }; + unsafe { Activity::ref_from_raw(NonNull::new(result).unwrap()) } + } + + pub fn new_with_action(config: impl AsConfig, mut action: F) -> Ref + where + F: FnMut(&AnalysisContext), + { + unsafe extern "C" fn cb_action( + ctxt: *mut c_void, + analysis: *mut BNAnalysisContext, + ) { + let ctxt = &mut *(ctxt as *mut F); + if let Some(analysis) = NonNull::new(analysis) { + ctxt(&AnalysisContext::from_raw(analysis)) + } + } + let config = config.as_config(); + let result = unsafe { + BNCreateActivity( + config.as_ptr(), + &mut action as *mut F as *mut c_void, + Some(cb_action::), + ) + }; + unsafe { Activity::ref_from_raw(NonNull::new(result).unwrap()) } + } + + pub fn name(&self) -> String { + let result = unsafe { BNActivityGetName(self.handle.as_ptr()) }; + assert!(!result.is_null()); + unsafe { BnString::into_string(result) } + } +} + +impl ToOwned for Activity { + type Owned = Ref; + + fn to_owned(&self) -> Self::Owned { + unsafe { RefCountable::inc_ref(self) } + } +} + +unsafe impl RefCountable for Activity { + unsafe fn inc_ref(handle: &Self) -> Ref { + Ref::new(Self { + handle: NonNull::new(BNNewActivityReference(handle.handle.as_ptr())) + .expect("valid handle"), + }) + } + + unsafe fn dec_ref(handle: &Self) { + BNFreeActivity(handle.handle.as_ptr()); + } +} + +pub trait AsConfig { + fn as_config(&self) -> CString; +} + +impl AsConfig for &str { + fn as_config(&self) -> std::ffi::CString { + self.to_cstr() + } +} + +/// The configuration for an `Activity`, defining its metadata, eligibility criteria, and execution semantics. +#[must_use] +#[derive(Deserialize, Serialize, Debug)] +pub struct Config { + /// A unique identifier for the activity. + pub name: String, + + /// A human-readable title for the activity. + pub title: String, + + /// A brief description of the activity's purpose and functionality. + pub description: String, + + /// The role of the activity within the workflow, determining its behavior and interaction with other activities. + #[serde(default)] + pub role: Role, + + /// Names by which this activity has previously been known. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub aliases: Vec, + + /// The conditions that determine when the activity should execute. + #[serde(default)] + pub eligibility: Eligibility, + + #[serde(skip_serializing_if = "Option::is_none")] + pub dependencies: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct Dependencies { + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub downstream: Vec, +} + +impl Config { + /// Creates a new instance with role [`Role::Action`] and the specified name, title, and description. + pub fn action( + name: impl Into, + title: impl Into, + description: impl Into, + ) -> Self { + Self { + name: name.into(), + title: title.into(), + description: description.into(), + role: Role::Action, + aliases: Vec::new(), + eligibility: Eligibility::default(), + dependencies: None, + } + } + + /// Sets the [`aliases`](field@Config::aliases) field, which contains names by which this activity has previously been known. + pub fn aliases(mut self, aliases: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.aliases = aliases.into_iter().map(|s| s.into()).collect(); + self + } + + /// Sets the [`eligibility`](field@Config::eligibility) field, which defines the conditions under which this activity is eligible for execution. + pub fn eligibility(mut self, eligibility: Eligibility) -> Self { + self.eligibility = eligibility; + self + } + + /// Sets the [`dependencies`](field@Config::dependencies) field to specify dependencies that should be triggered after this activity completes. + pub fn downstream_dependencies(mut self, dependencies: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.dependencies = Some(Dependencies { + downstream: dependencies.into_iter().map(|s| s.into()).collect(), + }); + self + } +} + +impl AsConfig for &Config { + fn as_config(&self) -> CString { + serde_json::to_string(self) + .expect("Failed to serialize Config") + .to_cstr() + } +} + +impl AsConfig for Config { + fn as_config(&self) -> CString { + (&self).as_config() + } +} + +/// Defines the behavior of the activity in the workflow. +/// +/// NOTE: Activities with the subflow role are only permitted in module workflows. +/// Subflows are not supported within function workflows. +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +#[derive(Default)] +pub enum Role { + /// The default role; performs a specific task. + #[default] + Action, + + /// Contains child activities and uses an eligibility handler to determine which child activities to execute. + /// This enables the ability to have a dynamic and reactive execution pipeline. + Selector, + + /// Creates a new task context and asynchronously processes its workflow sub-graph on a new thread within + /// the workflow machine. The subflow executes asynchronously from the requestor, allowing the original + /// thread to return immediately. Within this context, multiple task actions can be enqueued, enabling + /// extensive parallel processing. After completing its workflow sub-graph, it enters a stall state, + /// waiting for all its asynchronous task actions to complete. + Subflow, + + /// Asynchronously processes the workflow graph on a new thread within the workflow machine. + /// `Task` activities enable the pipeline to execute asynchronously from its requestor. `Task` activities + /// require a task context to be present; if no task context exists, they execute immediately in the + /// current thread. + Task, + + Sequence, + Listener, +} + +/// The conditions that determine when an activity should execute. +#[must_use] +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Eligibility { + /// An object that automatically generates a boolean control setting and corresponding predicate. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto: Option, + + /// Indicates whether the activity should run only once across all file/analysis sessions. + /// Once the activity runs, its state is saved persistently, and it will not run again unless + /// explicitly reset. This is useful for activities that only need to be performed exactly once, + /// such as initial setup tasks. + #[serde(skip_serializing_if = "Option::is_none")] + pub run_once: Option, + + /// Indicates whether the activity should run only once per session. Its state is not + /// persisted, so it will run again in a new session. This is useful for activities + /// that should be performed once per analysis session, such as initialization steps + /// specific to a particular execution context. + #[serde(skip_serializing_if = "Option::is_none")] + pub run_once_per_session: Option, + + /// Indicates if a subflow is eligible for re-execution based on its eligibility logic. + #[serde(skip_serializing_if = "Option::is_none")] + pub continuation: Option, + + /// Objects that define the condition that must be met for the activity to be eligible to run. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub predicates: Vec, + + /// Logical operator that defines how multiple predicates are combined. + #[serde(skip_serializing_if = "Option::is_none")] + pub logical_operator: Option, +} + +impl Eligibility { + /// Creates a new instance with an automatically generated boolean control setting and corresponding predicate. + /// The setting is enabled by default. + pub fn auto() -> Self { + Eligibility { + auto: Some(Auto::new()), + run_once: None, + run_once_per_session: None, + continuation: None, + predicates: vec![], + logical_operator: None, + } + } + + /// Creates a new instance with an automatically generated boolean control setting and corresponding predicate. + /// The setting has the value `value` by default. + pub fn auto_with_default(value: bool) -> Self { + Eligibility { + auto: Some(Auto::new().default(value)), + run_once: None, + run_once_per_session: None, + continuation: None, + predicates: vec![], + logical_operator: None, + } + } + + /// Sets the [`run_once`](field@Eligibility::run_once) field, indicating whether the activity should run only once across all file/analysis sessions. + pub fn run_once(mut self, value: bool) -> Self { + self.run_once = Some(value); + self + } + + /// Sets the [`run_once_per_session`](field@Eligibility::run_once_per_session) field, indicating whether the activity should run only once per session. + pub fn run_once_per_session(mut self, value: bool) -> Self { + self.run_once_per_session = Some(value); + self + } + + /// Sets the [`continuation`](field@Eligibility::continuation) field, indicating whether a subflow is eligible for re-execution based on its eligibility logic. + pub fn continuation(mut self, value: bool) -> Self { + self.continuation = Some(value); + self + } + + /// Sets the predicate that must be satisfied for the activity to be eligible to run. + pub fn predicate(mut self, predicate: impl Into) -> Self { + self.predicates = vec![predicate.into()]; + self + } + + /// Sets the predicates that must be satisfied for the activity to be eligible to run. + /// If multiple predicates are provided, they are combined using a logical OR. + pub fn matching_any_predicate(mut self, predicates: &[Predicate]) -> Self { + self.predicates = predicates.to_vec(); + self.logical_operator = Some(PredicateLogicalOperator::Or); + self + } + + /// Sets the predicates that must be satisfied for the activity to be eligible to run. + /// If multiple predicates are provided, they are combined using a logical AND. + pub fn matching_all_predicates(mut self, predicates: &[Predicate]) -> Self { + self.predicates = predicates.to_vec(); + self.logical_operator = Some(PredicateLogicalOperator::And); + self + } +} + +impl Default for Eligibility { + fn default() -> Self { + Self::auto() + } +} + +/// Represents the request for an automatically generated boolean control setting and corresponding predicate. +#[must_use] +#[derive(Deserialize, Serialize, Debug, Default)] +pub struct Auto { + /// The default value for the setting. If `None`, the setting is enabled by default. + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, +} + +impl Auto { + /// Creates a new `Auto` instance that represents a setting that is enabled by default. + pub fn new() -> Self { + Self { default: None } + } + + /// Sets the `default` value for the setting. + pub fn default(mut self, value: bool) -> Self { + self.default = Some(value); + self + } +} + +/// A predicate that can be used to determine the eligibility of an activity. +/// +/// See [`ViewType`] and [`Setting`] for specific predicates that can be used. +#[must_use] +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Predicate { + #[serde(flatten)] + predicate_type: PredicateType, + operator: Operator, + value: serde_json::Value, +} + +/// A predicate that checks the type of the [`BinaryView`](crate::binary_view::BinaryView). +#[must_use] +pub enum ViewType { + In(Vec), + NotIn(Vec), +} + +impl ViewType { + /// Creates a new predicate that checks if the type of the [`BinaryView`](crate::binary_view::BinaryView) + /// _is_ in the provided list. + pub fn in_(values: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + ViewType::In(values.into_iter().map(|s| s.as_ref().to_string()).collect()) + } + + /// Creates a new predicate that checks if the type of the [`BinaryView`](crate::binary_view::BinaryView) + /// _is not_ in the provided list. + pub fn not_in(values: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + ViewType::NotIn(values.into_iter().map(|s| s.as_ref().to_string()).collect()) + } +} + +impl From for Predicate { + fn from(predicate: ViewType) -> Self { + match predicate { + ViewType::In(value) => Predicate { + predicate_type: PredicateType::ViewType, + operator: Operator::In, + value: serde_json::json!(value), + }, + ViewType::NotIn(value) => Predicate { + predicate_type: PredicateType::ViewType, + operator: Operator::NotIn, + value: serde_json::json!(value), + }, + } + } +} + +/// A predicate that evaluates the value of a specific setting. +#[must_use] +pub struct Setting { + identifier: String, + operator: Operator, + value: serde_json::Value, +} + +impl Setting { + /// Creates a new predicate that evaluates the value of a specific setting against `value` using `operator`. + pub fn new( + identifier: impl Into, + operator: Operator, + value: impl serde::Serialize, + ) -> Self { + Self { + identifier: identifier.into(), + operator, + value: serde_json::json!(value), + } + } + + /// Creates a new predicate that checks if the value of the setting is equal to `value`. + pub fn eq(identifier: impl Into, value: impl serde::Serialize) -> Self { + Self::new(identifier, Operator::Eq, value) + } + + /// Creates a new predicate that checks if the value of the setting is not equal to `value`. + pub fn ne(identifier: impl Into, value: impl serde::Serialize) -> Self { + Self::new(identifier, Operator::Ne, value) + } + + /// Creates a new predicate that checks if the value of the setting is less than `value`. + pub fn lt(identifier: impl Into, value: impl serde::Serialize) -> Self { + Self::new(identifier, Operator::Lt, value) + } + + /// Creates a new predicate that checks if the value of the setting is less than or equal to `value`. + pub fn lte(identifier: impl Into, value: impl serde::Serialize) -> Self { + Self::new(identifier, Operator::Lte, value) + } + + /// Creates a new predicate that checks if the value of the setting is greater than `value`. + pub fn gt(identifier: impl Into, value: impl serde::Serialize) -> Self { + Self::new(identifier, Operator::Gt, value) + } + + /// Creates a new predicate that checks if the value of the setting is greater than or equal to `value`. + pub fn gte(identifier: impl Into, value: impl serde::Serialize) -> Self { + Self::new(identifier, Operator::Gte, value) + } + + /// Creates a new predicate that checks if the value of the setting is in the provided list. + pub fn in_(identifier: impl Into, value: impl serde::Serialize) -> Self { + Self::new(identifier, Operator::In, value) + } + + /// Creates a new predicate that checks if the value of the setting is not in the provided list. + pub fn not_in(identifier: impl Into, value: impl serde::Serialize) -> Self { + Self::new(identifier, Operator::NotIn, value) + } +} + +impl From for Predicate { + fn from(setting: Setting) -> Self { + Predicate { + predicate_type: PredicateType::Setting { + identifier: setting.identifier, + }, + operator: setting.operator, + value: setting.value, + } + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase", tag = "type")] +enum PredicateType { + Setting { identifier: String }, + ViewType, +} + +#[derive(Deserialize, Serialize, Debug, Copy, Clone)] +pub enum Operator { + #[serde(rename = "==")] + Eq, + #[serde(rename = "!=")] + Ne, + #[serde(rename = "<")] + Lt, + #[serde(rename = "<=")] + Lte, + #[serde(rename = ">")] + Gt, + #[serde(rename = ">=")] + Gte, + #[serde(rename = "in")] + In, + #[serde(rename = "not in")] + NotIn, +} + +#[derive(Deserialize, Serialize, Debug, Clone, Copy)] +#[serde(rename_all = "camelCase")] +pub enum PredicateLogicalOperator { + And, + Or, +} diff --git a/rust/tests/workflow.rs b/rust/tests/workflow.rs index bf8818c88e..8fbe0240ab 100644 --- a/rust/tests/workflow.rs +++ b/rust/tests/workflow.rs @@ -8,8 +8,11 @@ use binaryninja::workflow::Workflow; #[test] fn test_workflow_clone() { let _session = Session::new().expect("Failed to initialize session"); - let original_workflow = Workflow::new("core.function.baseAnalysis"); - let mut cloned_workflow = original_workflow.clone_to("clone_workflow"); + let original_workflow = Workflow::get("core.function.metaAnalysis").unwrap(); + let mut cloned_workflow = original_workflow + .clone_to("clone_workflow") + .register() + .unwrap(); assert_eq!(cloned_workflow.name().as_str(), "clone_workflow"); assert_eq!( @@ -17,36 +20,30 @@ fn test_workflow_clone() { original_workflow.configuration() ); - cloned_workflow = original_workflow.clone_to(""); + // `clone_to` with an empty name should re-use the original workflow's name. + cloned_workflow = original_workflow.clone_to("").register().unwrap(); assert_eq!( cloned_workflow.name().as_str(), - "core.function.baseAnalysis" + original_workflow.name().as_str() ); } #[test] fn test_workflow_registration() { let _session = Session::new().expect("Failed to initialize session"); - // Validate NULL workflows cannot be registered - let workflow = Workflow::new("null"); - assert_eq!(workflow.name().as_str(), "null"); - assert!(!workflow.registered()); - workflow - .register() - .expect_err("Re-registration of null is allowed"); // Validate new workflows can be registered - let test_workflow = Workflow::instance("core.function.baseAnalysis").clone_to("test_workflow"); + let test_workflow = Workflow::get("core.function.baseAnalysis") + .unwrap() + .clone_to("test_workflow"); - assert_eq!(test_workflow.name().as_str(), "test_workflow"); - assert!(!test_workflow.registered()); - test_workflow + let test_workflow = test_workflow .register() .expect("Failed to register workflow"); assert!(test_workflow.registered()); assert_eq!( test_workflow.size(), - Workflow::instance("core.function.baseAnalysis").size() + Workflow::get("core.function.baseAnalysis").unwrap().size() ); Workflow::list() .iter() @@ -57,32 +54,12 @@ fn test_workflow_registration() { .iter() .find(|&w| w == "test_workflow") .expect("Workflow not found in settings"); - - // Validate that registered workflows are immutable - let immutable_workflow = Workflow::instance("test_workflow"); - assert!(!immutable_workflow.clear()); - assert!(immutable_workflow.contains("core.function.advancedAnalysis")); - assert!(!immutable_workflow.remove("core.function.advancedAnalysis")); - assert!(!Workflow::instance("core.function.baseAnalysis").clear()); + assert!(Workflow::get("test_workflow").is_some()); // Validate re-registration of baseAnalysis is not allowed - let base_workflow_clone = Workflow::instance("core.function.baseAnalysis").clone_to(""); - - assert_eq!( - base_workflow_clone.name().as_str(), - "core.function.baseAnalysis" - ); - assert!(!base_workflow_clone.registered()); + let base_workflow_clone = Workflow::cloned("core.function.baseAnalysis").unwrap(); base_workflow_clone .register() + .map(|_| ()) .expect_err("Re-registration of baseAnalysis is allowed"); - assert_eq!( - base_workflow_clone.size(), - Workflow::instance("core.function.baseAnalysis").size() - ); - assert_eq!( - base_workflow_clone.configuration(), - Workflow::instance("core.function.baseAnalysis").configuration() - ); - assert!(!base_workflow_clone.registered()); }