neo-decompiler 0.10.2

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::super::super::*;

#[test]
fn high_level_roll_dynamic_index_preserves_stack_depth_after_drops() {
    // ROLL consumes the index and moves one existing value to the top; it does
    // not create a new stack item. After dropping the rolled value and the
    // remaining value, RET should be void.
    let script = [
        0x57, 0x00, 0x01, // INITSLOT 0 locals, 1 arg
        0x11, 0x12, 0x78, 0x52, 0x45, 0x45,
        0x40, // PUSH1; PUSH2; LDARG0; ROLL; DROP; DROP; RET
    ];
    let high_level = decompile_high_level(&script);

    assert!(
        high_level.contains("roll(arg0)"),
        "dynamic ROLL should still surface the helper expression: {high_level}"
    );
    assert!(
        high_level.contains("return;"),
        "dynamic ROLL must preserve stack depth after its result is dropped: {high_level}"
    );
    assert!(
        !high_level.contains("return 1;")
            && !high_level.contains("return 2;")
            && !high_level.contains("return t"),
        "dynamic ROLL must not leave a phantom stack value behind: {high_level}"
    );
}

#[test]
fn high_level_xdrop_dynamic_index_preserves_stack_depth_after_drop() {
    // XDROP consumes the index and removes one existing stack value. After one
    // more DROP, no value should remain for RET.
    let script = [
        0x57, 0x00, 0x01, // INITSLOT 0 locals, 1 arg
        0x11, 0x12, 0x78, 0x48, 0x45, 0x40, // PUSH1; PUSH2; LDARG0; XDROP; DROP; RET
    ];
    let high_level = decompile_high_level(&script);

    assert!(
        high_level.contains("return;"),
        "dynamic XDROP must preserve stack depth after a follow-up DROP: {high_level}"
    );
    assert!(
        !high_level.contains("return 1;")
            && !high_level.contains("return 2;")
            && !high_level.contains("return t"),
        "dynamic XDROP must not leave a phantom stack value behind: {high_level}"
    );
}

#[test]
fn high_level_xdrop_dynamic_index_does_not_return_stale_known_value() {
    // XDROP removes an unknown source value. The remaining top may be either
    // original value, so the decompiler must not confidently return 1 or 2.
    let script = [
        0x57, 0x00, 0x01, // INITSLOT 0 locals, 1 arg
        0x11, 0x12, 0x78, 0x48, 0x40, // PUSH1; PUSH2; LDARG0; XDROP; RET
    ];
    let high_level = decompile_high_level(&script);

    assert!(
        high_level.contains("xdrop_remaining(arg0)"),
        "dynamic XDROP should surface an unknown remaining top value: {high_level}"
    );
    assert!(
        !high_level.contains("return 1;") && !high_level.contains("return 2;"),
        "dynamic XDROP must not return a stale known value: {high_level}"
    );
}

#[test]
fn high_level_xdrop_dynamic_index_drop_does_not_return_deeper_stale_known_value() {
    // With three source values, an unknown XDROP can leave different values in
    // every surviving stack position. A later DROP must not expose a stale
    // deeper literal as the return value.
    let script = [
        0x57, 0x00, 0x01, // INITSLOT 0 locals, 1 arg
        0x11, 0x12, 0x13, 0x78, 0x48, 0x45,
        0x40,
        // PUSH1; PUSH2; PUSH3; LDARG0; XDROP; DROP; RET
    ];
    let high_level = decompile_high_level(&script);

    assert!(
        high_level.contains("xdrop_remaining(arg0)"),
        "dynamic XDROP should keep deeper survivors unknown: {high_level}"
    );
    assert!(
        !high_level.contains("return 1;")
            && !high_level.contains("return 2;")
            && !high_level.contains("return 3;"),
        "dynamic XDROP must not return a stale deeper value: {high_level}"
    );
}

#[test]
fn high_level_roll_dynamic_index_result_drop_does_not_return_stale_known_value() {
    // After dropping the dynamically rolled value, the remaining top depends on
    // which value was rolled. It must not be rendered as a confident literal.
    let script = [
        0x57, 0x00, 0x01, // INITSLOT 0 locals, 1 arg
        0x11, 0x12, 0x78, 0x52, 0x45, 0x40, // PUSH1; PUSH2; LDARG0; ROLL; DROP; RET
    ];
    let high_level = decompile_high_level(&script);

    assert!(
        high_level.contains("roll_remaining(arg0)"),
        "dynamic ROLL should preserve an unknown remaining top value: {high_level}"
    );
    assert!(
        !high_level.contains("return 1;") && !high_level.contains("return 2;"),
        "dynamic ROLL must not return a stale known value after dropping its result: {high_level}"
    );
}

#[test]
fn high_level_roll_dynamic_index_drops_do_not_return_deeper_stale_known_value() {
    // Dropping the unknown rolled value and then one survivor still leaves an
    // unknown value on top; the decompiler must not expose an older literal.
    let script = [
        0x57, 0x00, 0x01, // INITSLOT 0 locals, 1 arg
        0x11, 0x12, 0x13, 0x78, 0x52, 0x45, 0x45,
        0x40,
        // PUSH1; PUSH2; PUSH3; LDARG0; ROLL; DROP; DROP; RET
    ];
    let high_level = decompile_high_level(&script);

    assert!(
        high_level.contains("roll_remaining(arg0)"),
        "dynamic ROLL should keep deeper survivors unknown: {high_level}"
    );
    assert!(
        !high_level.contains("return 1;")
            && !high_level.contains("return 2;")
            && !high_level.contains("return 3;"),
        "dynamic ROLL must not return a stale deeper value: {high_level}"
    );
}

#[test]
fn high_level_reversen_dynamic_count_does_not_return_stale_known_value() {
    // Unknown reversal count means the top of the stack is not statically
    // knowable.
    let script = [
        0x57, 0x00, 0x01, // INITSLOT 0 locals, 1 arg
        0x11, 0x12, 0x78, 0x55, 0x40, // PUSH1; PUSH2; LDARG0; REVERSEN; RET
    ];
    let high_level = decompile_high_level(&script);

    assert!(
        high_level.contains("reverse_n_top(arg0)"),
        "dynamic REVERSEN should surface an unknown top value: {high_level}"
    );
    assert!(
        !high_level.contains("return 1;") && !high_level.contains("return 2;"),
        "dynamic REVERSEN must not return a stale known value: {high_level}"
    );
}

#[test]
fn high_level_reversen_dynamic_count_drop_does_not_return_deeper_stale_known_value() {
    // Unknown reversal count can change every stack position, so dropping the
    // top must not reveal a confident stale literal underneath.
    let script = [
        0x57, 0x00, 0x01, // INITSLOT 0 locals, 1 arg
        0x11, 0x12, 0x13, 0x78, 0x55, 0x45,
        0x40,
        // PUSH1; PUSH2; PUSH3; LDARG0; REVERSEN; DROP; RET
    ];
    let high_level = decompile_high_level(&script);

    assert!(
        high_level.contains("reverse_n_top(arg0)"),
        "dynamic REVERSEN should keep deeper survivors unknown: {high_level}"
    );
    assert!(
        !high_level.contains("return 1;")
            && !high_level.contains("return 2;")
            && !high_level.contains("return 3;"),
        "dynamic REVERSEN must not return a stale deeper value: {high_level}"
    );
}

fn decompile_high_level(script: &[u8]) -> String {
    let nef_bytes = build_nef(script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    decompilation.high_level.expect("high-level output")
}