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 super::*;
use crate::instruction::OpCode;

#[test]
fn high_level_lifts_try_finally_blocks() {
    // Script models:
    // TRY (finally at +3)
    //   PUSH1
    //   ENDTRY +2 (resume after finally)
    // FINALLY:
    //   PUSH2
    //   ENDFINALLY
    // RET
    let script = [
        OpCode::Try.byte(),
        0x00,
        0x03,
        OpCode::Push1.byte(),
        OpCode::Endtry.byte(),
        0x02,
        OpCode::Push2.byte(),
        OpCode::Endfinally.byte(),
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("try {"),
        "missing try block: {high_level}"
    );
    assert!(
        high_level.contains("finally {"),
        "missing finally block: {high_level}"
    );
    assert!(
        !high_level.contains("end-try ->"),
        "structured try should suppress ENDTRY note: {high_level}"
    );
    assert!(
        !high_level.contains("endfinally"),
        "structured try should suppress ENDFINALLY note: {high_level}"
    );
}

#[test]
fn high_level_lifts_try_catch_blocks() {
    // Script models:
    // TRY (catch at +6)
    //   PUSH1
    //   ENDTRY +3 (skip catch)
    // CATCH:
    //   PUSH2
    //   ENDTRY +0
    // RET
    let script = [
        OpCode::Try.byte(),
        0x03,
        0x00,
        OpCode::Push1.byte(),
        OpCode::Endtry.byte(),
        0x03,
        OpCode::Push2.byte(),
        OpCode::Endtry.byte(),
        0x00,
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("try {"),
        "missing try header: {high_level}"
    );
    assert!(
        high_level.contains("catch {"),
        "missing catch header: {high_level}"
    );
    assert!(
        !high_level.contains("end-try ->"),
        "structured try should suppress ENDTRY notes: {high_level}"
    );
}

#[test]
fn high_level_lifts_try_catch_finally_blocks() {
    // Script models:
    // TRY (catch at +3, finally at +6)
    //   PUSH1
    //   ENDTRY +5 (skip catch/finally)
    // CATCH:
    //   PUSH2
    //   ENDTRY +2 (skip finally)
    // FINALLY:
    //   PUSH3
    //   ENDFINALLY
    // RET
    let script = [
        OpCode::Try.byte(),
        0x03,
        0x06,
        OpCode::Push1.byte(),
        OpCode::Endtry.byte(),
        0x05,
        OpCode::Push2.byte(),
        OpCode::Endtry.byte(),
        0x02,
        OpCode::Push3.byte(),
        OpCode::Endfinally.byte(),
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("try {"),
        "missing try header: {high_level}"
    );
    assert!(
        high_level.contains("catch {"),
        "missing catch header: {high_level}"
    );
    assert!(
        high_level.contains("finally {"),
        "missing finally header: {high_level}"
    );
    assert!(
        !high_level.contains("endfinally"),
        "structured try should suppress ENDFINALLY note: {high_level}"
    );
}

#[test]
fn high_level_lifts_try_finally_with_throw_inside() {
    // TRY (finally at +4)
    //   PUSH1
    //   THROW
    //   ENDTRY +2 (resume after finally)
    // FINALLY:
    //   PUSH2
    //   ENDFINALLY
    // RET
    let script = [
        OpCode::Try.byte(),
        0x00,
        0x04,                 // TRY (catch=0, finally=+4)
        OpCode::Push1.byte(), // PUSH1
        OpCode::Throw.byte(), // THROW
        OpCode::Endtry.byte(),
        0x02,                      // ENDTRY +2
        OpCode::Push2.byte(),      // PUSH2
        OpCode::Endfinally.byte(), // ENDFINALLY
        OpCode::Ret.byte(),        // RET
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("try {"),
        "missing try block: {high_level}"
    );
    assert!(
        high_level.contains("throw("),
        "THROW inside try should be lifted: {high_level}"
    );
    assert!(
        high_level.contains("finally {"),
        "missing finally block: {high_level}"
    );
}

#[test]
fn high_level_lifts_try_catch_with_abort_in_catch() {
    // TRY (catch at +3)
    //   PUSH1
    //   ENDTRY +3 (skip catch)
    // CATCH:
    //   ABORT
    //   ENDTRY +0
    // RET
    let script = [
        OpCode::Try.byte(),
        0x03,
        0x00,                 // TRY (catch=+3, finally=0)
        OpCode::Push1.byte(), // PUSH1
        OpCode::Endtry.byte(),
        0x03,                 // ENDTRY +3
        OpCode::Abort.byte(), // ABORT
        OpCode::Endtry.byte(),
        0x00,               // ENDTRY +0
        OpCode::Ret.byte(), // RET
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("try {"),
        "missing try header: {high_level}"
    );
    assert!(
        high_level.contains("catch {"),
        "missing catch header: {high_level}"
    );
    assert!(
        high_level.contains("abort()"),
        "ABORT inside catch should be lifted: {high_level}"
    );
}

#[test]
fn high_level_models_catch_entry_stack_with_exception_value() {
    // TRY (catch at +3)
    //   PUSH1
    //   ENDTRY +4 (skip catch)
    // CATCH:
    //   STLOC0  // consume exception object
    //   LDLOC0
    //   ENDTRY +0
    // RET
    let script = [
        OpCode::Try.byte(),
        0x06,
        0x00,                 // TRY (catch=+6, finally=0)
        OpCode::Push1.byte(), // PUSH1
        OpCode::Endtry.byte(),
        0x06,                  // ENDTRY +6
        OpCode::Stloc0.byte(), // STLOC0
        OpCode::Ldloc0.byte(), // LDLOC0
        OpCode::Endtry.byte(),
        0x00,               // ENDTRY +0
        OpCode::Ret.byte(), // RET
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("catch {"),
        "missing catch header: {high_level}"
    );
    assert!(
        high_level.contains("let loc0 = exception;"),
        "catch entry should model exception object stack item for STLOC0: {high_level}"
    );
    assert!(
        !high_level.contains("insufficient values on stack"),
        "catch entry STLOC0 should not underflow the stack: {high_level}"
    );
}

#[test]
fn malformed_try_out_of_bounds_handlers_keep_braces_balanced() {
    // A TRY whose catch/finally offsets point far out of bounds: the matching
    // `catch {`/`finally {` headers are never emitted (their target offsets are
    // never reached during the walk), but a closer is registered for each. The
    // closer flush must clamp to the open-brace depth so the rendered output
    // stays brace-balanced instead of emitting stray `}`.
    let script = [OpCode::Try.byte(), 0x7F, 0x7F, OpCode::Ret.byte()]; // TRY catch=+127 finally=+127 ; RET
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");
    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    let balance: i32 = high_level
        .chars()
        .map(|c| match c {
            '{' => 1,
            '}' => -1,
            _ => 0,
        })
        .sum();
    assert_eq!(
        balance, 0,
        "malformed try output must stay brace-balanced:\n{high_level}"
    );
}