neo-decompiler 0.8.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::Operand;

#[test]
fn decodes_simple_sequence() {
    let bytecode = [0x10, 0x11, 0x9E, 0x40];
    let instructions = Disassembler::new()
        .disassemble(&bytecode)
        .expect("disassembly succeeds");

    let mnemonics: Vec<_> = instructions
        .iter()
        .map(|ins| ins.opcode.mnemonic())
        .collect();
    assert_eq!(mnemonics, vec!["PUSH0", "PUSH1", "ADD", "RET"]);
}

#[test]
fn errors_on_unknown_opcode() {
    let bytecode = [0xFF];
    let err = Disassembler::with_unknown_handling(UnknownHandling::Error)
        .disassemble(&bytecode)
        .unwrap_err();
    assert!(matches!(
        err,
        crate::error::Error::Disassembly(DisassemblyError::UnknownOpcode {
            opcode: 0xFF,
            offset: 0
        })
    ));
}

#[test]
fn permits_unknown_opcode_when_configured() {
    let bytecode = [0xFF, 0x40];
    let instructions = Disassembler::with_unknown_handling(UnknownHandling::Permit)
        .disassemble(&bytecode)
        .expect("disassembly succeeds in tolerant mode");

    assert_eq!(instructions.len(), 2);
    assert!(matches!(instructions[0].opcode, OpCode::Unknown(0xFF)));
    assert_eq!(instructions[1].opcode, OpCode::Ret);
}

#[test]
fn reports_warning_for_unknown_opcode_in_tolerant_mode() {
    let bytecode = [0xFF, 0x40];
    let output = Disassembler::with_unknown_handling(UnknownHandling::Permit)
        .disassemble_with_warnings(&bytecode)
        .expect("disassembly succeeds in tolerant mode");

    assert_eq!(output.instructions.len(), 2);
    assert_eq!(output.warnings.len(), 1);
    assert!(matches!(
        output.warnings[0],
        DisassemblyWarning::UnknownOpcode {
            opcode: 0xFF,
            offset: 0
        }
    ));
}

#[test]
fn fails_on_truncated_operand() {
    let bytecode = [0x01, 0x00];
    let err = Disassembler::new().disassemble(&bytecode).unwrap_err();
    assert!(matches!(
        err,
        crate::error::Error::Disassembly(DisassemblyError::UnexpectedEof { offset: 0 })
    ));
}

#[test]
fn decodes_calla_no_operand() {
    // CALLA (0x36) takes no inline operand — it pops a Pointer from the stack.
    let bytecode = [0x36];
    let instructions = Disassembler::new()
        .disassemble(&bytecode)
        .expect("disassembly succeeds");

    assert_eq!(instructions.len(), 1);
    assert_eq!(instructions[0].opcode, OpCode::CallA);
    assert_eq!(instructions[0].operand, None);
}

#[test]
fn decodes_pushdata2() {
    let bytecode = [0x0D, 0x04, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
    let instruction = Disassembler::new().disassemble(&bytecode).expect("success")[0].clone();
    assert_eq!(instruction.opcode.mnemonic(), "PUSHDATA2");
    assert_eq!(instruction.offset, 0);
    assert_eq!(
        instruction.operand,
        Some(Operand::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]))
    );
    assert_eq!(instruction.opcode, OpCode::Pushdata2);
}

#[test]
fn decodes_jump_long() {
    let bytecode = [0x23, 0x34, 0x12, 0x00, 0x00];
    let instruction = Disassembler::new().disassemble(&bytecode).expect("success")[0].clone();
    assert_eq!(instruction.opcode, OpCode::Jmp_L);
    assert_eq!(instruction.operand, Some(Operand::Jump32(0x1234)));
    assert_eq!(instruction.offset, 0);
}

