use std::collections::HashSet;
use crate::manifest::ManifestParameter;
pub(super) use super::super::helpers::make_unique_identifier;
use super::super::helpers::sanitize_identifier;
#[derive(Clone)]
pub(super) struct CSharpParameter {
pub(super) name: String,
pub(super) ty: String,
}
pub(super) fn collect_csharp_parameters(parameters: &[ManifestParameter]) -> Vec<CSharpParameter> {
let mut used_names = HashSet::new();
parameters
.iter()
.map(|param| CSharpParameter {
name: make_unique_identifier(sanitize_csharp_identifier(¶m.name), &mut used_names),
ty: format_manifest_type_csharp(¶m.kind, false),
})
.collect()
}
pub(super) fn format_csharp_parameters(params: &[CSharpParameter]) -> String {
params
.iter()
.map(|param| format!("{} {}", param.ty, param.name))
.collect::<Vec<_>>()
.join(", ")
}
pub(super) fn format_manifest_type_csharp(kind: &str, for_return: bool) -> String {
match kind.to_ascii_lowercase().as_str() {
"void" if for_return => "void".into(),
"boolean" | "bool" => "bool".into(),
"integer" | "int" => "BigInteger".into(),
"string" => "string".into(),
"hash160" => "UInt160".into(),
"hash256" => "UInt256".into(),
"publickey" => "ECPoint".into(),
"bytearray" | "bytes" => "ByteString".into(),
"signature" => "ByteString".into(),
"array" => "object[]".into(),
"map" => "object".into(),
"interopinterface" => "object".into(),
"any" => "object".into(),
_ => "object".into(),
}
}
pub(super) fn format_method_signature(name: &str, parameters: &str, return_type: &str) -> String {
if parameters.is_empty() {
format!("public static {return_type} {name}()")
} else {
format!("public static {return_type} {name}({parameters})")
}
}
pub(in crate::decompiler) fn csharpize_statement(line: &str) -> String {
let trimmed = line.trim();
if trimmed.is_empty() {
return String::new();
}
if trimmed.starts_with("//") {
return trimmed.to_string();
}
if let Some(stripped) = trimmed.strip_prefix("let ") {
return format!("var {}", csharpize_expression(stripped));
}
if trimmed == "loop {" {
return "while (true) {".to_string();
}
if let Some(condition) = trimmed
.strip_prefix("if ")
.and_then(|rest| rest.strip_suffix(" {"))
{
return format!("if ({}) {{", csharpize_expression(condition.trim()));
}
if let Some(condition) = trimmed
.strip_prefix("else if ")
.and_then(|rest| rest.strip_suffix(" {"))
{
return format!("else if ({}) {{", csharpize_expression(condition.trim()));
}
if let Some(condition) = trimmed
.strip_prefix("} else if ")
.and_then(|rest| rest.strip_suffix(" {"))
{
return format!("}} else if ({}) {{", csharpize_expression(condition.trim()));
}
if let Some(condition) = trimmed
.strip_prefix("while ")
.and_then(|rest| rest.strip_suffix(" {"))
{
return format!("while ({}) {{", csharpize_expression(condition.trim()));
}
if trimmed.starts_with("for (") && trimmed.ends_with(" {") {
let inner = &trimmed[4..trimmed.len() - 2];
let inner = inner.strip_prefix('(').unwrap_or(inner);
let inner = inner.strip_suffix(')').unwrap_or(inner);
let converted = inner.replacen("let ", "var ", 1);
return format!("for ({}) {{", csharpize_expression(&converted));
}
if let Some(scrutinee) = trimmed
.strip_prefix("switch ")
.and_then(|rest| rest.strip_suffix(" {"))
{
return format!("switch ({}) {{", csharpize_expression(scrutinee.trim()));
}
if let Some(value) = trimmed
.strip_prefix("case ")
.and_then(|rest| rest.strip_suffix(" {"))
{
return format!("case {}: {{", value.trim());
}
if trimmed == "default {" {
return "default: {".to_string();
}
if let Some(target) = trimmed.strip_prefix("leave ") {
return format!("goto {target}");
}
if let Some(rest) = trimmed
.strip_prefix("throw(")
.and_then(|r| r.strip_suffix(");"))
{
let operand = csharpize_expression(rest);
return format!(
"throw new Exception({});",
wrap_exception_operand_for_csharp(&operand)
);
}
if trimmed == "abort();" {
return "throw new Exception();".to_string();
}
if let Some(rest) = trimmed
.strip_prefix("abort(")
.and_then(|r| r.strip_suffix(");"))
{
let operand = csharpize_expression(rest);
return format!(
"throw new Exception({});",
wrap_exception_operand_for_csharp(&operand)
);
}
if let Some(args) = trimmed
.strip_prefix("assert(")
.and_then(|r| r.strip_suffix(");"))
{
if let Some((cond, message)) = split_top_level_comma(args) {
let message_expr = csharpize_expression(message.trim());
return format!(
"if (!({})) throw new Exception({});",
csharpize_expression(cond.trim()),
wrap_exception_operand_for_csharp(&message_expr)
);
}
return format!(
"if (!({})) throw new Exception();",
csharpize_expression(args.trim())
);
}
csharpize_expression(trimmed)
}
fn wrap_exception_operand_for_csharp(operand: &str) -> String {
let trimmed = operand.trim();
if operand_appears_string_typed(trimmed) {
operand.to_string()
} else {
format!("$\"{{{trimmed}}}\"")
}
}
fn operand_appears_string_typed(text: &str) -> bool {
text.contains('"')
}
fn csharpize_expression(text: &str) -> String {
rewrite_numeric_helpers(&rewrite_cat_operator(text))
}
fn rewrite_numeric_helpers(line: &str) -> String {
let bytes = line.as_bytes();
let mut out = String::with_capacity(line.len());
let mut i = 0;
let mut in_string: Option<u8> = None;
while i < bytes.len() {
let b = bytes[i];
if !b.is_ascii() {
if line.is_char_boundary(i) {
let ch = line[i..].chars().next().unwrap_or('\u{FFFD}');
out.push(ch);
i += ch.len_utf8();
} else {
i += 1;
}
continue;
}
if let Some(quote) = in_string {
out.push(b as char);
if b == b'\\' && i + 1 < bytes.len() {
let esc = line[i + 1..].chars().next().unwrap_or('\u{FFFD}');
out.push(esc);
i += 1 + esc.len_utf8();
continue;
}
if b == quote {
in_string = None;
}
i += 1;
continue;
}
if b == b'"' || b == b'\'' {
in_string = Some(b);
out.push(b as char);
i += 1;
continue;
}
let prev_is_ident_continuation = i > 0 && {
let p = bytes[i - 1];
p.is_ascii_alphanumeric() || p == b'_'
};
if !prev_is_ident_continuation {
if let Some(rendered) = match_unary_pattern(&line[i..]) {
out.push_str(&rendered.body);
i += rendered.consumed;
continue;
}
if let Some((replacement, consumed)) = match_collection_constructor(&line[i..]) {
out.push_str(replacement);
i += consumed;
continue;
}
if let Some((rendered, consumed)) = match_map_literal(&line[i..]) {
out.push_str(&rendered);
i += consumed;
continue;
}
if let Some((rendered, consumed)) = match_big_byte_literal(&line[i..]) {
out.push_str(&rendered);
i += consumed;
continue;
}
if let Some((rendered, consumed)) = match_big_integer_literal(&line[i..]) {
out.push_str(&rendered);
i += consumed;
continue;
}
if let Some(rule) = match_numeric_helper(&bytes[i..]) {
if !rule.int_cast_args.is_empty() {
if let Some(rendered) = format_helper_with_casts(&line[i..], &rule) {
out.push_str(&rendered.body);
i += rendered.consumed;
continue;
}
}
out.push_str(rule.replacement);
out.push('(');
i += rule.needle_len;
continue;
}
}
out.push(b as char);
i += 1;
}
out
}
fn match_big_integer_literal(s: &str) -> Option<(String, usize)> {
let bytes = s.as_bytes();
if bytes.is_empty() || !bytes[0].is_ascii_digit() {
return None;
}
let mut j = 0;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
if j < bytes.len() {
let after = bytes[j];
if after == b'x'
|| after == b'X'
|| after.is_ascii_alphabetic()
|| after == b'_'
|| after == b'.'
{
return None;
}
}
let digits = &s[..j];
if !decimal_exceeds_u64(digits) {
return None;
}
Some((format!("BigInteger.Parse(\"{digits}\")"), j))
}
fn match_map_literal(rest: &str) -> Option<(String, usize)> {
let inner = rest.strip_prefix("Map(")?;
let close = matching_paren(inner)?;
let body = inner[..close].trim();
if body.is_empty() || body.contains("/*") {
return None;
}
let mut entries = Vec::new();
for entry in split_top_level_args(body) {
let (key, value) = split_top_level_colon(entry)?;
entries.push(format!(
"[{}] = {}",
csharpize_expression(key.trim()),
csharpize_expression(value.trim())
));
}
let consumed = "Map(".len() + close + 1;
Some((
format!("new Map<object, object> {{ {} }}", entries.join(", ")),
consumed,
))
}
fn matching_paren(s: &str) -> Option<usize> {
let bytes = s.as_bytes();
let mut depth = 0i32;
let mut in_string: Option<u8> = None;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if let Some(quote) = in_string {
if b == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if b == quote {
in_string = None;
}
i += 1;
continue;
}
match b {
b'"' | b'\'' => in_string = Some(b),
b')' if depth == 0 => return Some(i),
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
_ => {}
}
i += 1;
}
None
}
fn split_top_level_colon(entry: &str) -> Option<(&str, &str)> {
let bytes = entry.as_bytes();
let mut depth = 0i32;
let mut in_string: Option<u8> = None;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if let Some(quote) = in_string {
if b == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if b == quote {
in_string = None;
}
i += 1;
continue;
}
match b {
b'"' | b'\'' => in_string = Some(b),
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b':' if depth == 0 => return Some((&entry[..i], &entry[i + 1..])),
_ => {}
}
i += 1;
}
None
}
fn match_big_byte_literal(s: &str) -> Option<(String, usize)> {
let bytes = s.as_bytes();
if bytes.len() < 2 || bytes[0] != b'0' || !(bytes[1] == b'x' || bytes[1] == b'X') {
return None;
}
let mut j = 2;
while j < bytes.len() && bytes[j].is_ascii_hexdigit() {
j += 1;
}
let hex = &s[2..j];
if hex.len() <= 16 || hex.len() % 2 != 0 {
return None;
}
if j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
return None;
}
let mut rendered = String::from("new byte[] { ");
for (idx, pair) in hex.as_bytes().chunks_exact(2).enumerate() {
if idx > 0 {
rendered.push_str(", ");
}
rendered.push_str("0x");
rendered.push(pair[0] as char);
rendered.push(pair[1] as char);
}
rendered.push_str(" }");
Some((rendered, j))
}
fn decimal_exceeds_u64(digits: &str) -> bool {
const U64_MAX: &str = "18446744073709551615";
let trimmed = digits.trim_start_matches('0');
let significant = if trimmed.is_empty() { "0" } else { trimmed };
match significant.len().cmp(&U64_MAX.len()) {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Equal => significant > U64_MAX,
}
}
struct HelperRule {
replacement: &'static str,
needle_len: usize,
int_cast_args: &'static [usize],
}
fn match_collection_constructor(rest: &str) -> Option<(&'static str, usize)> {
const TABLE: &[(&str, &str)] = &[
("Map()", "new Map<object, object>()"),
("Struct()", "new Struct()"),
("[]", "new object[0]"),
];
for (needle, replacement) in TABLE {
if rest.starts_with(needle) {
return Some((replacement, needle.len()));
}
}
None
}
fn match_unary_pattern(rest: &str) -> Option<HelperRewrite> {
if let Some(rendered) = match_simple_unary(rest, "is_null(", |arg| format!("({arg} is null)")) {
return Some(rendered);
}
if let Some(rendered) = match_simple_unary(rest, "new_buffer(", |arg| {
format!("new byte[{}]", wrap_int_cast_unless_literal(arg))
}) {
return Some(rendered);
}
if let Some(rendered) = match_simple_unary(rest, "new_array(", |arg| {
format!("new object[{}]", wrap_int_cast_unless_literal(arg))
}) {
return Some(rendered);
}
if let Some(rendered) = match_simple_unary(rest, "clear_items(", |arg| format!("{arg}.Clear()"))
{
return Some(rendered);
}
if let Some(rendered) = match_simple_unary(rest, "keys(", |arg| format!("{arg}.Keys")) {
return Some(rendered);
}
if let Some(rendered) = match_simple_unary(rest, "values(", |arg| format!("{arg}.Values")) {
return Some(rendered);
}
if let Some(rendered) =
match_simple_unary(rest, "reverse_items(", |arg| format!("{arg}.Reverse()"))
{
return Some(rendered);
}
for (needle, method) in METHOD_CALL_TABLE {
if let Some(rendered) = match_method_call(rest, needle, method) {
return Some(rendered);
}
}
for (needle, csharp_type) in CONVERT_TYPED_TABLE {
if let Some(rendered) =
match_simple_unary(rest, needle, |arg| format!("({csharp_type})({arg})"))
{
return Some(rendered);
}
}
for (needle, csharp_type) in IS_TYPE_TYPED_TABLE {
if let Some(rendered) =
match_simple_unary(rest, needle, |arg| format!("({arg} is {csharp_type})"))
{
return Some(rendered);
}
}
None
}
const METHOD_CALL_TABLE: &[(&str, &str)] = &[
("remove_item(", "Remove"),
("append(", "Add"),
("has_key(", "ContainsKey"),
];
const CONVERT_TYPED_TABLE: &[(&str, &str)] = &[
("convert_to_bool(", "bool"),
("convert_to_integer(", "BigInteger"),
("convert_to_bytestring(", "ByteString"),
("convert_to_buffer(", "byte[]"),
];
const IS_TYPE_TYPED_TABLE: &[(&str, &str)] = &[
("is_type_bool(", "bool"),
("is_type_integer(", "BigInteger"),
("is_type_bytestring(", "ByteString"),
("is_type_buffer(", "byte[]"),
];
fn wrap_int_cast_unless_literal(arg: &str) -> String {
let trimmed = arg.trim();
let is_decimal_literal = !trimmed.is_empty()
&& trimmed
.strip_prefix('-')
.unwrap_or(trimmed)
.chars()
.all(|ch| ch.is_ascii_digit());
if is_decimal_literal {
trimmed.to_string()
} else {
format!("(int)({trimmed})")
}
}
fn match_simple_unary(
rest: &str,
needle: &str,
render: impl FnOnce(&str) -> String,
) -> Option<HelperRewrite> {
if !rest.starts_with(needle) {
return None;
}
let after_open = &rest[needle.len()..];
let close_index = find_matching_close_paren(after_open.as_bytes())?;
let arg = after_open[..close_index].trim();
Some(HelperRewrite {
body: render(arg),
consumed: needle.len() + close_index + 1,
})
}
fn match_method_call(rest: &str, needle: &str, method_name: &str) -> Option<HelperRewrite> {
if !rest.starts_with(needle) {
return None;
}
let after_open = &rest[needle.len()..];
let close_index = find_matching_close_paren(after_open.as_bytes())?;
let args = &after_open[..close_index];
let parts = split_top_level_args(args);
if parts.is_empty() {
return None;
}
let receiver = parts[0].trim();
let rest_args = parts[1..]
.iter()
.map(|p| p.trim())
.collect::<Vec<_>>()
.join(", ");
Some(HelperRewrite {
body: format!("{receiver}.{method_name}({rest_args})"),
consumed: needle.len() + close_index + 1,
})
}
fn match_numeric_helper(bytes: &[u8]) -> Option<HelperRule> {
const TABLE: &[(&[u8], &str, &[usize])] = &[
(b"abs(", "BigInteger.Abs", &[]),
(b"min(", "BigInteger.Min", &[]),
(b"max(", "BigInteger.Max", &[]),
(b"pow(", "BigInteger.Pow", &[1]),
(b"modpow(", "BigInteger.ModPow", &[]),
(b"sign(", "Helper.Sign", &[]),
(b"sqrt(", "Helper.Sqrt", &[]),
(b"modmul(", "Helper.ModMul", &[]),
(b"within(", "Helper.Within", &[]),
(b"left(", "Helper.Left", &[1]),
(b"right(", "Helper.Right", &[1]),
(b"substr(", "Helper.Substr", &[1, 2]),
];
for (needle, replacement, int_cast_args) in TABLE {
if bytes.starts_with(needle) {
return Some(HelperRule {
replacement,
needle_len: needle.len(),
int_cast_args,
});
}
}
None
}
struct HelperRewrite {
body: String,
consumed: usize,
}
fn format_helper_with_casts(rest: &str, rule: &HelperRule) -> Option<HelperRewrite> {
let after_open = &rest[rule.needle_len..];
let close_index = find_matching_close_paren(after_open.as_bytes())?;
let args = &after_open[..close_index];
let parts = split_top_level_args(args);
let mut rendered = Vec::with_capacity(parts.len());
for (index, part) in parts.iter().enumerate() {
let normalized = csharpize_expression(part.trim());
if rule.int_cast_args.contains(&index) {
rendered.push(wrap_int_cast_unless_literal(&normalized));
} else {
rendered.push(normalized);
}
}
let body = format!("{}({})", rule.replacement, rendered.join(", "));
Some(HelperRewrite {
consumed: rule.needle_len + close_index + 1,
body,
})
}
fn split_top_level_args(args: &str) -> Vec<&str> {
let bytes = args.as_bytes();
let mut parts = Vec::new();
let mut start = 0usize;
let mut depth = 0i32;
let mut in_string: Option<u8> = None;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if let Some(quote) = in_string {
if b == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if b == quote {
in_string = None;
}
i += 1;
continue;
}
match b {
b'"' | b'\'' => in_string = Some(b),
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b',' if depth == 0 => {
parts.push(&args[start..i]);
start = i + 1;
}
_ => {}
}
i += 1;
}
if !args.is_empty() {
parts.push(&args[start..]);
}
parts
}
fn find_matching_close_paren(bytes: &[u8]) -> Option<usize> {
let mut depth: i32 = 0;
let mut in_string: Option<u8> = None;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if let Some(quote) = in_string {
if b == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if b == quote {
in_string = None;
}
i += 1;
continue;
}
match b {
b'"' | b'\'' => in_string = Some(b),
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => {
if depth == 0 {
return Some(i);
}
depth -= 1;
}
_ => {}
}
i += 1;
}
None
}
fn split_top_level_comma(args: &str) -> Option<(&str, &str)> {
let bytes = args.as_bytes();
let mut depth = 0i32;
let mut in_string: Option<u8> = None;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if let Some(quote) = in_string {
if b == b'\\' && i + 1 < bytes.len() {
i += 2;
continue;
}
if b == quote {
in_string = None;
}
i += 1;
continue;
}
match b {
b'"' | b'\'' => in_string = Some(b),
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
b',' if depth == 0 => return Some((&args[..i], &args[i + 1..])),
_ => {}
}
i += 1;
}
None
}
fn rewrite_cat_operator(line: &str) -> String {
if !line.contains(" cat ") {
return line.to_string();
}
let bytes = line.as_bytes();
let mut out = String::with_capacity(line.len());
let mut i = 0;
let mut in_string: Option<u8> = None;
while i < bytes.len() {
let b = bytes[i];
if !b.is_ascii() {
if line.is_char_boundary(i) {
let ch = line[i..].chars().next().unwrap_or('\u{FFFD}');
out.push(ch);
i += ch.len_utf8();
} else {
i += 1;
}
continue;
}
if let Some(quote) = in_string {
out.push(b as char);
if b == b'\\' && i + 1 < bytes.len() {
let esc = line[i + 1..].chars().next().unwrap_or('\u{FFFD}');
out.push(esc);
i += 1 + esc.len_utf8();
continue;
}
if b == quote {
in_string = None;
}
i += 1;
continue;
}
if b == b'"' || b == b'\'' {
in_string = Some(b);
out.push(b as char);
i += 1;
continue;
}
if b == b' '
&& i + 4 < bytes.len()
&& &bytes[i..i + 5] == b" cat "
&& i > 0
{
out.push_str(" + ");
i += 5;
continue;
}
out.push(b as char);
i += 1;
}
out
}
pub(in crate::decompiler) fn line_is_csharp_terminator(line: &str) -> bool {
let trimmed = line.trim();
trimmed.starts_with("return ")
|| trimmed == "return;"
|| trimmed.starts_with("return;")
|| trimmed.starts_with("throw ")
|| trimmed == "throw;"
|| trimmed.starts_with("goto ")
|| trimmed == "break;"
|| trimmed == "continue;"
}
pub(super) fn sanitize_csharp_identifier(input: &str) -> String {
let ident = sanitize_identifier(input);
if is_csharp_keyword(&ident) {
format!("@{ident}")
} else {
ident
}
}
fn is_csharp_keyword(ident: &str) -> bool {
matches!(
ident,
"abstract"
| "as"
| "base"
| "bool"
| "break"
| "byte"
| "case"
| "catch"
| "char"
| "checked"
| "class"
| "const"
| "continue"
| "decimal"
| "default"
| "delegate"
| "do"
| "double"
| "else"
| "enum"
| "event"
| "explicit"
| "extern"
| "false"
| "finally"
| "fixed"
| "float"
| "for"
| "foreach"
| "goto"
| "if"
| "implicit"
| "in"
| "int"
| "interface"
| "internal"
| "is"
| "lock"
| "long"
| "namespace"
| "new"
| "null"
| "object"
| "operator"
| "out"
| "override"
| "params"
| "private"
| "protected"
| "public"
| "readonly"
| "ref"
| "return"
| "sbyte"
| "sealed"
| "short"
| "sizeof"
| "stackalloc"
| "static"
| "string"
| "struct"
| "switch"
| "this"
| "throw"
| "true"
| "try"
| "typeof"
| "uint"
| "ulong"
| "unchecked"
| "unsafe"
| "ushort"
| "using"
| "virtual"
| "void"
| "volatile"
| "while"
| "add"
| "alias"
| "ascending"
| "async"
| "await"
| "by"
| "descending"
| "dynamic"
| "equals"
| "from"
| "get"
| "global"
| "group"
| "init"
| "into"
| "join"
| "let"
| "nameof"
| "on"
| "orderby"
| "partial"
| "remove"
| "select"
| "set"
| "unmanaged"
| "value"
| "var"
| "when"
| "where"
| "with"
| "yield"
)
}
pub(super) fn escape_csharp_string(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
}