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::*;
use crate::instruction::OpCode;

#[test]
fn high_level_view_renders_manifest_groups_block() {
    // `groups` (signed pubkey memberships authorising contract
    // updates) was dropped from the high-level summary. The C#
    // emitter has no idiomatic place to surface this metadata since
    // it's set at deployment time, not declared in source — but the
    // high-level summary is meant to be a complete inspection view of
    // the manifest, so it should show what the manifest contains.
    let nef_bytes = sample_nef();
    let manifest = ContractManifest::from_json_str(
        r#"
            {
                "name": "Demo",
                "groups": [
                    {"pubkey": "02f49ce0c33aabbccdd", "signature": "BAt..."},
                    {"pubkey": "02b00b1eaaaabbbbcccc", "signature": "BAd..."}
                ],
                "abi": {"methods": [], "events": []},
                "permissions": [],
                "trusts": "*",
                "extra": {}
            }
            "#,
    )
    .expect("manifest parsed");

    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, Some(manifest), OutputFormat::All)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("groups {"),
        "high-level should open a groups block:\n{high_level}"
    );
    assert!(high_level.contains("pubkey=02f49ce0c33aabbccdd"));
    assert!(high_level.contains("pubkey=02b00b1eaaaabbbbcccc"));
    // Signature is intentionally elided — opaque base64, no human value.
    assert!(!high_level.contains("BAt..."));
    assert!(!high_level.contains("signature="));

    // C# header should mirror the high-level rendering with a
    // `// groups:` comment block (parity with the existing
    // `// permissions:` and `// trusts:` blocks). The `groups` field
    // has no source-level attribute in Neo SmartContract Framework
    // (set at deployment, not declared in code), so a comment is the
    // right surface for completeness.
    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("// groups:"),
        "C# header should open a groups comment block:\n{csharp}"
    );
    assert!(csharp.contains("//   pubkey=02f49ce0c33aabbccdd"));
    assert!(csharp.contains("//   pubkey=02b00b1eaaaabbbbcccc"));
    // Same elision policy as high-level: signature is opaque.
    assert!(!csharp.contains("BAt..."));
    assert!(!csharp.contains("signature="));
}

#[test]
fn csharp_view_renders_non_string_scalar_extra_metadata() {
    // Manifests in the wild occasionally carry numeric or boolean
    // entries in `extra` (e.g. `"Version": 1`, `"Verified": true`).
    // The renderer used to gate on `value.as_str()` and silently drop
    // anything else, hiding real metadata. Now both renderers
    // stringify scalars via `render_extra_scalar`, so users see the
    // value verbatim.
    let nef_bytes = sample_nef();
    let manifest = ContractManifest::from_json_str(
        r#"
            {
                "name": "Demo",
                "abi": {"methods": [], "events": []},
                "permissions": [],
                "trusts": "*",
                "extra": {
                    "Author": "Anon",
                    "Version": 2,
                    "Verified": true,
                    "Notes": null
                }
            }
            "#,
    )
    .expect("manifest parsed");

    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, Some(manifest), OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(csharp.contains("[ManifestExtra(\"Author\", \"Anon\")]"));
    assert!(csharp.contains("[ManifestExtra(\"Version\", \"2\")]"));
    assert!(csharp.contains("[ManifestExtra(\"Verified\", \"true\")]"));
    // null has no canonical short form — entry is dropped, not rendered as "null".
    assert!(!csharp.contains("ManifestExtra(\"Notes\""));

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(high_level.contains("// Author: Anon"));
    assert!(high_level.contains("// Version: 2"));
    assert!(high_level.contains("// Verified: true"));
    assert!(!high_level.contains("// Notes:"));
}

#[test]
fn csharp_multi_entry_typed_trusts_render_as_block() {
    // Manifest with structured `trusts: {hashes:[...], groups:[...]}`
    // produces a typed list with 4 entries — too many for a single
    // line, so the C# header should break it into a `// trusts:`
    // block parallel to `// permissions:`.
    let nef_bytes = build_nef(&[OpCode::Ret.byte()]);
    let manifest = ContractManifest::from_json_str(
        r#"
            {
                "name": "MultiTrust",
                "abi": {
                    "methods": [
                        {
                            "name": "main",
                            "parameters": [],
                            "returntype": "Void",
                            "offset": 0
                        }
                    ],
                    "events": []
                },
                "permissions": [],
                "trusts": {
                    "hashes": ["0xabc", "0xdef"],
                    "groups": ["02foo", "02bar"]
                }
            }
            "#,
    )
    .expect("manifest parsed");

    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, Some(manifest), OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("// trusts:"),
        "multi-entry typed trusts should render as a block: {csharp}"
    );
    assert!(
        csharp.contains("//   hash:0xabc"),
        "hash entries should be indented under the trusts block: {csharp}"
    );
    assert!(
        csharp.contains("//   hash:0xdef"),
        "hash entries should be indented under the trusts block: {csharp}"
    );
    assert!(
        csharp.contains("//   group:02foo"),
        "group entries should be indented under the trusts block: {csharp}"
    );
    assert!(
        csharp.contains("//   group:02bar"),
        "group entries should be indented under the trusts block: {csharp}"
    );
    assert!(
        !csharp.contains("// trusts = [hash:0xabc, hash:0xdef, group:02foo, group:02bar]"),
        "multi-entry trusts must not stretch onto a single line: {csharp}"
    );
}

