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::expressions::csharpize_expression;
use super::syntax::{find_matching_close_paren, split_top_level_args};

pub(super) struct HelperRule {
    pub(super) replacement: &'static str,
    pub(super) needle_len: usize,
    /// 0-based positions within the call's argument list that need to be
    /// wrapped in `(int)(...)` for the C# overload signature to match.
    pub(super) int_cast_args: &'static [usize],
}

pub(super) struct HelperRewrite {
    pub(super) body: String,
    pub(super) consumed: usize,
}

/// Recognise empty NEWMAP / NEWARRAY0 / NEWSTRUCT0 constructor shapes the lift
/// emits and rewrite to compilable C# forms.
pub(super) fn match_collection_constructor(rest: &str) -> Option<(&'static str, usize)> {
    const TABLE: &[(&str, &str)] = &[
        ("Map()", "new Map<object, object>()"),
        ("Struct()", "new Struct()"),
        ("[]", "new object[0]"),
    ];
    for (needle, replacement) in TABLE {
        if rest.starts_with(needle) {
            return Some((replacement, needle.len()));
        }
    }
    None
}

/// Recognise lifted unary helpers that don't map cleanly to a simple
/// `Method(arg)` rewrite and expand into inline C# expressions.
pub(super) fn match_unary_pattern(rest: &str) -> Option<HelperRewrite> {
    if let Some(rendered) = match_simple_unary(rest, "is_null(", |arg| format!("({arg} is null)")) {
        return Some(rendered);
    }
    if let Some(rendered) = match_simple_unary(rest, "new_buffer(", |arg| {
        format!("new byte[{}]", wrap_int_cast_unless_literal(arg))
    }) {
        return Some(rendered);
    }
    if let Some(rendered) = match_simple_unary(rest, "new_array(", |arg| {
        format!("new object[{}]", wrap_int_cast_unless_literal(arg))
    }) {
        return Some(rendered);
    }
    if let Some(rendered) = match_simple_unary(rest, "clear_items(", |arg| format!("{arg}.Clear()"))
    {
        return Some(rendered);
    }
    if let Some(rendered) = match_simple_unary(rest, "keys(", |arg| format!("{arg}.Keys")) {
        return Some(rendered);
    }
    if let Some(rendered) = match_simple_unary(rest, "values(", |arg| format!("{arg}.Values")) {
        return Some(rendered);
    }
    if let Some(rendered) =
        match_simple_unary(rest, "reverse_items(", |arg| format!("{arg}.Reverse()"))
    {
        return Some(rendered);
    }
    for (needle, method) in METHOD_CALL_TABLE {
        if let Some(rendered) = match_method_call(rest, needle, method) {
            return Some(rendered);
        }
    }
    for (needle, csharp_type) in CONVERT_TYPED_TABLE {
        if let Some(rendered) =
            match_simple_unary(rest, needle, |arg| format!("({csharp_type})({arg})"))
        {
            return Some(rendered);
        }
    }
    for (needle, csharp_type) in IS_TYPE_TYPED_TABLE {
        if let Some(rendered) =
            match_simple_unary(rest, needle, |arg| format!("({arg} is {csharp_type})"))
        {
            return Some(rendered);
        }
    }
    None
}

const METHOD_CALL_TABLE: &[(&str, &str)] = &[
    ("remove_item(", "Remove"),
    ("append(", "Add"),
    ("has_key(", "ContainsKey"),
];

const CONVERT_TYPED_TABLE: &[(&str, &str)] = &[
    ("convert_to_bool(", "bool"),
    ("convert_to_integer(", "BigInteger"),
    ("convert_to_bytestring(", "ByteString"),
    ("convert_to_buffer(", "byte[]"),
];

const IS_TYPE_TYPED_TABLE: &[(&str, &str)] = &[
    ("is_type_bool(", "bool"),
    ("is_type_integer(", "BigInteger"),
    ("is_type_bytestring(", "ByteString"),
    ("is_type_buffer(", "byte[]"),
];

