use super::*;
use rsleigh_api::Architecture;
use rsleigh_decompile::slice::selector::{self, Selector};
fn address(value: &str) -> Result<u64, String> {
let value = value
.strip_prefix("0x")
.or_else(|| value.strip_prefix("0X"))
.ok_or("selector addresses require a 0x-prefixed hexadecimal address")?;
u64::from_str_radix(value, 16).map_err(|_| "invalid selector instruction address".into())
}
fn parse_selector(args: &[String]) -> Result<Selector, String> {
let var = value_arg(args, "--var")?;
let call = value_arg(args, "--call-site")?;
let argument = value_arg(args, "--arg")?;
let condition = value_arg(args, "--condition")?;
let at = value_arg(args, "--at")?;
let returns = args.iter().filter(|a| a.as_str() == "--return").count();
if usize::from(var.is_some())
+ usize::from(call.is_some())
+ usize::from(condition.is_some())
+ returns
!= 1
{
return Err("select exactly one root: --var ID, --call-site ADDRESS --arg INDEX, --return [--at ADDRESS], or --condition ADDRESS".into());
}
if argument.is_some() != call.is_some() {
return Err("--call-site and --arg must be used together".into());
}
if at.is_some() && returns != 1 {
return Err("--at requires --return".into());
}
if let Some(var) = var {
Ok(Selector::Variable {
var_id: var
.parse()
.map_err(|_| "--var requires an unsigned SSA variable ID")?,
})
} else if let Some(call) = call {
Ok(Selector::CallArgument {
address: address(call)?,
index: argument
.unwrap()
.parse()
.map_err(|_| "--arg requires a zero-based unsigned argument index")?,
})
} else if let Some(condition) = condition {
Ok(Selector::Condition {
address: address(condition)?,
})
} else {
Ok(Selector::Return {
address: at.map(address).transpose()?,
})
}
}
pub(super) fn run(data: &[u8], args: &[String]) -> Result<u8, String> {
let _execution = execution_scope(args)?;
let target = value_arg(args, "--ssa-slice")?.ok_or("--ssa-slice requires a function")?;
let selector = parse_selector(args)?;
let max_nodes = number_arg(args, "--max-nodes", 64)?;
let max_depth = number_arg(args, "--max-depth", 16)?;
let obj = parse_object_lenient(data).map_err(|e| e.to_string())?;
let (arch, _segs, symbols) =
agent_symbols(&obj, data).ok_or("unsupported binary format or architecture")?;
let address = card::resolve_target(target, &symbols)?;
let (snapshot, mut metrics) = cache::analyze(data, address, arch, args)?;
let snapshot = match snapshot {
cache::Outcome::Complete(snapshot) => snapshot,
cache::Outcome::Stopped {
instructions,
diagnostics,
} => {
let status = if instructions.is_empty() {
"failed"
} else {
"partial"
};
let total = instructions.len();
let evidence:Vec<_>=instructions.into_iter().take(AGENT_CARD_MAX_INSTRUCTIONS).map(|i|
json!({"address":format!("0x{:x}",i.address),"length":i.len,"disassembly":i.disassembly,"constructor":i.constructor})).collect();
emit(
&json!({"schema":"rsleigh.ssa-slice/v3","status":status,"error":"analysis execution limit reached",
"file_sha256":compute_sha256(data),"function_address":format!("0x{address:x}"),"tool_version":env!("CARGO_PKG_VERSION"),
"selector":selector,"slice":Value::Null,"diagnostics":diagnostics,"metrics":metrics,
"evidence":{"instructions":evidence,"decoded_total":total,"truncated":total>AGENT_CARD_MAX_INSTRUCTIONS}}),
);
return Ok(status_exit(status));
}
};
let snapshot = std::rc::Rc::new(snapshot);
let ssa = &snapshot.ssa;
let instructions = &snapshot.instructions;
let mut diagnostics = snapshot.diagnostics.clone();
let operations: Vec<_> = instructions
.iter()
.map(|i| (i.address, i.operations.as_slice()))
.collect();
let cc = rsleigh_decompile::detect_cc(arch, Some(data));
let selection = if matches!(selector, Selector::CallArgument { .. })
&& matches!(arch, Architecture::MIPS32 | Architecture::RiscV64)
{
Err(selector::SelectionError {code:"unsupported_root", message:"integer argument selectors are not yet supported for this architecture's calling convention".into(), candidates:vec![]})
} else {
selector::resolve_operations(&ssa, &operations, selector.clone(), cc)
};
let selection = match selection {
Ok(selection) => selection,
Err(error) => {
emit(&json!({"schema":"rsleigh.ssa-slice/v3","status":"failed",
"file_sha256":compute_sha256(data),"function_address":format!("0x{address:x}"),
"tool_version":env!("CARGO_PKG_VERSION"),"snapshot":"post-fold/v3",
"selector":selector,"selection_error":error,"error":error.message,"metrics":metrics}));
return Ok(1);
}
};
let mut snapshots =
std::collections::BTreeMap::from([(address, std::rc::Rc::clone(&snapshot))]);
let mut function_metrics = std::collections::BTreeMap::from([(address, metrics.clone())]);
let mut partial_functions = Vec::new();
let known_functions: std::collections::HashSet<_> =
symbols.iter().map(|(address, _)| *address).collect();
let imports = build_import_map(&obj, data);
let limits = rsleigh_decompile::slice::interprocedural::Limits {
max_nodes,
max_depth,
max_call_depth: number_arg(args, "--max-call-depth", 2)?,
max_functions: number_arg(args, "--max-functions", 16)?,
max_work: number_arg(args, "--max-traversal-work", 100_000)?,
};
let slice = rsleigh_decompile::slice::interprocedural::backward(
address,
rsleigh_decompile::ir::VarId(selection.root),
std::rc::Rc::clone(&snapshot),
cc,
&imports,
limits,
|target| {
if matches!(arch, Architecture::MIPS32 | Architecture::RiscV64) {
return Err("unsupported_calling_convention: helper-call traversal is not yet supported for this architecture".into());
}
if let Some(snapshot) = snapshots.get(&target) {
return Ok(std::rc::Rc::clone(snapshot));
}
if !known_functions.contains(&target) {
return Err(format!(
"0x{target:x}: target is not a discovered function entry"
));
}
let (outcome, child_metrics) = cache::analyze(data, target, arch, args)?;
function_metrics.insert(target, child_metrics);
let child = match outcome {
cache::Outcome::Complete(snapshot) => snapshot,
cache::Outcome::Stopped {
instructions,
diagnostics,
} => {
let total = instructions.len();
let evidence: Vec<_> = instructions.into_iter().take(AGENT_CARD_MAX_INSTRUCTIONS).map(|i|
json!({"address":format!("0x{:x}",i.address),"length":i.len,"disassembly":i.disassembly,"constructor":i.constructor})).collect();
partial_functions.push(json!({"function_address":target,"instructions":evidence,"decoded_total":total,"diagnostics":diagnostics,"truncated":total>AGENT_CARD_MAX_INSTRUCTIONS}));
return Err(format!("0x{target:x}: analysis execution limit"));
}
};
if !child.diagnostics.is_empty()
|| child.ssa.diagnostics.iter().any(|d| {
matches!(
d.severity,
rsleigh_decompile::ir::Severity::Warn
| rsleigh_decompile::ir::Severity::Error
)
})
{
return Err(format!("0x{target:x}: callee analysis is incomplete"));
}
let child = std::rc::Rc::new(child);
snapshots.insert(target, std::rc::Rc::clone(&child));
Ok(child)
},
)?;
let origins: std::collections::BTreeSet<_> = slice
.nodes
.iter()
.flat_map(|n| {
n.node
.origins
.operations
.iter()
.map(move |origin| (n.function_address, *origin))
})
.collect();
let evidence: Vec<_> = origins.into_iter().map(|(function_address, origin)| {
let snapshot = snapshots.get(&function_address).ok_or("origin function missing from traversal")?;
let instruction = snapshot.instructions.binary_search_by_key(&origin.instruction_address, |i| i.address)
.ok().map(|i| &snapshot.instructions[i]).ok_or("origin instruction missing from snapshot")?;
let operation = instruction.operations.get(origin.operation_index).ok_or("origin operation missing from snapshot")?;
Ok(json!({"function_address":function_address,"snapshot_id":function_metrics[&function_address]["snapshot_id"],
"origin":origin,"operation":operation,"op":format!("{operation:?}"),"disassembly":instruction.disassembly}))
}).collect::<Result<_, String>>()?;
for (function_address, snapshot) in &snapshots {
for diag in &snapshot.ssa.diagnostics {
diagnostics.push(json!({"stage":"ssa","code":format!("{:?}",diag.kind),"function_address":function_address,
"severity":format!("{:?}",diag.severity),"address":diag.addr.map(|a| format!("0x{a:x}")),"message":diag.detail}));
}
}
if slice.nodes.is_empty() {
let instructions: Vec<_> = snapshot.instructions.iter().take(AGENT_CARD_MAX_INSTRUCTIONS).map(|i|
json!({"address":format!("0x{:x}",i.address),"length":i.len,"disassembly":i.disassembly,"constructor":i.constructor})).collect();
partial_functions.push(json!({"function_address":address,"instructions":instructions,
"decoded_total":snapshot.instructions.len(),"truncated":snapshot.instructions.len()>AGENT_CARD_MAX_INSTRUCTIONS}));
}
metrics["execution"] = json!(rsleigh_decompile::budget::metrics());
let functions: Vec<_> = function_metrics.iter().map(|(function_address, metrics)| json!({"function_address":function_address,"snapshot_id":metrics["snapshot_id"]})).collect();
metrics["functions"] = json!(function_metrics.into_iter().map(|(function_address, metrics)| json!({"function_address":function_address,"metrics":metrics})).collect::<Vec<_>>());
let status = if slice.complete && diagnostics.is_empty() {
"ok"
} else {
"partial"
};
emit(&json!({"schema":"rsleigh.ssa-slice/v3","status":status,
"file_sha256":compute_sha256(data),"tool_version":env!("CARGO_PKG_VERSION"),
"function_address":format!("0x{address:x}"),"arch":format!("{arch:?}"),
"snapshot":"post-fold/v3","diagnostics":diagnostics,"selection":selection,"slice":slice,"metrics":metrics,
"evidence":{"snapshot_id":metrics["snapshot_id"],"operations":evidence,"operation_stage":"raw-pcode/v1",
"origins_per_node_limit":rsleigh_decompile::provenance::MAX_ORIGINS,"functions":functions,"partial_functions":partial_functions},
"scope":"bounded expression, exact-location memory, and helper-call dependencies; remaining aliases, side effects, and traversal limits are explicit boundaries"}));
Ok(status_exit(status))
}