#[test]
fn decodes_pusha_backward_offset_as_signed_i32() {
    // PUSHA carries a signed 32-bit relative offset (backward pointers
    // are legal per the C# OpCode definition). A6 FF FF FF is -90 and
    // must render as "-90" in listings, not as unsigned 4294967206.
    let bytecode = [0x0A, 0xA6, 0xFF, 0xFF, 0xFF];
    let instruction = Disassembler::new().disassemble(&bytecode).expect("success")[0].clone();
    assert_eq!(instruction.opcode, OpCode::PushA);
    assert_eq!(instruction.operand, Some(Operand::I32(-90)));
    assert_eq!(instruction.operand.expect("operand").to_string(), "-90");
}

#[test]
fn decodes_pusha_forward_offset_as_signed_i32() {
    let bytecode = [0x0A, 0x05, 0x00, 0x00, 0x00, 0x40];
    let instruction = Disassembler::new().disassemble(&bytecode).expect("success")[0].clone();
    assert_eq!(instruction.opcode, OpCode::PushA);
    assert_eq!(instruction.operand, Some(Operand::I32(5)));
    assert_eq!(instruction.operand.expect("operand").to_string(), "5");
}

#[test]
fn pusha_truncated_operand_returns_unexpected_eof() {
    // PUSHA needs a 4-byte operand; provide only 3.
    let bytecode = [0x0A, 0xA6, 0xFF, 0xFF];
    let err = Disassembler::new().disassemble(&bytecode).unwrap_err();
    assert!(matches!(
        err,
        crate::error::Error::Disassembly(DisassemblyError::UnexpectedEof { offset: 0 })
    ));
}

#[test]
fn pushdata2_truncated_length_prefix_returns_unexpected_eof() {
    // PUSHDATA2's 2-byte length prefix is itself truncated (1 byte).
    let bytecode = [0x0D, 0x10];
    let err = Disassembler::new().disassemble(&bytecode).unwrap_err();
    assert!(matches!(
        err,
        crate::error::Error::Disassembly(DisassemblyError::UnexpectedEof { offset: 0 })
    ));
}

#[test]
fn pushdata4_truncated_length_prefix_returns_unexpected_eof() {
    // PUSHDATA4's 4-byte length prefix is itself truncated (3 bytes).
    // The partial bytes would decode to 0xFFFFFF if read off the end —
    // the error must still be UnexpectedEof, not OperandTooLarge.
    let bytecode = [0x0E, 0xFF, 0xFF, 0xFF];
    let err = Disassembler::new().disassemble(&bytecode).unwrap_err();
    assert!(matches!(
        err,
        crate::error::Error::Disassembly(DisassemblyError::UnexpectedEof { offset: 0 })
    ));
}

#[test]
fn decodes_syscall_operand_with_name() {
    // System.Runtime.Platform
    let bytecode = [0x41, 0xB2, 0x79, 0xFC, 0xF6];
    let instruction = Disassembler::new().disassemble(&bytecode).expect("success")[0].clone();
    assert_eq!(instruction.opcode, OpCode::Syscall);
    let operand = instruction.operand.expect("syscall operand");
    assert_eq!(operand.to_string(), "System.Runtime.Platform (0xF6FC79B2)");
}

#[test]
fn pushdata4_excessive_length_returns_operand_too_large() {
    // PUSHDATA4 length is little-endian u32.
    // MAX_OPERAND_LEN is 1_048_576, so request one more than that.
    let bytecode = [0x0E, 0x01, 0x00, 0x10, 0x00];
    let err = Disassembler::new().disassemble(&bytecode).unwrap_err();
    assert!(matches!(
        err,
        crate::error::Error::Disassembly(DisassemblyError::OperandTooLarge {
            offset: 0,
            len: 1_048_577
        })
    ));
}

#[test]
fn pushdata4_truncated_payload_returns_unexpected_eof() {
    // PUSHDATA4 claims a 4-byte payload but only provides 2 bytes.
    let bytecode = [0x0E, 0x04, 0x00, 0x00, 0x00, 0xAA, 0xBB];
    let err = Disassembler::new().disassemble(&bytecode).unwrap_err();
    assert!(matches!(
        err,
        crate::error::Error::Disassembly(DisassemblyError::UnexpectedEof { offset: 0 })
    ));
}