fn wrap_int_cast_unless_literal(arg: &str) -> String {
    let trimmed = arg.trim();
    let is_decimal_literal = !trimmed.is_empty()
        && trimmed
            .strip_prefix('-')
            .unwrap_or(trimmed)
            .chars()
            .all(|ch| ch.is_ascii_digit());
    if is_decimal_literal {
        trimmed.to_string()
    } else {
        format!("(int)({trimmed})")
    }
}

fn match_simple_unary(
    rest: &str,
    needle: &str,
    render: impl FnOnce(&str) -> String,
) -> Option<HelperRewrite> {
    if !rest.starts_with(needle) {
        return None;
    }
    let after_open = &rest[needle.len()..];
    let close_index = find_matching_close_paren(after_open.as_bytes())?;
    let arg = after_open[..close_index].trim();
    Some(HelperRewrite {
        body: render(arg),
        consumed: needle.len() + close_index + 1,
    })
}

/// Rewrite a lifted helper call of shape `prefix(arg0, arg1, ...)` into a C#
/// method invocation `arg0.method_name(arg1, ...)`.
fn match_method_call(rest: &str, needle: &str, method_name: &str) -> Option<HelperRewrite> {
    if !rest.starts_with(needle) {
        return None;
    }
    let after_open = &rest[needle.len()..];
    let close_index = find_matching_close_paren(after_open.as_bytes())?;
    let args = &after_open[..close_index];
    let parts = split_top_level_args(args);
    if parts.is_empty() {
        return None;
    }
    let receiver = parts[0].trim();
    let rest_args = parts[1..]
        .iter()
        .map(|p| p.trim())
        .collect::<Vec<_>>()
        .join(", ");
    Some(HelperRewrite {
        body: format!("{receiver}.{method_name}({rest_args})"),
        consumed: needle.len() + close_index + 1,
    })
}

pub(super) fn match_numeric_helper(bytes: &[u8]) -> Option<HelperRule> {
    const TABLE: &[(&[u8], &str, &[usize])] = &[
        (b"abs(", "BigInteger.Abs", &[]),
        (b"min(", "BigInteger.Min", &[]),
        (b"max(", "BigInteger.Max", &[]),
        (b"pow(", "BigInteger.Pow", &[1]),
        (b"modpow(", "BigInteger.ModPow", &[]),
        (b"sign(", "Helper.Sign", &[]),
        (b"sqrt(", "Helper.Sqrt", &[]),
        (b"modmul(", "Helper.ModMul", &[]),
        (b"within(", "Helper.Within", &[]),
        (b"left(", "Helper.Left", &[1]),
        (b"right(", "Helper.Right", &[1]),
        (b"substr(", "Helper.Substr", &[1, 2]),
    ];
    for (needle, replacement, int_cast_args) in TABLE {
        if bytes.starts_with(needle) {
            return Some(HelperRule {
                replacement,
                needle_len: needle.len(),
                int_cast_args,
            });
        }
    }
    None
}

pub(super) fn format_helper_with_casts(rest: &str, rule: &HelperRule) -> Option<HelperRewrite> {
    let after_open = &rest[rule.needle_len..];
    let close_index = find_matching_close_paren(after_open.as_bytes())?;
    let args = &after_open[..close_index];
    let parts = split_top_level_args(args);
    let mut rendered = Vec::with_capacity(parts.len());
    for (index, part) in parts.iter().enumerate() {
        let normalized = csharpize_expression(part.trim());
        if rule.int_cast_args.contains(&index) {
            rendered.push(wrap_int_cast_unless_literal(&normalized));
        } else {
            rendered.push(normalized);
        }
    }
    let body = format!("{}({})", rule.replacement, rendered.join(", "));
    Some(HelperRewrite {
        consumed: rule.needle_len + close_index + 1,
        body,
    })
}