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::HashSet;

use super::super::super::HighLevelEmitter;

impl HighLevelEmitter {
    /// Recognises the `label_X: ... goto label_X;` shape with no other
    /// references to `label_X` and rewrites it to a `loop { ... }` block.
    /// This is the canonical pattern for an unconditional infinite loop
    /// produced by the Neo C# compiler; lifting it removes both the label
    /// and the goto, leaving idiomatic source.
    pub(crate) fn rewrite_label_goto_to_loop(statements: &mut [String]) {
        // A `label_X:` can only fold into a `loop { }` when a matching STANDALONE
        // `goto label_X;` exists. Pre-collect those lines once so labels without
        // one are skipped in O(1) instead of each scanning the whole tail of the
        // vector. Without this the pass is O(labels x N): a crafted in-cap NEF
        // that emits many guarded gotos (e.g. crossing JMPIF chains, whose gotos
        // render as inline `if c { goto X; }` and never match the standalone
        // form) drives it quadratic — a decompiler-hang DoS.
        let standalone_gotos: HashSet<String> = statements
            .iter()
            .map(|stmt| stmt.trim())
            .filter(|trimmed| trimmed.starts_with("goto label_") && trimmed.ends_with(';'))
            .map(str::to_string)
            .collect();
        if standalone_gotos.is_empty() {
            return;
        }

        let mut index = 0;
        while index < statements.len() {
            let trimmed = statements[index].trim().to_string();

            // Match: `label_0xXXXX:`
            let Some(label) = trimmed.strip_suffix(':') else {
                index += 1;
                continue;
            };
            if !label.starts_with("label_") {
                index += 1;
                continue;
            }
            let label = label.to_string();

            // Find the matching `goto label_X;` at the same brace depth.
            let goto_target = format!("goto {label};");
            // Fast-skip: with no standalone goto for this label, the forward
            // search below can only fail, so skip the scan entirely.
            if !standalone_gotos.contains(&goto_target) {
                index += 1;
                continue;
            }
            let mut depth = 0i32;
            let mut goto_idx = None;
            for (j, stmt) in statements.iter().enumerate().skip(index + 1) {
                let t = stmt.trim();
                if t == goto_target && depth == 0 {
                    goto_idx = Some(j);
                    break;
                }
                if t.ends_with('{') {
                    depth += 1;
                }
                if t == "}" || t.starts_with("} ") {
                    depth -= 1;
                    if depth < 0 {
                        // Exited the enclosing block before finding the goto.
                        break;
                    }
                }
            }
            let Some(goto_idx) = goto_idx else {
                index += 1;
                continue;
            };

            // Bail if any other reference to the label appears anywhere —
            // a second goto means the label is a structured-jump target,
            // not just a back-edge for an infinite loop, and lifting would
            // change semantics. (`label_decl` is hoisted out of the scan so it
            // is allocated once rather than per statement, and `.any()`
            // short-circuits on the first extra reference.)
            let label_decl = format!("{label}:");
            let has_other_reference = statements.iter().enumerate().any(|(i, stmt)| {
                i != index
                    && i != goto_idx
                    && (stmt.trim() == label_decl || stmt.contains(&goto_target))
            });
            if has_other_reference {
                index += 1;
                continue;
            }

            // Preserve indentation from the original label line so the
            // loop block stays aligned with surrounding code.
            let label_indent_len = statements[index].len() - statements[index].trim_start().len();
            let label_indent = statements[index][..label_indent_len].to_string();
            let goto_indent_len =
                statements[goto_idx].len() - statements[goto_idx].trim_start().len();
            let goto_indent = statements[goto_idx][..goto_indent_len].to_string();

            statements[index] = format!("{label_indent}loop {{");
            statements[goto_idx] = format!("{goto_indent}}}");
            index += 1;
        }
    }
}