use ahash::AHashMap;
use bonsai_common::{cached_span_map_arc, workspace_relative_filter_path, FuncId, SymbolId};
use bonsai_workspace::Workspace;
use serde::{Deserialize, Serialize};
use crate::finding::{FindingMatch, TaintPropagationStep};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FlowRole {
Source,
Taint,
Sink,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FlowSourceLine {
pub n: u32,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub step: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<FlowRole>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FlowFunctionBody {
pub function: String,
pub file: String,
pub start_line: u32,
pub lines: Vec<FlowSourceLine>,
}
#[derive(Clone, Debug)]
struct CachedFunctionBody {
function: String,
file: String,
start_line: u32,
end_line: u32,
lines: Vec<FlowSourceLine>,
}
pub struct FlowBodyCache<'a> {
ws: &'a Workspace,
bodies: AHashMap<FuncId, Option<CachedFunctionBody>>,
}
impl<'a> FlowBodyCache<'a> {
pub fn new(ws: &'a Workspace) -> Self {
Self {
ws,
bodies: AHashMap::new(),
}
}
fn cached_body(&mut self, func: FuncId) -> Option<&CachedFunctionBody> {
if !self.bodies.contains_key(&func) {
let body = build_cached_function_body(self.ws, func);
self.bodies.insert(func, body);
}
self.bodies.get(&func).and_then(Option::as_ref)
}
pub fn build_flow_bodies(
&mut self,
chain_funcs: &[FuncId],
source: &FindingMatch,
taint_path: &[TaintPropagationStep],
terminal_role: FlowRole,
) -> Vec<FlowFunctionBody> {
if chain_funcs.is_empty() {
return Vec::new();
}
let mut hops: Vec<CachedFunctionBody> = chain_funcs
.iter()
.filter_map(|func| self.cached_body(*func).cloned())
.collect();
let mut events: Vec<(usize, u32, FlowRole)> = Vec::new();
if source.line > 0 {
if let Some(idx) = hop_index_for(&hops, &source.file, source.line, source.enclosing_fn.as_deref())
{
events.push((idx, source.line, FlowRole::Source));
}
}
let last_step = taint_path.len().saturating_sub(1);
for (idx, call) in taint_path.iter().enumerate() {
let role = if idx == last_step {
terminal_role
} else {
FlowRole::Taint
};
if let Some(hop) = hop_index_for(&hops, &call.file, call.line, Some(&call.caller)) {
events.push((hop, call.line, role));
}
}
let mut placed: Vec<(usize, u32, FlowRole)> = Vec::new();
for (hop, line, role) in events {
if let Some(existing) = placed.iter_mut().find(|p| p.0 == hop && p.1 == line) {
if role_strength(role) > role_strength(existing.2) {
existing.2 = role;
}
} else {
placed.push((hop, line, role));
}
}
for (step, (hop, line, role)) in placed.into_iter().enumerate() {
annotate_line(&mut hops[hop].lines, line, role, step as u32 + 1);
}
hops.into_iter()
.map(|cached| FlowFunctionBody {
function: cached.function,
file: cached.file,
start_line: cached.start_line,
lines: cached.lines,
})
.collect()
}
}
fn hop_index_for(
hops: &[CachedFunctionBody],
file: &str,
line: u32,
enclosing: Option<&str>,
) -> Option<usize> {
let contains = |hop: &CachedFunctionBody| {
same_file(&hop.file, file) && line >= hop.start_line && line <= hop.end_line
};
let candidates: Vec<usize> = hops
.iter()
.enumerate()
.filter(|(_, hop)| contains(hop))
.map(|(idx, _)| idx)
.collect();
if let Some(name) = enclosing {
let bare = name.split('@').next().unwrap_or(name);
if let Some(&idx) = candidates.iter().find(|&&idx| hops[idx].function == bare) {
return Some(idx);
}
}
candidates
.into_iter()
.min_by_key(|&idx| hops[idx].end_line - hops[idx].start_line)
}
fn same_file(a: &str, b: &str) -> bool {
if a == b {
return true;
}
let (long, short) = if a.len() >= b.len() { (a, b) } else { (b, a) };
!short.is_empty() && long.ends_with(short) && long[..long.len() - short.len()].ends_with(['/', '\\'])
}
fn role_strength(role: FlowRole) -> u8 {
match role {
FlowRole::Sink => 3,
FlowRole::Source => 2,
FlowRole::Taint => 1,
}
}
fn annotate_line(lines: &mut [FlowSourceLine], line: u32, role: FlowRole, step: u32) {
if let Some(slot) = lines.iter_mut().find(|l| l.n == line && l.role.is_none()) {
slot.step = Some(step);
slot.role = Some(role);
}
}
fn build_cached_function_body(ws: &Workspace, func: FuncId) -> Option<CachedFunctionBody> {
let decl = ws.exact_decl(SymbolId::new(func.raw()))?;
let file = decl.span.file;
let snapshot = ws.vfs().snapshot(file).ok()?;
let src = snapshot.text.as_ref();
let span_map = cached_span_map_arc(file, snapshot.version, &snapshot.text);
let body_span = decl.body_span.unwrap_or(decl.span);
let header_line = span_map.line_col(decl.name_span.start).line;
let start_line = span_map.line_col(body_span.start).line;
let end_line = span_map.line_col(body_span.end.saturating_sub(1)).line;
let first_line = header_line.min(start_line);
let src_lines: Vec<&str> = src.split('\n').collect();
let lines: Vec<FlowSourceLine> = (first_line..=end_line)
.map(|n| FlowSourceLine {
n,
text: src_lines
.get(n.saturating_sub(1) as usize)
.copied()
.unwrap_or("")
.to_string(),
step: None,
role: None,
})
.collect();
Some(CachedFunctionBody {
function: decl.name.clone(),
file: workspace_relative_filter_path(
ws.db().workspace_root().as_deref(),
&snapshot.path.display().to_string(),
),
start_line: first_line,
end_line,
lines,
})
}
pub fn build_flow_bodies(
ws: &Workspace,
chain_funcs: &[FuncId],
source: &FindingMatch,
taint_path: &[TaintPropagationStep],
terminal_role: FlowRole,
) -> Vec<FlowFunctionBody> {
FlowBodyCache::new(ws).build_flow_bodies(chain_funcs, source, taint_path, terminal_role)
}