neo-decompiler 0.10.1

Neo N3 NEF decompiler: parse, disassemble, lift bytecode to high-level pseudocode and C# skeletons, with a CLI, JSON reports, and optional WebAssembly bindings.
Documentation
use crate::decompiler::analysis::{MethodRef, MethodTable};
use crate::decompiler::cfg::{BasicBlock, BlockId, Cfg, EdgeKind, Terminator};
use crate::instruction::Instruction;

use super::MethodView;

/// Build a sub-CFG for each method: select blocks whose first instruction
/// lies within the method's range, prepend a synthesised entry block, and
/// rewrite cross-range `Jump`/`Jmpif*`/`Jmpifnot*` terminators to `Return`
/// so the sub-CFG is self-contained. `instructions` is the whole-script
/// instruction stream; each `MethodView` receives the slice whose offsets
/// fall within the method's range so `SsaBuilder` only sees the method's
/// instructions.
pub(crate) fn extract_method_cfgs(
    whole: &Cfg,
    table: &MethodTable,
    instructions: &[Instruction],
) -> Vec<MethodView> {
    let mut out = Vec::new();
    for (start, end, method) in table.methods() {
        let method_instructions: Vec<_> = instructions
            .iter()
            .filter(|i| i.offset >= start && i.offset < end)
            .cloned()
            .collect();
        if let Some(view) = extract_one(whole, start, end, method.clone(), method_instructions) {
            out.push(view);
        }
    }
    out
}

fn extract_one(
    whole: &Cfg,
    start: usize,
    end: usize,
    method: MethodRef,
    instructions: Vec<Instruction>,
) -> Option<MethodView> {
    let mut selected: Vec<&BasicBlock> = whole
        .blocks()
        .filter(|b| b.instruction_range.start < end && b.start_offset >= start)
        .collect();
    if selected.is_empty() {
        return None;
    }
    let entry_existing = selected.iter().find(|b| b.start_offset == start).copied();
    selected.sort_by_key(|b| b.id.0);
    // Pick a fresh block ID for the synthesised entry that does not collide
    // with any block in the whole CFG (the existing entry block, if any, has
    // id == start, so using a high unused ID avoids aliasing).
    let max_id = whole.blocks().map(|b| b.id.0).max().unwrap_or(0);
    let entry_id = BlockId(max_id + 1);
    let mut sub = Cfg::new();

    for b in &selected {
        let mut nb = (*b).clone();
        // Rewrite cross-range jumps on every selected block, including the
        // method entry: a cross-range Jump from the entry means the method
        // tail-calls / jumps out, which the decompiler renders as `return`.
        nb.terminator = rewrite_terminator(&nb.terminator, &selected);
        sub.add_block(nb);
    }

    let entry_terminator = match entry_existing {
        Some(e) if matches!(e.terminator, Terminator::Fallthrough { .. }) => {
            Terminator::Fallthrough { target: e.id }
        }
        Some(e) => Terminator::Jump { target: e.id },
        None => Terminator::Return,
    };
    sub.add_block(BasicBlock::new(
        entry_id,
        start,
        start,
        start..start,
        entry_terminator,
    ));
    if let Some(eid) = entry_existing.map(|e| e.id) {
        sub.add_edge(entry_id, eid, EdgeKind::Unconditional);
    }

    for b in &selected {
        for s in b.terminator.successors() {
            if sub.block(s).is_some() && s != b.id {
                sub.add_edge(b.id, s, EdgeKind::Unconditional);
            }
        }
    }

    Some(MethodView {
        method,
        cfg: sub,
        instructions,
    })
}

fn rewrite_terminator(term: &Terminator, selected: &[&BasicBlock]) -> Terminator {
    let in_range = |bid: BlockId| selected.iter().any(|b| b.id == bid);
    match term {
        Terminator::Jump { target } if !in_range(*target) => Terminator::Return,
        Terminator::Branch {
            then_target,
            else_target,
        } if !in_range(*then_target) || !in_range(*else_target) => Terminator::Return,
        _ => term.clone(),
    }
}