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 crate::instruction::{Instruction, OpCode};

use super::super::super::super::{HighLevelEmitter, LiteralValue};

const DEFAULT_UNKNOWN_UNPACK_COUNT: usize = 4;

impl HighLevelEmitter {
    pub(in super::super::super::super) fn emit_unpack(&mut self, instruction: &Instruction) {
        self.push_comment(instruction);
        if let Some(value) = self.pop_stack_value() {
            if self.try_replay_known_pack(&value) {
                return;
            }
            self.emit_unknown_unpack(instruction, value);
        } else {
            self.stack_underflow(instruction, 1);
        }
    }

    fn try_replay_known_pack(&mut self, value: &str) -> bool {
        let Some(elements) = self.packed_values_by_name.get(value).cloned() else {
            return false;
        };
        // Neo VM UNPACK pushes array[n-1] first, array[0] last (on top). Our
        // tracked elements are array-index ordered, so push in reverse.
        for element in elements.iter().rev() {
            self.stack.push(element.clone());
        }
        let count_temp = self.next_temp();
        let count = elements.len() as i64;
        self.statements.push(format!(
            "let {count_temp} = len({value}); // unpack also pushes element count"
        ));
        self.literal_values
            .insert(count_temp.clone(), LiteralValue::Integer(count));
        self.stack.push(count_temp);
        true
    }

    fn emit_unknown_unpack(&mut self, instruction: &Instruction, value: String) {
        // Neo VM UNPACK pops a compound type, pushes each element, then pushes
        // the count. Infer the element count from the common DROP + stores
        // pattern after UNPACK.
        let element_count = self.infer_unpack_element_count(instruction);
        let elements_temp = self.next_temp();
        self.statements.push(format!(
            "let {elements_temp} = unpack({value}); // unknown unpack source"
        ));
        for index in 0..element_count {
            let element_temp = self.next_temp();
            self.statements.push(format!(
                "let {element_temp} = unpack_item({elements_temp}, {index}); // synthetic unpack element"
            ));
            self.stack.push(element_temp);
        }

        let count_temp = self.next_temp();
        self.statements.push(format!(
            "let {count_temp} = len({value}); // unpack also pushes element count"
        ));
        self.stack.push(count_temp);
    }

    /// Infer the actual element count for an UNPACK with unknown source by
    /// scanning forward in the instruction stream. The typical post-UNPACK
    /// pattern is DROP (count), then N single-pop instructions that consume
    /// the elements. If DUP preceded UNPACK, one pop consumes the original.
    fn infer_unpack_element_count(&self, instruction: &Instruction) -> usize {
        let Some(&unpack_index) = self.index_by_offset.get(&instruction.offset) else {
            return DEFAULT_UNKNOWN_UNPACK_COUNT;
        };

        let mut cursor = unpack_index + 1;
        if cursor >= self.program.len() || self.program[cursor].opcode != OpCode::Drop {
            return DEFAULT_UNKNOWN_UNPACK_COUNT;
        }
        cursor += 1;

        let mut pops = 0usize;
        while cursor < self.program.len() && Self::is_single_pop(self.program[cursor].opcode) {
            pops += 1;
            cursor += 1;
        }

        if pops == 0 {
            return DEFAULT_UNKNOWN_UNPACK_COUNT;
        }

        let has_dup_before =
            unpack_index > 0 && self.program[unpack_index - 1].opcode == OpCode::Dup;
        let count = if has_dup_before {
            pops.saturating_sub(1)
        } else {
            pops
        };

        if count == 0 {
            DEFAULT_UNKNOWN_UNPACK_COUNT
        } else {
            count
        }
    }

    fn is_single_pop(opcode: OpCode) -> bool {
        matches!(
            opcode,
            OpCode::Drop
                | OpCode::Stloc0
                | OpCode::Stloc1
                | OpCode::Stloc2
                | OpCode::Stloc3
                | OpCode::Stloc4
                | OpCode::Stloc5
                | OpCode::Stloc6
                | OpCode::Stloc
                | OpCode::Starg0
                | OpCode::Starg1
                | OpCode::Starg2
                | OpCode::Starg3
                | OpCode::Starg4
                | OpCode::Starg5
                | OpCode::Starg6
                | OpCode::Starg
                | OpCode::Stsfld0
                | OpCode::Stsfld1
                | OpCode::Stsfld2
                | OpCode::Stsfld3
                | OpCode::Stsfld4
                | OpCode::Stsfld5
                | OpCode::Stsfld6
                | OpCode::Stsfld
        )
    }
}