Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 5 additions & 24 deletions src/Neo.CLI/CLI/MainService.Tools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -409,37 +409,18 @@ private static string Base64Fixed(string str)
/// <summary>
/// Base64 Smart Contract Script Analysis
/// input: DARkYXRhAgBlzR0MFPdcrAXPVptVduMEs2lf1jQjxKIKDBT3XKwFz1abVXbjBLNpX9Y0I8SiChTAHwwIdHJhbnNmZXIMFKNSbimM12LkFYX/8KGvm2ttFxulQWJ9W1I=
/// output:
/// PUSHDATA1 data
/// PUSHINT32 500000000
/// PUSHDATA1 0x0aa2c42334d65f69b304e376559b56cf05ac5cf7
/// PUSHDATA1 0x0aa2c42334d65f69b304e376559b56cf05ac5cf7
/// PUSH4
/// PACK
/// PUSH15
/// PUSHDATA1 transfer
/// PUSHDATA1 0xa51b176d6b9bafa1f0ff8515e462d78c296e52a3
/// SYSCALL System.Contract.Call
/// output (VMInstruction.ToString, with an L-prefix padded to at least 4 digits):
/// L0000:0000 PUSHDATA1 64617461 // data
/// L0001:0006 PUSHINT32 500000000
/// L0002:000B PUSHDATA1 0AA2C42334D65F69B304E376559B56CF05AC5CF7
/// </summary>
[ParseFunction("Base64 Smart Contract Script Analysis")]
private string? ScriptsToOpCode(string base64)
{
try
{
var bytes = Convert.FromBase64String(base64);
var sb = new StringBuilder();
var line = 0;

foreach (var instruct in new VMInstruction(bytes))
{
if (instruct.OperandSize == 0)
sb.AppendFormat("L{0:D04}:{1:X04} {2}{3}", line, instruct.Position, instruct.OpCode, Environment.NewLine);
else
sb.AppendFormat("L{0:D04}:{1:X04} {2,-10}{3}{4}", line, instruct.Position, instruct.OpCode, instruct.DecodeOperand(), Environment.NewLine);
line++;
}

return sb.ToString();
return VMInstruction.FormatListing(bytes);
}
catch
{
Expand Down
4 changes: 4 additions & 0 deletions src/Neo.CLI/Neo.CLI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
<ProjectReference Include="..\Neo.ConsoleService\Neo.ConsoleService.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Neo.CLI.Tests" />
</ItemGroup>

<ItemGroup>
<Content Include="neo.ico" />
</ItemGroup>
Expand Down
143 changes: 134 additions & 9 deletions src/Neo.CLI/Tools/VMInstruction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Neo.Cryptography.ECC;
using Neo.SmartContract;
using Neo.VM;
using System.Buffers.Binary;
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Unicode;

namespace Neo.CLI;

Expand Down Expand Up @@ -97,11 +100,43 @@ IEnumerator IEnumerable.GetEnumerator() =>

public override string ToString()
{
var posWidth = HexWidth(_script.Length);
if (OperandSize == 0)
return string.Format($"{{0:X{posWidth}}} {{1}}", Position, OpCode);
return string.Format($"{{0:X{posWidth}}} {{1,-10}}{{2}}", Position, OpCode, DecodeOperand());
}

/// <summary>
/// Lists instructions as <c>L0000:0000 NOP</c>. The L-prefix width is at least 4 digits
/// and grows with the script size / instruction count.
/// </summary>
internal static string FormatListing(ReadOnlyMemory<byte> script)
{
var rows = new List<VMInstruction>();
foreach (var instruct in new VMInstruction(script))
rows.Add(instruct);

var lastIndex = Math.Max(0, rows.Count - 1);
var width = DecimalWidth(Math.Max(lastIndex, script.Length));
var sb = new StringBuilder();
sb.AppendFormat("{0:X04} {1,-10}{2}", Position, OpCode, DecodeOperand());
for (var i = 0; i < rows.Count; i++)
{
sb.Append('L');
sb.Append(i.ToString($"D{width}", CultureInfo.InvariantCulture));
sb.Append(':');
sb.Append(rows[i]);
sb.AppendLine();
}

return sb.ToString();
}

internal static int DecimalWidth(int value)
=> Math.Max(4, Math.Max(0, value).ToString(CultureInfo.InvariantCulture).Length);

internal static int HexWidth(int length)
=> Math.Max(4, Math.Max(0, length == 0 ? 0 : length - 1).ToString("X").Length);

public T AsToken<T>(uint index = 0)
where T : unmanaged
{
Expand All @@ -119,8 +154,6 @@ public T AsToken<T>(uint index = 0)
public string DecodeOperand()
{
var operand = Operand[OperandPrefixSize..].ToArray();
var asStr = Encoding.UTF8.GetString(operand);
var readable = asStr.All(char.IsAsciiLetterOrDigit);

return OpCode switch
{
Expand Down Expand Up @@ -157,17 +190,109 @@ OpCode.STSFLD or
OpCode.LDARG or
OpCode.STARG or
OpCode.INITSSLOT => $"{AsToken<byte>()}",
OpCode.PUSHINT8 => $"{AsToken<sbyte>()}",
OpCode.PUSHINT16 => $"{AsToken<short>()}",
OpCode.PUSHINT32 => $"{AsToken<int>()}",
OpCode.PUSHINT64 => $"{AsToken<long>()}",
OpCode.PUSHINT8 => FormatInteger(AsToken<sbyte>()),
OpCode.PUSHINT16 => FormatInteger(AsToken<short>()),
OpCode.PUSHINT32 => FormatInteger(AsToken<int>()),
OpCode.PUSHINT64 => FormatInteger(AsToken<long>()),
OpCode.PUSHINT128 or
OpCode.PUSHINT256 => $"{new BigInteger(operand)}",
OpCode.SYSCALL => $"[{ApplicationEngine.Services[Unsafe.As<byte, uint>(ref operand[0])].Name}]",
OpCode.PUSHDATA1 or
OpCode.PUSHDATA2 or
OpCode.PUSHDATA4 => readable ? $"{Convert.ToHexString(operand)} // {asStr}" : Convert.ToHexString(operand),
_ => readable ? $"\"{asStr}\"" : $"{Convert.ToHexString(operand)}",
OpCode.PUSHDATA4 => FormatPushData(operand),
_ => TryGetReadableText(operand, out var text)
? $"\"{text}\""
: $"{Convert.ToHexString(operand)} // blob {operand.Length} bytes",
};
}

private static string FormatPushData(byte[] operand)
{
var hex = Convert.ToHexString(operand);
if (TryGetReadableText(operand, out var text))
return $"{hex} // {text}";

if (TryDecodeEcPoint(operand, out var point))
return $"{hex} // {point}";

if (operand.Length == UInt160.Length)
return $"{hex} // {new UInt160(operand)}";

if (operand.Length == UInt256.Length)
return $"{hex} // {new UInt256(operand)}";

return $"{hex} // blob {operand.Length} bytes";
}

private static string FormatInteger(long value)
=> value.ToString(CultureInfo.InvariantCulture);

/// <summary>
/// Strict UTF-8 with at least one non-control rune. Control runes are escaped
/// (<c>\n</c>, <c>\r</c>, <c>\t</c>, <c>\xNN</c>) so they are not written as raw output.
/// Invalid sequences are rejected; <see cref="Encoding.UTF8"/> replacement is not used.
/// </summary>
private static bool TryGetReadableText(ReadOnlySpan<byte> utf8, out string text)
{
text = null!;
if (utf8.IsEmpty || !Utf8.IsValid(utf8))
return false;

var decoded = Encoding.UTF8.GetString(utf8);
var hasGraphic = false;
var sb = new StringBuilder(decoded.Length);
foreach (var rune in decoded.EnumerateRunes())
{
if (Rune.IsControl(rune) || rune.Value == 0x7F)
sb.Append(EscapeControlRune(rune));
else
{
hasGraphic = true;
sb.Append(rune);
}
}

if (!hasGraphic)
return false;

text = sb.ToString();
return true;
}

private static string EscapeControlRune(Rune rune)
=> rune.Value switch
{
'\n' => "\\n",
'\r' => "\\r",
'\t' => "\\t",
'\0' => "\\0",
_ => rune.Value <= 0xFF ? $"\\x{rune.Value:X2}" : $"\\u{rune.Value:X4}",
};

private static bool TryDecodeEcPoint(byte[] operand, out ECPoint point)
{
point = null!;
if (operand.Length is not (33 or 65))
return false;
if (operand[0] is not (0x02 or 0x03 or 0x04))
return false;
try
{
point = ECPoint.DecodePoint(operand, ECCurve.Secp256r1);
return true;
}
catch (FormatException)
{
try
{
point = ECPoint.DecodePoint(operand, ECCurve.Secp256k1);
return true;
}
catch (FormatException)
{
return false;
}
}
}

}
Loading
Loading