#![allow(clippy::unwrap_used)]
use std::fs;
use neo_decompiler::instruction::OpCode;
use neo_decompiler::{ContractManifest, Decompiler, OutputFormat};
fn repo_root() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn artifact(name: &str) -> (Vec<u8>, Option<String>) {
let root = repo_root();
let nef = fs::read(
root.join("TestingArtifacts")
.join(name)
.with_extension("nef"),
)
.unwrap();
let manifest = fs::read_to_string(
root.join("TestingArtifacts")
.join(name)
.with_extension("manifest.json"),
)
.ok();
(nef, manifest)
}
fn write_varint(buf: &mut Vec<u8>, value: u32) {
match value {
0x00..=0xFC => buf.push(value as u8),
0xFD..=0xFFFF => {
buf.push(0xFD);
buf.extend_from_slice(&(value as u16).to_le_bytes());
}
_ => {
buf.push(0xFE);
buf.extend_from_slice(&value.to_le_bytes());
}
}
}
fn build_nef(script: &[u8]) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(b"NEF3");
let mut compiler = [0u8; 64];
compiler[..4].copy_from_slice(b"test");
data.extend_from_slice(&compiler);
data.push(0); data.push(0); data.push(0); data.extend_from_slice(&0u16.to_le_bytes()); write_varint(&mut data, script.len() as u32);
data.extend_from_slice(script);
let checksum = neo_decompiler::nef::NefParser::calculate_checksum(&data);
data.extend_from_slice(&checksum.to_le_bytes());
data
}
fn decompile_csharp(nef: &[u8], manifest: Option<&str>, typed: bool) -> String {
let m = manifest.and_then(|s| ContractManifest::from_json_str(s).ok());
let decompiler = Decompiler::new().with_typed_declarations(typed);
let dec = decompiler
.decompile_bytes_with_manifest(nef, m, OutputFormat::CSharp)
.unwrap();
dec.csharp.unwrap_or_default()
}
fn decompile_high_level(nef: &[u8], manifest: Option<&str>, typed: bool) -> String {
let m = manifest.and_then(|s| ContractManifest::from_json_str(s).ok());
let decompiler = Decompiler::new().with_typed_declarations(typed);
let dec = decompiler
.decompile_bytes_with_manifest(nef, m, OutputFormat::HighLevel)
.unwrap();
dec.high_level.unwrap_or_default()
}
#[test]
fn typed_declarations_annotate_inferred_integer_locals() {
let (nef, manifest) = artifact("edgecases/LoopIf");
let untyped = decompile_csharp(&nef, manifest.as_deref(), false);
assert!(
!untyped.contains("BigInteger loc0"),
"typed-off output must not declare loc0 as BigInteger:\n{untyped}"
);
let typed = decompile_csharp(&nef, manifest.as_deref(), true);
assert!(
typed.contains("BigInteger loc0"),
"typed output should declare loc0 as BigInteger; got:\n{typed}"
);
}
#[test]
fn typed_declarations_annotate_high_level_inferred_integer_locals() {
let (nef, manifest) = artifact("edgecases/LoopIf");
let untyped = decompile_high_level(&nef, manifest.as_deref(), false);
assert!(
!untyped.contains("int loc0"),
"typed-off high-level output must not declare loc0 as int:\n{untyped}"
);
let typed = decompile_high_level(&nef, manifest.as_deref(), true);
assert!(
typed.contains("int loc0"),
"typed high-level output should declare loc0 as int; got:\n{typed}"
);
}
#[test]
fn typed_declarations_annotate_inferred_static_slots() {
let nef = build_nef(&[
OpCode::Initsslot.byte(),
0x01,
OpCode::Newmap.byte(),
OpCode::Stsfld0.byte(),
OpCode::Ret.byte(),
]);
let high_level_untyped = decompile_high_level(&nef, None, false);
assert!(
!high_level_untyped.contains("map static0"),
"typed-off high-level output must not declare static0 as map:\n{high_level_untyped}"
);
let high_level_typed = decompile_high_level(&nef, None, true);
assert!(
high_level_typed.contains("map static0 = Map();"),
"typed high-level output should declare static0 as map; got:\n{high_level_typed}"
);
let csharp_untyped = decompile_csharp(&nef, None, false);
assert!(
!csharp_untyped.contains("Map static0"),
"typed-off C# output must not declare static0 as Map:\n{csharp_untyped}"
);
let csharp_typed = decompile_csharp(&nef, None, true);
assert!(
csharp_typed.contains("Map static0 = new Map<object, object>();"),
"typed C# output should declare static0 as Map; got:\n{csharp_typed}"
);
}
#[test]
fn typed_declarations_annotate_csharp_for_loop_initializers() {
let nef = build_nef(&[
OpCode::Initslot.byte(),
0x01,
0x00,
OpCode::Push0.byte(),
OpCode::Stloc0.byte(),
OpCode::Ldloc0.byte(),
OpCode::Push3.byte(),
OpCode::Lt.byte(),
OpCode::Jmpifnot.byte(),
0x09,
OpCode::Nop.byte(),
OpCode::Ldloc0.byte(),
OpCode::Push1.byte(),
OpCode::Add.byte(),
OpCode::Stloc0.byte(),
OpCode::Jmp.byte(),
0xF6,
OpCode::Ret.byte(),
]);
let untyped = decompile_csharp(&nef, None, false);
assert!(
!untyped.contains("for (BigInteger loc0"),
"typed-off C# output must keep for-loop declarations untyped:\n{untyped}"
);
let typed = decompile_csharp(&nef, None, true);
assert!(
typed.contains("for (BigInteger loc0"),
"typed C# output should annotate for-loop loc0 declarations; got:\n{typed}"
);
}
#[test]
fn typed_declarations_annotate_high_level_inferred_arguments() {
let nef = build_nef(&[
OpCode::Initslot.byte(),
0x00,
0x01,
OpCode::Ldarg0.byte(),
OpCode::Push1.byte(),
OpCode::Add.byte(),
OpCode::Ret.byte(),
]);
let untyped = decompile_high_level(&nef, None, false);
assert!(
untyped.contains("fn script_entry(arg0)"),
"typed-off high-level output should preserve the historical untyped argument signature:\n{untyped}"
);
let typed = decompile_high_level(&nef, None, true);
assert!(
typed.contains("fn script_entry(arg0: int)"),
"typed high-level output should annotate inferred argument types; got:\n{typed}"
);
}
#[test]
fn typed_declarations_annotate_csharp_inferred_arguments() {
let nef = build_nef(&[
OpCode::Initslot.byte(),
0x00,
0x01,
OpCode::Ldarg0.byte(),
OpCode::Push1.byte(),
OpCode::Add.byte(),
OpCode::Ret.byte(),
]);
let untyped = decompile_csharp(&nef, None, false);
assert!(
untyped.contains("public static object ScriptEntry(object arg0)"),
"typed-off C# output should preserve the historical object argument signature:\n{untyped}"
);
let typed = decompile_csharp(&nef, None, true);
assert!(
typed.contains("public static object ScriptEntry(BigInteger arg0)"),
"typed C# output should annotate inferred argument types; got:\n{typed}"
);
}
#[test]
fn typed_declarations_backpropagate_argument_types_through_local_aliases() {
let nef = build_nef(&[
OpCode::Initslot.byte(),
0x01,
0x01,
OpCode::Ldarg0.byte(),
OpCode::Stloc0.byte(),
OpCode::Ldloc0.byte(),
OpCode::Push1.byte(),
OpCode::Add.byte(),
OpCode::Ret.byte(),
]);
let high_level = decompile_high_level(&nef, None, true);
assert!(
high_level.contains("fn script_entry(arg0: int)"),
"typed high-level output should backpropagate local alias constraints to arg0; got:\n{high_level}"
);
assert!(
high_level.contains("int loc0"),
"typed high-level output should still annotate the local alias; got:\n{high_level}"
);
let csharp = decompile_csharp(&nef, None, true);
assert!(
csharp.contains("public static object ScriptEntry(BigInteger arg0)"),
"typed C# output should backpropagate local alias constraints to arg0; got:\n{csharp}"
);
assert!(
csharp.contains("BigInteger loc0"),
"typed C# output should still annotate the local alias; got:\n{csharp}"
);
}
#[test]
fn typed_declarations_annotate_collection_helper_results() {
let nef = build_nef(&[
OpCode::Initslot.byte(),
0x02,
0x00,
OpCode::Newmap.byte(),
OpCode::Dup.byte(),
OpCode::Keys.byte(),
OpCode::Stloc0.byte(),
OpCode::Values.byte(),
OpCode::Stloc1.byte(),
OpCode::Ret.byte(),
]);
let high_level = decompile_high_level(&nef, None, true);
assert!(
high_level.contains("object[] loc0"),
"typed high-level output should annotate KEYS result as object[]; got:\n{high_level}"
);
assert!(
high_level.contains("object[] loc1"),
"typed high-level output should annotate VALUES result as object[]; got:\n{high_level}"
);
let csharp = decompile_csharp(&nef, None, true);
assert!(
csharp.contains("object[] loc0"),
"typed C# output should annotate KEYS result as object[]; got:\n{csharp}"
);
assert!(
csharp.contains("object[] loc1"),
"typed C# output should annotate VALUES result as object[]; got:\n{csharp}"
);
}
#[test]
fn typed_declarations_annotate_byte_slice_results() {
let nef = build_nef(&[
OpCode::Initslot.byte(),
0x03,
0x00,
OpCode::Pushdata1.byte(),
0x03,
b'a',
b'b',
b'c',
OpCode::Push0.byte(),
OpCode::Push1.byte(),
OpCode::Substr.byte(),
OpCode::Stloc0.byte(),
OpCode::Pushdata1.byte(),
0x03,
b'a',
b'b',
b'c',
OpCode::Push1.byte(),
OpCode::Left.byte(),
OpCode::Stloc1.byte(),
OpCode::Pushdata1.byte(),
0x03,
b'a',
b'b',
b'c',
OpCode::Push1.byte(),
OpCode::Right.byte(),
OpCode::Stloc2.byte(),
OpCode::Ret.byte(),
]);
let high_level = decompile_high_level(&nef, None, true);
assert!(
high_level.contains("byte[] loc0"),
"typed high-level output should annotate SUBSTR result as byte[]; got:\n{high_level}"
);
assert!(
high_level.contains("byte[] loc1"),
"typed high-level output should annotate LEFT result as byte[]; got:\n{high_level}"
);
assert!(
high_level.contains("byte[] loc2"),
"typed high-level output should annotate RIGHT result as byte[]; got:\n{high_level}"
);
let csharp = decompile_csharp(&nef, None, true);
assert!(
csharp.contains("ByteString loc0"),
"typed C# output should annotate SUBSTR result as ByteString; got:\n{csharp}"
);
assert!(
csharp.contains("ByteString loc1"),
"typed C# output should annotate LEFT result as ByteString; got:\n{csharp}"
);
assert!(
csharp.contains("ByteString loc2"),
"typed C# output should annotate RIGHT result as ByteString; got:\n{csharp}"
);
}
#[test]
fn typed_declarations_annotate_cat_arguments_and_result() {
let nef = build_nef(&[
OpCode::Initslot.byte(),
0x01,
0x02,
OpCode::Ldarg0.byte(),
OpCode::Ldarg1.byte(),
OpCode::Cat.byte(),
OpCode::Stloc0.byte(),
OpCode::Ret.byte(),
]);
let high_level = decompile_high_level(&nef, None, true);
assert!(
high_level.contains("fn script_entry(arg0: byte[], arg1: byte[])"),
"typed high-level output should annotate CAT argument constraints; got:\n{high_level}"
);
assert!(
high_level.contains("byte[] loc0"),
"typed high-level output should annotate CAT result as byte[]; got:\n{high_level}"
);
let csharp = decompile_csharp(&nef, None, true);
assert!(
csharp.contains("public static object ScriptEntry(ByteString arg0, ByteString arg1)"),
"typed C# output should annotate CAT argument constraints; got:\n{csharp}"
);
assert!(
csharp.contains("ByteString loc0"),
"typed C# output should annotate CAT result as ByteString; got:\n{csharp}"
);
}
#[test]
fn typed_declarations_produce_valid_empty_type_fallback() {
let (nef, manifest) = artifact("edgecases/LoopIf");
let typed = decompile_csharp(&nef, manifest.as_deref(), true);
assert!(
!typed.contains("\n loc0 =") && !typed.contains("var loc0"),
"typed output must not contain a malformed empty-type declaration"
);
for line in typed.lines() {
let t = line.trim();
if t.starts_with("t0 ") || t.starts_with("t0=") {
panic!("temp should not appear as a bare declaration: {t:?}");
}
}
}
#[test]
fn typed_declarations_off_matches_default() {
let (nef, manifest) = artifact("edgecases/LoopIf");
let parsed_manifest =
|| ContractManifest::from_json_str(manifest.as_deref().unwrap_or("")).ok();
let default_csharp = Decompiler::new()
.decompile_bytes_with_manifest(&nef, parsed_manifest(), OutputFormat::CSharp)
.unwrap()
.csharp
.unwrap_or_default();
let csharp_off = decompile_csharp(&nef, manifest.as_deref(), false);
assert_eq!(
default_csharp, csharp_off,
"with_typed_declarations(false) must equal default C# output"
);
let default_high_level = Decompiler::new()
.decompile_bytes_with_manifest(&nef, parsed_manifest(), OutputFormat::HighLevel)
.unwrap()
.high_level
.unwrap_or_default();
let high_level_off = decompile_high_level(&nef, manifest.as_deref(), false);
assert_eq!(
default_high_level, high_level_off,
"with_typed_declarations(false) must equal default high-level output"
);
}