neo-decompiler 0.6.2

Minimal tooling for inspecting Neo N3 NEF bytecode
Documentation
use std::collections::HashSet;
use std::fmt::Write;

use crate::instruction::Instruction;
use crate::manifest::{ContractManifest, ManifestMethod};

use super::super::super::helpers::{
    find_manifest_entry_method, next_inferred_method_offset, offset_as_usize,
};
use super::super::helpers::{
    collect_csharp_parameters, escape_csharp_string, format_csharp_parameters,
    format_manifest_type_csharp, format_method_signature, sanitize_csharp_identifier,
};
use super::body;

pub(super) struct MethodsContext<'a> {
    pub(super) instructions: &'a [Instruction],
    pub(super) inferred_method_starts: &'a [usize],
    pub(super) body_context: body::LiftedBodyContext<'a>,
}

pub(super) fn write_manifest_methods(
    output: &mut String,
    manifest: &ContractManifest,
    context: &MethodsContext<'_>,
    warnings: &mut Vec<String>,
) {
    let entry_method = write_script_entry_if_needed(output, manifest, context, warnings);
    let entry_offset = context
        .instructions
        .first()
        .map(|ins| ins.offset)
        .unwrap_or(0);

    let mut used_signatures: HashSet<(String, String)> = HashSet::new();
    let mut sorted_methods: Vec<&ManifestMethod> = manifest.abi.methods.iter().collect();
    sorted_methods.sort_by_key(|m| m.offset.unwrap_or(i32::MAX));

    let (with_offsets, without_offsets): (Vec<_>, Vec<_>) =
        sorted_methods.into_iter().partition(|m| m.offset.is_some());

    for method in with_offsets.iter() {
        let start = offset_as_usize(method.offset).unwrap_or(0);
        let end = next_inferred_method_offset(context.inferred_method_starts, start)
            .or_else(|| context.instructions.last().map(|i| i.offset + 1))
            .unwrap_or(start);
        let slice: Vec<Instruction> = context
            .instructions
            .iter()
            .filter(|ins| ins.offset >= start && ins.offset < end)
            .cloned()
            .collect();

        let params = collect_csharp_parameters(&method.parameters);
        let signature_key = params
            .iter()
            .map(|param| param.ty.as_str())
            .collect::<Vec<_>>()
            .join(",");
        let param_signature = format_csharp_parameters(&params);
        let base_name = sanitize_csharp_identifier(&method.name);
        let method_name = make_unique_method_name(base_name, &signature_key, &mut used_signatures);
        let return_type = format_manifest_type_csharp(&method.return_type);
        let signature = format_method_signature(&method_name, &param_signature, &return_type);

        write_method_attributes(output, &method_name, &method.name, method.safe);
        writeln!(output, "        {signature}").unwrap();
        writeln!(output, "        {{").unwrap();

        if slice.is_empty() {
            writeln!(output, "            throw new NotImplementedException();").unwrap();
        } else {
            let labels: Vec<String> = params.iter().map(|p| p.name.clone()).collect();
            let is_void = method.return_type == "Void";
            body::write_lifted_body(
                output,
                &slice,
                Some(&labels),
                warnings,
                &context.body_context,
                is_void,
            );
        }

        writeln!(output, "        }}").unwrap();
        writeln!(output).unwrap();
    }

    for method in without_offsets {
        let params = collect_csharp_parameters(&method.parameters);
        let signature_key = params
            .iter()
            .map(|param| param.ty.as_str())
            .collect::<Vec<_>>()
            .join(",");
        let param_signature = format_csharp_parameters(&params);
        let base_name = sanitize_csharp_identifier(&method.name);
        let method_name = make_unique_method_name(base_name, &signature_key, &mut used_signatures);
        let return_type = format_manifest_type_csharp(&method.return_type);
        let signature = format_method_signature(&method_name, &param_signature, &return_type);

        write_method_attributes(output, &method_name, &method.name, method.safe);
        writeln!(output, "        {signature}").unwrap();
        writeln!(output, "        {{").unwrap();

        if entry_method
            .as_ref()
            .map(|(entry, _)| std::ptr::eq(*entry, method))
            .unwrap_or(false)
        {
            let end = next_inferred_method_offset(context.inferred_method_starts, entry_offset);
            let slice: Vec<Instruction> = match end {
                Some(end) => context
                    .instructions
                    .iter()
                    .filter(|ins| ins.offset >= entry_offset && ins.offset < end)
                    .cloned()
                    .collect(),
                None => context.instructions.to_vec(),
            };
            let slice = if slice.is_empty() {
                context.instructions.to_vec()
            } else {
                slice
            };
            let labels: Vec<String> = params.iter().map(|p| p.name.clone()).collect();
            let is_void = method.return_type == "Void";
            body::write_lifted_body(
                output,
                &slice,
                Some(&labels),
                warnings,
                &context.body_context,
                is_void,
            );
        } else {
            writeln!(output, "            throw new NotImplementedException();").unwrap();
        }

        writeln!(output, "        }}").unwrap();
        writeln!(output).unwrap();
    }
}

