Cybersecurity-Projects/PROJECTS/intermediate/binary-analysis-tool/backend/crates/axumortem-engine/src/pass.rs

406 lines
11 KiB
Rust

// ©AngelaMos | 2026
// pass.rs
//
// Analysis pass trait, pass manager, and topological execution
//
// Defines the AnalysisPass trait (sealed via a private module
// to prevent external implementations) with name(),
// dependencies(), and run() methods. PassManager accepts a
// Vec of boxed passes, computes a topological execution order
// via Kahn's algorithm (panics on cycles), and run_all()
// executes them in dependency order, continuing through
// failures and recording each PassOutcome with timing and
// error info. PassReport aggregates outcomes with
// all_succeeded() and failed_passes() query methods.
// Includes unit tests using MockPass to verify topological
// ordering, diamond dependencies, failure continuation,
// cycle detection, and duration tracking.
//
// Connects to:
// context.rs - AnalysisContext (passed to each pass)
// error.rs - EngineError (returned by pass::run)
use std::collections::{HashMap, VecDeque};
use std::time::Instant;
use crate::context::AnalysisContext;
use crate::error::EngineError;
mod private {
pub trait Sealed {}
}
pub trait AnalysisPass: private::Sealed + Send + Sync {
fn name(&self) -> &'static str;
fn dependencies(&self) -> &[&'static str];
fn run(
&self,
ctx: &mut AnalysisContext,
) -> Result<(), EngineError>;
}
#[derive(Debug, Clone)]
pub struct PassOutcome {
pub name: &'static str,
pub success: bool,
pub duration_ms: u64,
pub error_message: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PassReport {
pub outcomes: Vec<PassOutcome>,
}
impl PassReport {
pub fn all_succeeded(&self) -> bool {
self.outcomes.iter().all(|o| o.success)
}
pub fn failed_passes(&self) -> Vec<&PassOutcome> {
self.outcomes
.iter()
.filter(|o| !o.success)
.collect()
}
}
pub struct PassManager {
passes: Vec<Box<dyn AnalysisPass>>,
order: Vec<usize>,
}
impl PassManager {
pub fn new(
passes: Vec<Box<dyn AnalysisPass>>,
) -> Self {
let order = topological_order(&passes);
Self { passes, order }
}
pub fn run_all(
&self,
ctx: &mut AnalysisContext,
) -> PassReport {
let mut outcomes = Vec::with_capacity(self.passes.len());
for &idx in &self.order {
let pass = &self.passes[idx];
let start = Instant::now();
let result = pass.run(ctx);
let duration_ms =
start.elapsed().as_millis() as u64;
let outcome = match result {
Ok(()) => {
tracing::info!(
pass = pass.name(),
duration_ms,
"pass completed"
);
PassOutcome {
name: pass.name(),
success: true,
duration_ms,
error_message: None,
}
}
Err(e) => {
tracing::error!(
pass = pass.name(),
error = %e,
duration_ms,
"pass failed"
);
PassOutcome {
name: pass.name(),
success: false,
duration_ms,
error_message: Some(e.to_string()),
}
}
};
outcomes.push(outcome);
}
PassReport { outcomes }
}
}
fn topological_order(
passes: &[Box<dyn AnalysisPass>],
) -> Vec<usize> {
let name_to_idx: HashMap<&str, usize> = passes
.iter()
.enumerate()
.map(|(i, p)| (p.name(), i))
.collect();
let n = passes.len();
let mut in_degree = vec![0usize; n];
let mut adjacency: Vec<Vec<usize>> = vec![vec![]; n];
for (idx, pass) in passes.iter().enumerate() {
for dep_name in pass.dependencies() {
if let Some(&dep_idx) = name_to_idx.get(dep_name)
{
adjacency[dep_idx].push(idx);
in_degree[idx] += 1;
}
}
}
let mut queue: VecDeque<usize> = in_degree
.iter()
.enumerate()
.filter(|&(_, deg)| *deg == 0)
.map(|(i, _)| i)
.collect();
let mut order = Vec::with_capacity(n);
while let Some(node) = queue.pop_front() {
order.push(node);
for &neighbor in &adjacency[node] {
in_degree[neighbor] -= 1;
if in_degree[neighbor] == 0 {
queue.push_back(neighbor);
}
}
}
assert_eq!(
order.len(),
n,
"cycle detected in pass dependencies — this is a programmer error"
);
order
}
pub(crate) use private::Sealed;
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
struct MockPass {
name: &'static str,
deps: Vec<&'static str>,
log: Arc<Mutex<Vec<&'static str>>>,
should_fail: bool,
}
impl Sealed for MockPass {}
impl AnalysisPass for MockPass {
fn name(&self) -> &'static str {
self.name
}
fn dependencies(&self) -> &[&'static str] {
&self.deps
}
fn run(
&self,
_ctx: &mut AnalysisContext,
) -> Result<(), EngineError> {
self.log.lock().unwrap().push(self.name);
if self.should_fail {
return Err(EngineError::PassFailed {
pass: self.name,
source: "mock failure".into(),
});
}
Ok(())
}
}
fn make_ctx() -> AnalysisContext {
AnalysisContext::new(
crate::context::BinarySource::Buffered(
Arc::from(vec![0u8; 4]),
),
"deadbeef".into(),
"test.bin".into(),
4,
)
}
#[test]
fn topological_sort_respects_dependencies() {
let log = Arc::new(Mutex::new(Vec::new()));
let passes: Vec<Box<dyn AnalysisPass>> = vec![
Box::new(MockPass {
name: "c",
deps: vec!["b"],
log: Arc::clone(&log),
should_fail: false,
}),
Box::new(MockPass {
name: "a",
deps: vec![],
log: Arc::clone(&log),
should_fail: false,
}),
Box::new(MockPass {
name: "b",
deps: vec!["a"],
log: Arc::clone(&log),
should_fail: false,
}),
];
let manager = PassManager::new(passes);
let mut ctx = make_ctx();
let report = manager.run_all(&mut ctx);
let execution_order = log.lock().unwrap().clone();
assert_eq!(execution_order, vec!["a", "b", "c"]);
assert!(report.all_succeeded());
assert_eq!(report.outcomes.len(), 3);
}
#[test]
fn continues_on_failure() {
let log = Arc::new(Mutex::new(Vec::new()));
let passes: Vec<Box<dyn AnalysisPass>> = vec![
Box::new(MockPass {
name: "first",
deps: vec![],
log: Arc::clone(&log),
should_fail: false,
}),
Box::new(MockPass {
name: "second",
deps: vec![],
log: Arc::clone(&log),
should_fail: true,
}),
Box::new(MockPass {
name: "third",
deps: vec![],
log: Arc::clone(&log),
should_fail: false,
}),
];
let manager = PassManager::new(passes);
let mut ctx = make_ctx();
let report = manager.run_all(&mut ctx);
let execution_order = log.lock().unwrap().clone();
assert_eq!(
execution_order,
vec!["first", "second", "third"]
);
assert!(!report.all_succeeded());
assert_eq!(report.failed_passes().len(), 1);
assert_eq!(
report.failed_passes()[0].name,
"second"
);
}
#[test]
fn diamond_dependency_ordering() {
let log = Arc::new(Mutex::new(Vec::new()));
let passes: Vec<Box<dyn AnalysisPass>> = vec![
Box::new(MockPass {
name: "score",
deps: vec!["imports", "entropy"],
log: Arc::clone(&log),
should_fail: false,
}),
Box::new(MockPass {
name: "entropy",
deps: vec!["format"],
log: Arc::clone(&log),
should_fail: false,
}),
Box::new(MockPass {
name: "format",
deps: vec![],
log: Arc::clone(&log),
should_fail: false,
}),
Box::new(MockPass {
name: "imports",
deps: vec!["format"],
log: Arc::clone(&log),
should_fail: false,
}),
];
let manager = PassManager::new(passes);
let mut ctx = make_ctx();
manager.run_all(&mut ctx);
let order = log.lock().unwrap().clone();
let format_pos =
order.iter().position(|&n| n == "format").unwrap();
let imports_pos =
order.iter().position(|&n| n == "imports").unwrap();
let entropy_pos =
order.iter().position(|&n| n == "entropy").unwrap();
let score_pos =
order.iter().position(|&n| n == "score").unwrap();
assert!(format_pos < imports_pos);
assert!(format_pos < entropy_pos);
assert!(imports_pos < score_pos);
assert!(entropy_pos < score_pos);
}
#[test]
#[should_panic(expected = "cycle detected")]
fn detects_cycle() {
let log = Arc::new(Mutex::new(Vec::new()));
let passes: Vec<Box<dyn AnalysisPass>> = vec![
Box::new(MockPass {
name: "a",
deps: vec!["b"],
log: Arc::clone(&log),
should_fail: false,
}),
Box::new(MockPass {
name: "b",
deps: vec!["a"],
log: Arc::clone(&log),
should_fail: false,
}),
];
let _manager = PassManager::new(passes);
}
#[test]
fn reports_duration() {
let log = Arc::new(Mutex::new(Vec::new()));
let passes: Vec<Box<dyn AnalysisPass>> = vec![
Box::new(MockPass {
name: "fast",
deps: vec![],
log: Arc::clone(&log),
should_fail: false,
}),
];
let manager = PassManager::new(passes);
let mut ctx = make_ctx();
let report = manager.run_all(&mut ctx);
assert_eq!(report.outcomes.len(), 1);
assert_eq!(report.outcomes[0].name, "fast");
assert!(report.outcomes[0].success);
}
}