#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss
)]
use std::collections::{BTreeMap, HashSet};
use crate::instruction::{Instruction, OpCode, Operand};
use crate::manifest::ContractManifest;
use crate::nef::{describe_call_flags, NefFile};
use crate::{syscalls, util};
use super::super::{MethodRef, MethodTable};
use super::arguments::resolve_ldarg_calla_targets;
use super::model::{CallEdge, CallGraph, CallTarget};
use super::pointers::calla_target_from_pusha;
#[must_use]
pub fn build_call_graph(
nef: &NefFile,
instructions: &[Instruction],
manifest: Option<&ContractManifest>,
) -> CallGraph {
let table = MethodTable::new(instructions, manifest);
let mut methods: BTreeMap<usize, MethodRef> = table
.spans()
.iter()
.map(|span| (span.method.offset, span.method.clone()))
.collect();
let instruction_offsets: HashSet<usize> = instructions.iter().map(|i| i.offset).collect();
let mut edges = Vec::new();
for (index, instr) in instructions.iter().enumerate() {
match instr.opcode {
OpCode::Syscall => push_syscall_edge(&mut edges, &table, instr),
OpCode::Call | OpCode::Call_L => push_direct_call_edge(
&mut edges,
&mut methods,
&table,
&instruction_offsets,
instr,
),
OpCode::CallT => push_method_token_edge(&mut edges, nef, &table, instr),
OpCode::CallA => push_calla_edge(
&mut edges,
&mut methods,
&table,
&instruction_offsets,
instructions,
index,
instr,
),
_ => {}
}
}
resolve_ldarg_calla_targets(instructions, &mut edges, &table, &mut methods);
CallGraph {
methods: methods.into_values().collect(),
edges,
}
}
fn push_syscall_edge(edges: &mut Vec<CallEdge>, table: &MethodTable, instr: &Instruction) {
let Some(Operand::Syscall(hash)) = instr.operand else {
return;
};
let info = syscalls::lookup(hash);
edges.push(CallEdge {
caller: table.method_for_offset(instr.offset),
call_offset: instr.offset,
opcode: instr.opcode.to_string(),
target: CallTarget::Syscall {
hash,
name: info.map(|i| i.name.to_string()),
returns_value: info.map(|i| i.returns_value).unwrap_or(true),
},
});
}
fn push_direct_call_edge(
edges: &mut Vec<CallEdge>,
methods: &mut BTreeMap<usize, MethodRef>,
table: &MethodTable,
instruction_offsets: &HashSet<usize>,
instr: &Instruction,
) {
let caller = table.method_for_offset(instr.offset);
match relative_target_isize(instr) {
Some(target) if target >= 0 && instruction_offsets.contains(&(target as usize)) => {
let target = target as usize;
let callee = table.resolve_internal_target(target);
methods.insert(callee.offset, callee.clone());
edges.push(CallEdge {
caller,
call_offset: instr.offset,
opcode: instr.opcode.to_string(),
target: CallTarget::Internal { method: callee },
});
}
Some(target) => edges.push(CallEdge {
caller,
call_offset: instr.offset,
opcode: instr.opcode.to_string(),
target: CallTarget::UnresolvedInternal { target },
}),
None => edges.push(CallEdge {
caller,
call_offset: instr.offset,
opcode: instr.opcode.to_string(),
target: CallTarget::UnresolvedInternal { target: -1 },
}),
}
}
fn push_method_token_edge(
edges: &mut Vec<CallEdge>,
nef: &NefFile,
table: &MethodTable,
instr: &Instruction,
) {
let Some(Operand::U16(index)) = instr.operand else {
return;
};
let token = nef.method_tokens.get(index as usize);
if let Some(token) = token {
edges.push(CallEdge {
caller: table.method_for_offset(instr.offset),
call_offset: instr.offset,
opcode: instr.opcode.to_string(),
target: CallTarget::MethodToken {
index,
hash_le: util::format_hash(&token.hash),
hash_be: util::format_hash_be(&token.hash),
method: token.method.clone(),
parameters_count: token.parameters_count,
has_return_value: token.has_return_value,
call_flags: token.call_flags,
call_flags_description: describe_call_flags(token.call_flags),
},
});
} else {
edges.push(CallEdge {
caller: table.method_for_offset(instr.offset),
call_offset: instr.offset,
opcode: instr.opcode.to_string(),
target: CallTarget::Indirect {
opcode: instr.opcode.to_string(),
operand: Some(index),
},
});
}
}
fn push_calla_edge(
edges: &mut Vec<CallEdge>,
methods: &mut BTreeMap<usize, MethodRef>,
table: &MethodTable,
instruction_offsets: &HashSet<usize>,
instructions: &[Instruction],
index: usize,
instr: &Instruction,
) {
let caller = table.method_for_offset(instr.offset);
if let Some(target) = calla_target_from_pusha(instructions, index)
.filter(|target| instruction_offsets.contains(target))
{
let callee = table.resolve_internal_target(target);
methods.insert(callee.offset, callee.clone());
edges.push(CallEdge {
caller,
call_offset: instr.offset,
opcode: instr.opcode.to_string(),
target: CallTarget::Internal { method: callee },
});
} else {
edges.push(CallEdge {
caller,
call_offset: instr.offset,
opcode: instr.opcode.to_string(),
target: CallTarget::Indirect {
opcode: instr.opcode.to_string(),
operand: None,
},
});
}
}
fn relative_target_isize(instr: &Instruction) -> Option<isize> {
let delta = match &instr.operand {
Some(Operand::Jump(v)) => *v as isize,
Some(Operand::Jump32(v)) => *v as isize,
_ => return None,
};
Some(instr.offset as isize + delta)
}