use super::*;
use crate::instruction::OpCode;
#[test]
fn disassemble_bytes_returns_instruction_stream_without_rendering() {
let nef_bytes = sample_nef();
let output = Decompiler::new()
.disassemble_bytes(&nef_bytes)
.expect("disassembly succeeds");
assert_eq!(output.instructions.len(), 4);
assert!(output.warnings.is_empty());
assert_eq!(output.instructions[0].offset, 0);
assert_eq!(output.instructions[1].offset, 1);
}
#[test]
fn decompile_end_to_end() {
let nef_bytes = sample_nef();
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
assert_eq!(decompilation.instructions.len(), 4);
assert!(decompilation
.pseudocode
.as_deref()
.expect("pseudocode output")
.contains("ADD"));
assert!(decompilation
.high_level
.as_deref()
.expect("high-level output")
.contains("contract NeoContract"));
assert!(decompilation
.high_level
.as_deref()
.expect("high-level output")
.contains("fn script_entry()"));
}
#[test]
fn decompile_with_manifest_produces_contract_name() {
let nef_bytes = sample_nef();
let manifest = sample_manifest();
let decompilation = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, Some(manifest), OutputFormat::All)
.expect("decompile succeeds with manifest");
assert!(decompilation
.high_level
.as_deref()
.expect("high-level output")
.contains("contract ExampleContract"));
assert!(decompilation
.high_level
.as_deref()
.expect("high-level output")
.contains("fn main() -> int {"));
}
#[test]
fn cfg_to_dot_includes_contract_name_and_script_hash_in_label() {
let nef_bytes = sample_nef();
let manifest = sample_manifest();
let with = Decompiler::new()
.decompile_bytes_with_manifest(&nef_bytes, Some(manifest), OutputFormat::Pseudocode)
.expect("decompile with manifest");
let dot_with = with.cfg_to_dot();
assert!(dot_with.starts_with("digraph CFG {\n label=\""));
assert!(dot_with.contains("ExampleContract"), "{dot_with}");
assert!(dot_with.contains("instr)"), "{dot_with}");
assert!(dot_with.contains("labelloc=\"t\";"), "{dot_with}");
let without = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile without manifest");
let dot_without = without.cfg_to_dot();
assert!(dot_without.contains("label=\""), "{dot_without}");
assert!(!dot_without.contains("ExampleContract"), "{dot_without}");
assert!(dot_without.contains("instr)"), "{dot_without}");
}
#[test]
fn decompile_lifts_unconditional_jumps_without_control_flow_warning() {
let nef_bytes = build_nef(&[
OpCode::Jmp.byte(),
0x02,
OpCode::Jmp_L.byte(),
0x05,
0x00,
0x00,
0x00,
OpCode::Ret.byte(),
]);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
!high_level.contains("control flow not yet lifted"),
"unconditional jumps should no longer emit control-flow-not-lifted warnings: {high_level}"
);
assert!(
!high_level.contains("label_0x0002:"),
"fallthrough JMP target should be removed as orphan: {high_level}"
);
assert!(
!high_level.contains("label_0x0007:"),
"fallthrough JMP_L target should be removed as orphan: {high_level}"
);
}
#[test]
fn decompile_manifestless_entry_surfaces_initslot_args() {
let script = [
OpCode::Initslot.byte(),
0x02,
0x01,
OpCode::Ldarg0.byte(),
OpCode::Stloc0.byte(),
OpCode::Ldloc0.byte(),
OpCode::Ret.byte(),
];
let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
high_level.contains("fn script_entry(arg0)"),
"manifest-less script_entry should expose INITSLOT-declared args: {high_level}"
);
assert!(
high_level.contains("let loc0 = arg0;"),
"body should reference the same arg label as the signature: {high_level}"
);
}
#[test]
fn decompile_known_syscall_drops_redundant_hash_comment_in_clean_mode() {
let script = [
OpCode::Syscall.byte(),
0xB7,
0xC3,
0x88,
0x03, OpCode::Stloc5.byte(),
OpCode::Ret.byte(),
];
let nef_bytes = build_nef(&script);
let trace = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds (trace)");
let trace_high = trace
.high_level
.as_deref()
.expect("high-level output (trace)");
assert!(
trace_high.contains("// 0x0388C3B7"),
"trace mode should keep the syscall hash comment: {trace_high}"
);
let clean = Decompiler::new()
.with_trace_comments(false)
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds (clean)");
let clean_high = clean
.high_level
.as_deref()
.expect("high-level output (clean)");
assert!(
!clean_high.contains("// 0x0388C3B7"),
"clean mode should drop the redundant syscall hash comment: {clean_high}"
);
assert!(
clean_high.contains("System.Runtime.GetTime"),
"syscall name must still appear in the call expression: {clean_high}"
);
}
#[test]
fn decompile_unknown_syscall_keeps_unknown_annotation() {
let script = [
OpCode::Syscall.byte(),
0xEF,
0xBE,
0xAD,
0xDE, OpCode::Stloc5.byte(),
OpCode::Ret.byte(),
];
let nef_bytes = build_nef(&script);
for emit_trace in [true, false] {
let decompilation = Decompiler::new()
.with_trace_comments(emit_trace)
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
high.contains("// warning: unknown syscall 0xDEADBEEF"),
"unknown-syscall annotation must always be emitted with the JS-style \
`// warning:` prefix (trace={emit_trace}): {high}"
);
assert!(
high.contains("0xDEADBEEF"),
"unknown-syscall hash must appear in the call expression itself: {high}"
);
assert!(
decompilation
.warnings
.iter()
.any(|w| w.contains("unknown syscall 0xDEADBEEF")),
"unknown-syscall warning must be surfaced via the warnings array (trace={emit_trace}): {:?}",
decompilation.warnings,
);
}
}
#[test]
fn decompile_tolerates_reserved_method_token_call_flag_bits() {
let nef_bytes = build_nef_with_single_token(
&[OpCode::CallT.byte(), 0x00, 0x00, OpCode::Ret.byte()], [0x11; 20],
"foo",
0,
false,
0x10,
);
let strict_err = NefParser::new()
.parse(&nef_bytes)
.expect_err("strict parser should reject reserved call flag bits");
assert!(matches!(
strict_err,
crate::error::Error::Nef(crate::error::NefError::CallFlagsInvalid { .. })
));
let decompilation = Decompiler::new()
.with_trace_comments(false)
.decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
.expect("decompiler should recover invalid method-token call flags");
assert_eq!(decompilation.nef.method_tokens[0].call_flags, 0x10);
assert!(
decompilation
.warnings
.iter()
.any(|warning| warning.contains("method token #0 call flags 0x10")),
"warning should identify the invalid token flags: {:?}",
decompilation.warnings
);
let high_level = decompilation.high_level.as_deref().expect("high-level");
assert!(
high_level.contains("flags=0x10 (Unsupported(0x10))"),
"high-level method-token header should keep raw invalid flags readable: {high_level}"
);
}
#[test]
fn decompile_lifts_endtry_transfers_without_control_flow_warning() {
let nef_bytes = build_nef(&[
OpCode::Endtry.byte(),
0x02,
OpCode::EndtryL.byte(),
0x05,
0x00,
0x00,
0x00,
OpCode::Ret.byte(),
]);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
!high_level.contains("control flow not yet lifted"),
"ENDTRY opcodes should no longer emit control-flow-not-lifted warnings: {high_level}"
);
assert!(
!high_level.contains("leave label_"),
"fallthrough ENDTRY transfers should be eliminated: {high_level}"
);
assert!(
!high_level.contains("label_0x0002:"),
"fallthrough ENDTRY target should be removed as orphan: {high_level}"
);
assert!(
!high_level.contains("label_0x0007:"),
"fallthrough ENDTRY_L target should be removed as orphan: {high_level}"
);
assert!(
high_level.contains("return;"),
"method body should still emit the trailing return: {high_level}"
);
}
#[test]
fn decompile_uses_label_style_for_unresolved_jump_targets() {
let nef_bytes = build_nef(&[OpCode::Jmp.byte(), 0x05, OpCode::Ret.byte()]);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
high_level.contains("goto label_0x0005;"),
"unresolved jump target should still use label-style transfer naming: {high_level}"
);
assert!(
!high_level.contains("goto_0x0005();"),
"legacy function-style jump placeholder should not be emitted: {high_level}"
);
}
#[test]
fn decompile_uses_label_style_for_unresolved_endtry_targets() {
let nef_bytes = build_nef(&[OpCode::Endtry.byte(), 0x05, OpCode::Ret.byte()]);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
high_level.contains("leave label_0x0005;"),
"unresolved endtry target should still use label-style transfer naming: {high_level}"
);
assert!(
!high_level.contains("leave_0x0005();"),
"legacy function-style endtry placeholder should not be emitted: {high_level}"
);
}
#[test]
fn decompile_nested_loop_in_if() {
let nef_bytes = build_nef(&[
OpCode::Push0.byte(), OpCode::Jmpifnot.byte(),
0x08, OpCode::Push0.byte(), OpCode::Jmpifnot.byte(),
0x05, OpCode::Nop.byte(), OpCode::Jmp.byte(),
0xFC, OpCode::Ret.byte(), ]);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
high_level.contains("if"),
"outer branch should produce an if block: {high_level}"
);
assert!(
high_level.contains("while"),
"inner loop should produce a while block: {high_level}"
);
assert!(
!high_level.contains("not yet translated"),
"nested loop-in-if should not emit not-yet-translated placeholders: {high_level}"
);
}
#[test]
fn decompile_try_in_loop() {
let nef_bytes = build_nef(&[
OpCode::Push0.byte(), OpCode::Jmpifnot.byte(),
0x0C, OpCode::Try.byte(),
0x07,
0x00, OpCode::Nop.byte(), OpCode::Endtry.byte(),
0x06, OpCode::Nop.byte(), OpCode::Endfinally.byte(), OpCode::Jmp.byte(),
0xF5, OpCode::Ret.byte(), ]);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
high_level.contains("try"),
"try block should be emitted inside the loop: {high_level}"
);
assert!(
high_level.contains("while") || high_level.contains("loop"),
"enclosing loop should be recognized: {high_level}"
);
}
#[test]
fn decompile_nested_if_else() {
let nef_bytes = build_nef(&[
OpCode::Push0.byte(), OpCode::Jmpifnot.byte(),
0x0A, OpCode::Push0.byte(), OpCode::Jmpifnot.byte(),
0x05, OpCode::Nop.byte(), OpCode::Jmp.byte(),
0x04, OpCode::Nop.byte(), OpCode::Nop.byte(), OpCode::Ret.byte(), ]);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
high_level.contains("if"),
"nested if structure should be present: {high_level}"
);
assert!(
high_level.contains("let t1"),
"inner variable should still be present after empty-if removal: {high_level}"
);
}
#[test]
fn decompile_all_comparison_jumps() {
let nef_bytes = build_nef(&[
OpCode::Push0.byte(),
OpCode::Push1.byte(),
OpCode::JmpEq.byte(),
0x01,
OpCode::Nop.byte(), OpCode::Push0.byte(),
OpCode::Push1.byte(),
OpCode::JmpNe.byte(),
0x01,
OpCode::Nop.byte(), OpCode::Push0.byte(),
OpCode::Push1.byte(),
OpCode::JmpGt.byte(),
0x01,
OpCode::Nop.byte(), OpCode::Push0.byte(),
OpCode::Push1.byte(),
OpCode::JmpGe.byte(),
0x01,
OpCode::Nop.byte(), OpCode::Push0.byte(),
OpCode::Push1.byte(),
OpCode::JmpLt.byte(),
0x01,
OpCode::Nop.byte(), OpCode::Push0.byte(),
OpCode::Push1.byte(),
OpCode::JmpLe.byte(),
0x01,
OpCode::Nop.byte(), OpCode::Ret.byte(), ]);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
for op in ["==", "!=", ">", ">=", "<", "<="] {
assert!(
high_level.contains(op),
"comparison operator {op} should appear in output: {high_level}"
);
}
assert!(
!high_level.contains("not yet translated"),
"comparison jumps should not emit not-yet-translated placeholders: {high_level}"
);
}
#[test]
fn packmap_pops_key_value_pairs_and_renders_entries() {
let nef_bytes = build_nef(&[
OpCode::Push4.byte(),
OpCode::Push3.byte(),
OpCode::Push2.byte(),
OpCode::Push1.byte(),
OpCode::Push2.byte(), OpCode::Packmap.byte(),
OpCode::Ret.byte(),
]);
let decompilation = Decompiler::new()
.with_inline_single_use_temps(true)
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
high_level.contains("Map(1: 2, 3: 4)"),
"PACKMAP should render key/value pairs in pop order: {high_level}"
);
}
#[test]
fn huge_pack_count_terminates_quickly() {
let mut script = vec![OpCode::Pushint64.byte()];
script.extend_from_slice(&i64::MAX.to_le_bytes());
script.extend_from_slice(&[OpCode::Pack.byte(), OpCode::Ret.byte()]); let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds despite the pathological count");
assert!(decompilation.high_level.is_some());
}
#[test]
fn huge_packmap_count_terminates_quickly() {
let mut script = vec![OpCode::Pushint64.byte()];
script.extend_from_slice(&i64::MAX.to_le_bytes());
script.extend_from_slice(&[OpCode::Packmap.byte(), OpCode::Ret.byte()]); let nef_bytes = build_nef(&script);
let decompilation = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds despite the pathological count");
assert!(decompilation.high_level.is_some());
}
#[test]
fn invalid_type_bytes_render_as_raw_hex() {
let nef_bytes = build_nef(&[
OpCode::Push1.byte(),
OpCode::NewarrayT.byte(),
0x99,
OpCode::Drop.byte(),
OpCode::Push1.byte(),
OpCode::Istype.byte(),
0x99,
OpCode::Ret.byte(),
]);
let decompilation = Decompiler::new()
.with_inline_single_use_temps(true)
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = decompilation
.high_level
.as_deref()
.expect("high-level output");
assert!(
high_level.contains("new_array_t(1, 0x99)"),
"NEWARRAY_T fallback should keep the raw type byte: {high_level}"
);
assert!(
high_level.contains("is_type(1, 0x99)"),
"ISTYPE fallback should keep the raw type byte: {high_level}"
);
}
#[test]
fn oversized_method_hits_high_level_lifting_cap() {
let mut script = Vec::new();
for _ in 0..20_000 {
script.extend_from_slice(&[OpCode::Push1.byte(), OpCode::Jmpif.byte(), 0x05]);
}
script.push(OpCode::Ret.byte()); let nef_bytes = build_nef(&script);
let result = Decompiler::new()
.decompile_bytes(&nef_bytes)
.expect("decompile succeeds");
let high_level = result.high_level.as_deref().expect("high-level output");
assert!(
high_level.contains("too large for high-level lifting"),
"oversized method should fall back to the cap note: {high_level}"
);
assert!(
result
.warnings
.iter()
.any(|warning| warning.contains("exceeds the high-level lifting limit")),
"should surface a skip warning: {:?}",
result.warnings
);
}