neo-decompiler 0.11.0

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 std::collections::{BTreeMap, BTreeSet};

use super::super::super::HighLevelEmitter;
use super::super::util::is_if_open;
use super::parse::{
    is_literal, parse_case_sides, parse_inline_if_goto, parse_label_line, parse_plain_goto_label,
    resolve_case_value, resolve_condition_expression,
};
use super::scan::{
    collect_trivia, find_else_block_after, find_label_after, find_label_body_end,
    find_label_in_range, temp_consumed_by_next_code,
};

const MIN_GUARDED_GOTO_CASES: usize = 2;

pub(super) fn try_build_guarded_goto_switch(
    statements: &[String],
    start: usize,
) -> Option<(Vec<String>, usize)> {
    let mut current_header = start;
    let mut labeled_cases: Vec<(String, String)> = Vec::new();
    let mut scrutinee: Option<String> = None;

    loop {
        let header_line = statements.get(current_header)?.trim();
        if let Some((condition, label)) = parse_inline_if_goto(header_line) {
            let resolved =
                resolve_condition_expression(statements, current_header, condition.as_str())?;
            let (next_scrutinee, case_token) = parse_case_sides(resolved.as_str())?;
            let case_value = resolve_case_value(statements, current_header, case_token)?;
            if !is_literal(case_value.as_str()) {
                return None;
            }
            if let Some(existing) = &scrutinee {
                if existing != &next_scrutinee {
                    return None;
                }
            } else {
                scrutinee = Some(next_scrutinee);
            }
            labeled_cases.push((case_value, label));

            let next_header =
                find_next_guarded_header_after_case_prelude(statements, current_header + 1)?;
            current_header = next_header;
            continue;
        }
        break;
    }

    let header_line = statements.get(current_header)?.trim();
    if !is_if_open(header_line) {
        return None;
    }
    let condition = super::super::util::extract_any_if_condition(header_line)?;
    let resolved = resolve_condition_expression(statements, current_header, condition)?;
    let (next_scrutinee, case_token) = parse_case_sides(resolved.as_str())?;
    let final_case_value = resolve_case_value(statements, current_header, case_token)?;
    if !is_literal(final_case_value.as_str()) {
        return None;
    }
    if let Some(existing) = &scrutinee {
        if existing != &next_scrutinee {
            return None;
        }
    } else {
        scrutinee = Some(next_scrutinee);
    }

    let final_if_end = HighLevelEmitter::find_block_end(statements, current_header)?;
    let (default_label, label_blocks_start) =
        parse_guarded_switch_body_header(statements, current_header + 1, final_if_end)?;
    let label_bodies = collect_label_bodies(statements, label_blocks_start, final_if_end)?;

    let mut cases: Vec<(String, Vec<String>)> = Vec::new();
    for (case_value, label) in &labeled_cases {
        let body = label_bodies.get(label)?;
        if body.is_empty() {
            return None;
        }
        cases.push((case_value.clone(), body.clone()));
    }

    let (final_case_body, default_body, rewrite_end) = if let Some((else_header, else_end)) =
        find_else_block_after(statements, final_if_end + 1)
    {
        if let Some(default_label_index) = find_label_in_range(
            statements,
            else_header + 1,
            else_end,
            default_label.as_str(),
        ) {
            let final_case_body = statements
                .get(else_header + 1..default_label_index)
                .unwrap_or_default()
                .to_vec();
            if final_case_body.is_empty() {
                return None;
            }
            let default_body = statements
                .get(default_label_index + 1..else_end)
                .unwrap_or_default()
                .to_vec();
            if default_body.is_empty() {
                return None;
            }
            (final_case_body, default_body, else_end)
        } else {
            let final_case_body = statements
                .get(else_header + 1..else_end)
                .unwrap_or_default()
                .to_vec();
            if final_case_body.is_empty() {
                return None;
            }
            let default_label_index =
                find_label_after(statements, else_end + 1, default_label.as_str())?;
            let default_end = find_label_body_end(statements, default_label_index + 1);
            if default_end < default_label_index + 1 {
                return None;
            }
            let default_body = statements
                .get(default_label_index + 1..=default_end)
                .unwrap_or_default()
                .to_vec();
            if default_body.is_empty() {
                return None;
            }
            (final_case_body, default_body, default_end)
        }
    } else {
        let default_label_index =
            find_label_after(statements, final_if_end + 1, default_label.as_str())?;
        let final_case_body = statements
            .get(final_if_end + 1..default_label_index)
            .unwrap_or_default()
            .to_vec();
        if final_case_body.is_empty() {
            return None;
        }
        let default_end = find_label_body_end(statements, default_label_index + 1);
        if default_end < default_label_index + 1 {
            return None;
        }
        let default_body = statements
            .get(default_label_index + 1..=default_end)
            .unwrap_or_default()
            .to_vec();
        if default_body.is_empty() {
            return None;
        }
        (final_case_body, default_body, default_end)
    };

    cases.push((final_case_value, final_case_body));

    if cases.len() < MIN_GUARDED_GOTO_CASES {
        return None;
    }

    let mut seen = BTreeSet::new();
    if !cases.iter().all(|(value, _)| seen.insert(value.clone())) {
        return None;
    }

    let mut output = Vec::new();
    output.push(format!("switch {} {{", scrutinee?));
    for (value, body) in &cases {
        output.push(format!("case {value} {{"));
        output.extend(body.iter().cloned());
        output.push("}".into());
    }
    output.push("default {".into());
    output.extend(default_body);
    output.push("}".into());
    output.push("}".into());

    Some((output, rewrite_end))
}