#[test]
fn header_surfaces_nef_compiler_and_source_fields() {
    // The NEF header carries `compiler` (always set in practice) and
    // `source` (often a repo URL or commit hash, sometimes empty).
    // Both fields are visible via `info` but were dropped from the
    // decompiled headers, leaving readers to run a separate command
    // to learn what produced the bytecode. Surface them as comment
    // lines under the script hash. Empty fields are silently
    // skipped (the test harness's `build_nef` writes `compiler =
    // "test"` and an empty source, so we exercise the present /
    // absent paths together).
    let nef_bytes = sample_nef();
    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("    // compiler: test"),
        "high-level should surface the NEF compiler field:\n{high_level}"
    );
    assert!(
        !high_level.contains("    // source:"),
        "empty source should not emit a placeholder line:\n{high_level}"
    );

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("        // compiler: test"),
        "C# header should surface the NEF compiler field at the C# indent:\n{csharp}"
    );
    assert!(
        !csharp.contains("// source:"),
        "empty source should not emit a placeholder line in C#:\n{csharp}"
    );
}

#[test]
fn csharp_header_renders_method_tokens_block() {
    // The high-level renderer surfaces `// method tokens declared in
    // NEF` so a reader can cross-reference each CALLT call against
    // its native contract / call flags. The C# header silently
    // dropped the table, leaving readers to scrape the NEF
    // separately. Render it as a comment block (parity with the
    // existing `// permissions:` / `// groups:` blocks) — Neo
    // SmartContract Framework has no source-level construct for
    // method tokens, so a comment is the correct surface.
    //
    // Hash chosen to match the StdLib native contract so the
    // renderer adds the friendly contract label.
    let stdlib_hash: [u8; 20] = [
        0xC0, 0xEF, 0x39, 0xCE, 0xE0, 0xE4, 0xE9, 0x25, 0xC6, 0xC2, 0xA0, 0x6A, 0x79, 0xE1, 0x44,
        0x0D, 0xD8, 0x6F, 0xCE, 0xAC,
    ];
    let nef_bytes = build_nef_with_single_token(
        &[OpCode::Ret.byte()],
        stdlib_hash,
        "Serialize",
        1,
        true,
        0x0F,
    );

    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("// method tokens declared in NEF:"),
        "C# header should open the method tokens comment block:\n{csharp}"
    );
    assert!(
        csharp.contains(
            "//   Serialize (StdLib::Serialize) hash=C0EF39CEE0E4E925C6C2A06A79E1440DD86FCEAC params=1 returns=true flags=0x0F"
        ),
        "method token line should match high-level layout (with native contract label):\n{csharp}"
    );
    assert!(
        csharp.contains("(ReadStates|WriteStates|AllowCall|AllowNotify)"),
        "call flags should be described:\n{csharp}"
    );
}

#[test]
fn csharp_header_omits_method_tokens_block_when_none() {
    // Empty token table => no header line at all (don't leave a
    // `// method tokens declared in NEF:` orphan).
    let nef_bytes = sample_nef();
    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        !csharp.contains("method tokens declared in NEF"),
        "no token block expected when NEF has no method tokens:\n{csharp}"
    );
}

#[test]
fn csharp_single_entry_typed_trusts_stay_on_one_line() {
    // Single-entry typed lists are short — keep them on one line so
    // the header doesn't grow unnecessarily for the common case.
    let nef_bytes = build_nef(&[OpCode::Ret.byte()]);
    let manifest = ContractManifest::from_json_str(
        r#"
            {
                "name": "SingleTrust",
                "abi": {
                    "methods": [
                        {
                            "name": "main",
                            "parameters": [],
                            "returntype": "Void",
                            "offset": 0
                        }
                    ],
                    "events": []
                },
                "permissions": [],
                "trusts": { "groups": ["02abcdef"] }
            }
            "#,
    )
    .expect("manifest parsed");

    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, Some(manifest), OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("// trusts = [group:02abcdef]"),
        "single-entry trusts should stay compact on one line: {csharp}"
    );
    assert!(
        !csharp.contains("// trusts:"),
        "single-entry trusts should not break into a block: {csharp}"
    );
}