#![cfg(feature = "web")]
use std::fs;
use std::path::PathBuf;
use neo_decompiler::instruction::OpCode;
use serde_json::Value;
const GAS_TOKEN_HASH: [u8; 20] = [
0xCF, 0x76, 0xE2, 0x8B, 0xD0, 0x06, 0x2C, 0x4A, 0x47, 0x8E, 0xE3, 0x55, 0x61, 0x01, 0x13, 0x19,
0xF3, 0xCF, 0xA4, 0xD2,
];
const SAMPLE_MANIFEST: &str = r#"
{
"name": "SampleToken",
"groups": [],
"supportedstandards": ["NEP-17"],
"features": {},
"abi": {
"methods": [
{
"name": "symbol",
"parameters": [],
"returntype": "String",
"offset": 0,
"safe": true
}
],
"events": []
},
"permissions": [],
"trusts": [],
"extra": {}
}
"#;
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_sample_nef() -> Vec<u8> {
let script = [
OpCode::Push0.byte(),
OpCode::Push1.byte(),
OpCode::Add.byte(),
OpCode::Ret.byte(),
];
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(1);
data.extend_from_slice(&GAS_TOKEN_HASH);
write_varint(&mut data, 8);
data.extend_from_slice(b"Transfer");
data.extend_from_slice(&2u16.to_le_bytes());
data.push(1);
data.push(0x0F);
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 build_nef_with_unknown_opcode() -> Vec<u8> {
let script = [0xFF, OpCode::Ret.byte()];
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 build_nef_with_invalid_call_flags() -> Vec<u8> {
let script = [OpCode::CallT.byte(), 0x00, 0x00, OpCode::Ret.byte()]; 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(1);
data.extend_from_slice(&GAS_TOKEN_HASH);
write_varint(&mut data, 8);
data.extend_from_slice(b"Transfer");
data.extend_from_slice(&2u16.to_le_bytes());
data.push(1);
data.push(0x10);
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 repo_root() -> PathBuf {
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"),
)
.expect("nef artifact");
let manifest = fs::read_to_string(
root.join("TestingArtifacts")
.join(name)
.with_extension("manifest.json"),
)
.ok();
(nef, manifest)
}
#[test]
fn web_info_report_exposes_hashes_and_manifest_summary() {
let report = neo_decompiler::web::info_report(&build_sample_nef(), Some(SAMPLE_MANIFEST))
.expect("info report");
let value = serde_json::to_value(&report).expect("serializable");
assert_eq!(value["compiler"], Value::String("test".to_string()));
assert_eq!(value["script_length"], Value::from(4));
assert_eq!(
value["manifest"]["name"],
Value::String("SampleToken".to_string())
);
assert_eq!(
value["method_tokens"][0]["method"],
Value::String("Transfer".to_string())
);
}
#[test]
fn web_info_report_surfaces_manifest_extra_metadata() {
let manifest_with_extra = r#"
{
"name": "Sample",
"groups": [],
"supportedstandards": [],
"features": {},
"abi": { "methods": [], "events": [] },
"permissions": [{ "contract": "*", "methods": "*" }],
"trusts": "*",
"extra": { "Author": "Edge Case", "Email": "edge@example.com" }
}
"#;
let report = neo_decompiler::web::info_report(&build_sample_nef(), Some(manifest_with_extra))
.expect("info report");
let value = serde_json::to_value(&report).expect("serializable");
assert_eq!(
value["manifest"]["extra"]["Author"],
Value::String("Edge Case".to_string())
);
assert_eq!(
value["manifest"]["extra"]["Email"],
Value::String("edge@example.com".to_string())
);
}
#[test]
fn web_info_report_tolerates_invalid_method_token_call_flags() {
let nef = build_nef_with_invalid_call_flags();
assert!(
neo_decompiler::nef::NefParser::new().parse(&nef).is_err(),
"strict parser should continue rejecting reserved call flag bits"
);
let report = neo_decompiler::web::info_report(&nef, None).expect("info report");
let value = serde_json::to_value(&report).expect("serializable");
assert_eq!(value["method_tokens"][0]["call_flags"], Value::from(0x10));
assert_eq!(
value["method_tokens"][0]["call_flags_description"],
Value::String("Unsupported(0x10)".to_string())
);
assert!(
value["warnings"]
.as_array()
.expect("warnings")
.iter()
.any(|warning| warning
.as_str()
.is_some_and(|text| text.contains("method token #0 call flags 0x10"))),
"warning should identify invalid method-token call flags: {value}",
);
}
#[test]
fn web_disasm_report_surfaces_unknown_opcode_warnings() {
let report = neo_decompiler::web::disasm_report(
&build_nef_with_unknown_opcode(),
neo_decompiler::web::WebDisasmOptions {
fail_on_unknown_opcodes: false,
},
)
.expect("disasm report");
let value = serde_json::to_value(&report).expect("serializable");
assert_eq!(
value["instructions"][0]["opcode"],
Value::String("UNKNOWN".to_string())
);
assert!(!value["warnings"].as_array().expect("warnings").is_empty());
assert!(
value["script_hash_le"]
.as_str()
.expect("script_hash_le")
.chars()
.all(|c| c.is_ascii_hexdigit() && (c.is_ascii_digit() || c.is_ascii_uppercase())),
"script_hash_le should be uppercase hex: {value}",
);
assert_eq!(
value["script_hash_le"]
.as_str()
.expect("script_hash_le")
.len(),
40,
);
}
#[test]
fn web_disasm_report_tolerates_invalid_method_token_call_flags() {
let report = neo_decompiler::web::disasm_report(
&build_nef_with_invalid_call_flags(),
neo_decompiler::web::WebDisasmOptions {
fail_on_unknown_opcodes: false,
},
)
.expect("disasm report");
let value = serde_json::to_value(&report).expect("serializable");
assert_eq!(
value["instructions"][0]["opcode"],
Value::String("CALLT".to_string())
);
assert!(
value["warnings"]
.as_array()
.expect("warnings")
.iter()
.any(|warning| warning
.as_str()
.is_some_and(|text| text.contains("method token #0 call flags 0x10"))),
"warning should identify invalid method-token call flags: {value}",
);
}
#[test]
fn web_decompile_report_exposes_high_level_and_csharp_outputs() {
let report = neo_decompiler::web::decompile_report(
&build_sample_nef(),
neo_decompiler::web::WebDecompileOptions {
manifest_json: Some(SAMPLE_MANIFEST.to_string()),
..Default::default()
},
)
.expect("decompile report");
let value = serde_json::to_value(&report).expect("serializable");
let high_level = value["high_level"].as_str().expect("high level");
assert!(high_level.contains("contract"));
assert!(
!high_level
.lines()
.any(|line| line.trim_start().starts_with("// 0000:")),
"default web decompile_report should not include trace comments:\n{high_level}",
);
assert!(value["csharp"]
.as_str()
.expect("csharp")
.contains("public class"));
assert!(value["analysis"]["call_graph"]["methods"].is_array());
assert_eq!(
value["compiler"].as_str().expect("compiler"),
"test",
"WebDecompileReport should surface the NEF compiler field at the top level",
);
assert!(
value["source"].is_null(),
"empty NEF source should serialize as null, not an empty string: {value}",
);
}
#[test]
fn web_decompile_report_emit_trace_comments_re_enables_per_instruction_comments() {
let report = neo_decompiler::web::decompile_report(
&build_sample_nef(),
neo_decompiler::web::WebDecompileOptions {
manifest_json: Some(SAMPLE_MANIFEST.to_string()),
emit_trace_comments: true,
..Default::default()
},
)
.expect("decompile report");
let value = serde_json::to_value(&report).expect("serializable");
let high_level = value["high_level"].as_str().expect("high level");
assert!(
high_level.contains("// 0000:"),
"emit_trace_comments=true should re-introduce trace comments:\n{high_level}",
);
}
#[test]
fn web_decompile_report_typed_declarations_annotate_slots_when_enabled() {
let (nef, manifest) = artifact("edgecases/LoopIf");
let report = neo_decompiler::web::decompile_report(
&nef,
neo_decompiler::web::WebDecompileOptions {
manifest_json: manifest,
typed_declarations: true,
..Default::default()
},
)
.expect("decompile report");
let value = serde_json::to_value(&report).expect("serializable");
let high_level = value["high_level"].as_str().expect("high level");
assert!(
high_level.contains("int loc0"),
"typed_declarations=true should annotate high-level locals:\n{high_level}",
);
let csharp = value["csharp"].as_str().expect("csharp");
assert!(
csharp.contains("BigInteger loc0"),
"typed_declarations=true should annotate C# locals:\n{csharp}",
);
}