use std::collections::BTreeSet;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use assert2::assert;
use idakit::decompiler::ctree::{Ctree, ExpressionKind, NodeRef, StatementKind};
use idakit::prelude::*;
use idakit::types::TypeShape;
fn gxx_available() -> bool {
Command::new("g++")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
fn expr_name(k: &ExpressionKind) -> &'static str {
match k {
ExpressionKind::Binary { .. } => "Binary",
ExpressionKind::Assign { .. } => "Assign",
ExpressionKind::Unary { .. } => "Unary",
ExpressionKind::Ternary { .. } => "Ternary",
ExpressionKind::Call { .. } => "Call",
ExpressionKind::Index { .. } => "Index",
ExpressionKind::MemberRef { .. } => "MemberRef",
ExpressionKind::MemberPtr { .. } => "MemberPtr",
ExpressionKind::Cast { .. } => "Cast",
ExpressionKind::Deref { .. } => "Deref",
ExpressionKind::Sizeof(_) => "Sizeof",
ExpressionKind::Num(_) => "Num",
ExpressionKind::Fnum(_) => "Fnum",
ExpressionKind::Str(_) => "Str",
ExpressionKind::Obj { .. } => "Obj",
ExpressionKind::Var(_) => "Var",
ExpressionKind::Helper(_) => "Helper",
ExpressionKind::TypeExpression => "TypeExpression",
ExpressionKind::Empty => "Empty",
ExpressionKind::Internal => "Internal",
}
}
fn stmt_name(k: &StatementKind) -> &'static str {
match k {
StatementKind::Block(_) => "Block",
StatementKind::Expression(_) => "Expression",
StatementKind::If { .. } => "If",
StatementKind::For { .. } => "For",
StatementKind::While { .. } => "While",
StatementKind::Do { .. } => "Do",
StatementKind::Switch { .. } => "Switch",
StatementKind::Break => "Break",
StatementKind::Continue => "Continue",
StatementKind::Return(_) => "Return",
StatementKind::Goto { .. } => "Goto",
StatementKind::Asm(_) => "Asm",
StatementKind::Try { .. } => "Try",
StatementKind::Throw(_) => "Throw",
StatementKind::Empty => "Empty",
}
}
fn shape_name(s: &TypeShape) -> &'static str {
match s {
TypeShape::Void => "Void",
TypeShape::Bool => "Bool",
TypeShape::Int { .. } => "Int",
TypeShape::Float { .. } => "Float",
TypeShape::Ptr(_) => "Ptr",
TypeShape::Array { .. } => "Array",
TypeShape::Struct { .. } => "Struct",
TypeShape::Union { .. } => "Union",
TypeShape::Enum { .. } => "Enum",
TypeShape::Function { .. } => "Function",
TypeShape::Typedef { .. } => "Typedef",
TypeShape::Opaque(_) => "Opaque",
TypeShape::Unknown => "Unknown",
}
}
fn all_reachable(tree: &Ctree) -> bool {
let total = tree.expressions().count() + tree.statements().count();
let seen: BTreeSet<NodeRef> = tree.descendants(NodeRef::Statement(tree.root())).collect();
seen.len() == total
}
fn switch_distinct_values(tree: &Ctree) -> impl Iterator<Item = usize> + '_ {
tree.statements().filter_map(|(_, s)| match &s.kind {
StatementKind::Switch { cases, .. } => Some(
cases
.iter()
.flat_map(|c| &c.values)
.collect::<BTreeSet<_>>()
.len(),
),
_ => None,
})
}
fn assert_extraction_covers(trees: &[Ctree]) {
let mut exprs = BTreeSet::new();
let mut stmts = BTreeSet::new();
let mut shapes = BTreeSet::new();
let mut any_expr_address = false;
let mut any_named_obj = false;
let mut any_non_arg_local = false;
let mut any_plain_arg_local = false;
let mut any_commented_local = false;
let mut switch_values: Vec<usize> = Vec::new();
for tree in trees {
for (_, e) in tree.expressions() {
exprs.insert(expr_name(&e.kind));
if e.address.is_some() {
any_expr_address = true;
}
if let ExpressionKind::Obj { name: Some(n), .. } = &e.kind
&& !n.is_empty()
{
any_named_obj = true;
}
}
for (_, s) in tree.statements() {
stmts.insert(stmt_name(&s.kind));
}
for (_, t) in tree.types() {
shapes.insert(shape_name(&t.shape));
}
for l in tree.locals() {
if !l.is_arg {
any_non_arg_local = true;
}
if l.is_arg && !l.is_byref && !l.is_result {
any_plain_arg_local = true;
}
if l.comment.is_some() {
any_commented_local = true;
}
}
switch_values.extend(switch_distinct_values(tree));
}
for k in [
"Binary", "Assign", "Unary", "Call", "Index", "Cast", "Deref", "Num", "Fnum", "Obj", "Var",
"Helper",
] {
assert!(exprs.contains(k), "missing expression kind {k}: {exprs:?}");
}
for k in [
"Block",
"Expression",
"If",
"For",
"While",
"Do",
"Switch",
"Break",
"Return",
"Empty",
] {
assert!(stmts.contains(k), "missing statement kind {k}: {stmts:?}");
}
for k in [
"Ptr", "Array", "Struct", "Enum", "Typedef", "Function", "Opaque",
] {
assert!(shapes.contains(k), "missing type shape {k}: {shapes:?}");
}
assert!(any_expr_address, "no expression carried a source address");
assert!(any_named_obj, "no global reference kept its symbol name");
assert!(any_non_arg_local, "every local was flagged an argument");
assert!(
any_plain_arg_local,
"no plain (value, non-result) argument survived"
);
assert!(!any_commented_local, "a local gained a spurious comment");
let best = switch_values.iter().copied().max().unwrap_or(0);
assert!(
best >= 5,
"switch case-value pool was mis-sliced: {best} distinct values, counts {switch_values:?}"
);
}
#[test]
fn ctree_nodes() {
if !gxx_available() {
eprintln!("skipping: g++ not available to build the fixture");
return;
}
let src = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/ctree_kinds.cpp"
);
let name = format!(
"idakit_ctree_kinds_{}{}",
std::process::id(),
std::env::consts::EXE_SUFFIX
);
let bin: PathBuf = std::env::temp_dir().join(&name);
let mut cmd = Command::new("g++");
cmd.args(["-O0", "-g0", "-w", "-fno-inline"]);
if cfg!(target_os = "macos") {
cmd.args(["-arch", "x86_64"]);
}
let status = cmd
.arg("-o")
.arg(&bin)
.arg(src)
.status()
.expect("failed to spawn g++");
assert!(status.success(), "g++ failed to compile the fixture");
let bin_str = bin.to_string_lossy().into_owned();
Ida::run(move |ida| {
ida.call(move |idb| {
idb.open(&bin_str)
.run_auto(true)
.call()
.expect("open + auto-analysis failed");
let targets: Vec<Address> = idb.functions().map(|f| f.address()).collect();
let mut trees: Vec<Ctree> = Vec::new();
let mut saw_pseudocode_body = false;
let mut saw_plausible_gap = false;
let mut saw_debug = false;
for addr in targets {
let Ok(df) = idb.decompile(addr) else {
continue;
};
if let Some(pc) = df.pseudocode() {
if pc.contains('{') {
saw_pseudocode_body = true;
}
}
let (visitor_total, expected) = df.expr_extraction_expectation();
if visitor_total > 1 && expected > 1 && visitor_total >= expected {
saw_plausible_gap = true;
}
let debug = format!("{df:?}");
if debug.contains("DecompiledFunction") && debug.contains("counts") {
saw_debug = true;
}
let Ok(tree) = df.ctree() else {
continue;
};
assert!(all_reachable(&tree), "extraction left an unreachable node");
trees.push(tree);
}
assert!(!trees.is_empty(), "no function decompiled");
assert!(
saw_pseudocode_body,
"pseudocode never rendered a function body"
);
assert!(
saw_plausible_gap,
"expr_extraction_expectation never returned real counts"
);
assert!(
saw_debug,
"Debug never rendered the DecompiledFunction struct"
);
assert_extraction_covers(&trees);
idb.close(false);
})
.unwrap_or_else(|e| e.resume());
})
.expect("kernel init failed");
let _ = std::fs::remove_file(&bin);
let _ = std::fs::remove_file(bin.with_file_name(format!("{name}.i64")));
}