pub(super) fn write_inferred_methods(
    output: &mut String,
    context: &MethodsContext<'_>,
    manifest: Option<&ContractManifest>,
    warnings: &mut Vec<String>,
) {
    let entry_offset = context.instructions.first().map(|ins| ins.offset);
    let manifest_offsets: HashSet<usize> = manifest
        .map(|manifest| {
            manifest
                .abi
                .methods
                .iter()
                .filter_map(|method| offset_as_usize(method.offset))
                .collect()
        })
        .unwrap_or_default();

    for start in context.inferred_method_starts {
        if Some(*start) == entry_offset || manifest_offsets.contains(start) {
            continue;
        }

        let end = next_inferred_method_offset(context.inferred_method_starts, *start)
            .or_else(|| context.instructions.last().map(|ins| ins.offset + 1))
            .unwrap_or(*start);
        let slice: Vec<Instruction> = context
            .instructions
            .iter()
            .filter(|ins| ins.offset >= *start && ins.offset < end)
            .cloned()
            .collect();
        if slice.is_empty()
            || slice
                .iter()
                .all(|ins| ins.opcode == crate::instruction::OpCode::Nop)
        {
            continue;
        }

        let arg_count = context
            .body_context
            .method_arg_counts_by_offset
            .get(start)
            .copied()
            .unwrap_or(0);
        let params = (0..arg_count)
            .map(|index| format!("dynamic arg{index}"))
            .collect::<Vec<_>>()
            .join(", ");

        writeln!(output).unwrap();
        writeln!(
            output,
            "        private static dynamic sub_0x{start:04X}({params})"
        )
        .unwrap();
        writeln!(output, "        {{").unwrap();
        let labels = (0..arg_count)
            .map(|index| format!("arg{index}"))
            .collect::<Vec<_>>();
        body::write_lifted_body(
            output,
            &slice,
            (!labels.is_empty()).then_some(labels.as_slice()),
            warnings,
            &context.body_context,
            false,
        );
        writeln!(output, "        }}").unwrap();
        writeln!(output).unwrap();
    }
}

fn write_script_entry_if_needed<'a>(
    output: &mut String,
    manifest: &'a ContractManifest,
    context: &MethodsContext<'_>,
    warnings: &mut Vec<String>,
) -> Option<(&'a ManifestMethod, bool)> {
    let entry_offset = context.instructions.first().map(|ins| ins.offset)?;

    let entry_method = find_manifest_entry_method(manifest, entry_offset);
    if entry_method.is_some() {
        return entry_method;
    }

    let end = next_inferred_method_offset(context.inferred_method_starts, entry_offset);
    let slice: Vec<Instruction> = match end {
        Some(end) => context
            .instructions
            .iter()
            .filter(|ins| ins.offset >= entry_offset && ins.offset < end)
            .cloned()
            .collect(),
        None => context.instructions.to_vec(),
    };

    let slice = if slice.is_empty() {
        context.instructions.to_vec()
    } else {
        slice
    };

    writeln!(
        output,
        "        // warning: manifest entry offset did not match script entry at 0x{entry_offset:04X}; using synthetic ScriptEntry"
    )
    .unwrap();
    let entry_signature = format_method_signature("ScriptEntry", "", "void");
    writeln!(output, "        {entry_signature}").unwrap();
    writeln!(output, "        {{").unwrap();
    body::write_lifted_body(output, &slice, None, warnings, &context.body_context, true);
    writeln!(output, "        }}").unwrap();
    writeln!(output).unwrap();
    None
}

pub(super) fn write_fallback_entry(
    output: &mut String,
    context: &MethodsContext<'_>,
    warnings: &mut Vec<String>,
) {
    let entry_offset = context
        .instructions
        .first()
        .map(|ins| ins.offset)
        .unwrap_or(0);
    let end = next_inferred_method_offset(context.inferred_method_starts, entry_offset)
        .or_else(|| context.instructions.last().map(|i| i.offset + 1))
        .unwrap_or(entry_offset);
    let slice: Vec<Instruction> = context
        .instructions
        .iter()
        .filter(|ins| ins.offset >= entry_offset && ins.offset < end)
        .cloned()
        .collect();
    let slice = if slice.is_empty() {
        context.instructions.to_vec()
    } else {
        slice
    };

    let entry_method_name = "ScriptEntry".to_string();
    let entry_signature = format_method_signature(&entry_method_name, "", "void");
    writeln!(output, "        {entry_signature}").unwrap();
    writeln!(output, "        {{").unwrap();
    body::write_lifted_body(output, &slice, None, warnings, &context.body_context, true);
    writeln!(output, "        }}").unwrap();
}

fn write_method_attributes(output: &mut String, method_name: &str, raw_name: &str, is_safe: bool) {
    if method_name != raw_name {
        writeln!(
            output,
            "        [DisplayName(\"{}\")]",
            escape_csharp_string(raw_name)
        )
        .unwrap();
    }
    if is_safe {
        writeln!(output, "        [Safe]").unwrap();
    }
}

fn make_unique_method_name(
    base: String,
    signature: &str,
    used: &mut HashSet<(String, String)>,
) -> String {
    let mut candidate = base.clone();
    let mut index = 1usize;
    while !used.insert((candidate.clone(), signature.to_string())) {
        candidate = format!("{base}_{index}");
        index += 1;
    }
    candidate
}