fn find_next_guarded_header_after_case_prelude(
    statements: &[String],
    start: usize,
) -> Option<usize> {
    let mut index = start;
    while index < statements.len() {
        let trimmed = statements[index].trim();
        if trimmed.is_empty() || trimmed.starts_with("//") {
            index += 1;
            continue;
        }
        if parse_inline_if_goto(trimmed).is_some() || is_if_open(trimmed) {
            return Some(index);
        }
        if let Some(assign) = HighLevelEmitter::parse_assignment(statements[index].as_str()) {
            if super::parse::is_temp(&assign.lhs)
                && temp_consumed_by_next_code(statements, index, &assign.lhs)
            {
                index += 1;
                continue;
            }
        }
        return None;
    }
    None
}

fn parse_guarded_switch_body_header(
    statements: &[String],
    start: usize,
    end: usize,
) -> Option<(String, usize)> {
    let (_, first_code) = collect_trivia(statements, start);
    if first_code >= end {
        return None;
    }
    let default_label = parse_plain_goto_label(statements[first_code].as_str())?;
    let (_, body_start) = collect_trivia(statements, first_code + 1);
    if body_start >= end {
        return None;
    }
    parse_label_line(statements[body_start].as_str())?;
    Some((default_label, body_start))
}

fn collect_label_bodies(
    statements: &[String],
    start: usize,
    end: usize,
) -> Option<BTreeMap<String, Vec<String>>> {
    let mut bodies: BTreeMap<String, Vec<String>> = BTreeMap::new();
    let mut current_label: Option<String> = None;
    let mut index = start;

    while index < end {
        let trimmed = statements[index].trim();
        if let Some(label) = parse_label_line(trimmed) {
            if bodies.contains_key(&label) {
                return None;
            }
            current_label = Some(label.clone());
            bodies.insert(label, Vec::new());
            index += 1;
            continue;
        }

        let Some(label) = current_label.as_ref() else {
            if trimmed.is_empty() || trimmed.starts_with("//") {
                index += 1;
                continue;
            }
            return None;
        };
        bodies
            .entry(label.clone())
            .or_default()
            .push(statements[index].clone());
        index += 1;
    }

    Some(bodies)
}