From 67e49755222f345eca49d357c9819d6f16f8bc50 Mon Sep 17 00:00:00 2001
From: NGD Admin <154295625+NGDAdmin@users.noreply.github.com>
Date: Thu, 22 May 2025 14:05:35 +0800
Subject: [PATCH 001/158] [`ut`] 100% Coverage Trie.Get (#3952) (#3957)
* 100% Coverage Trie.Get
* fix ut
Co-authored-by: Shargon
---
.../Cryptography/MPTTrie/Trie.Delete.cs | 24 +--
.../Cryptography/MPTTrie/Trie.Find.cs | 6 +-
.../Cryptography/MPTTrie/Trie.Get.cs | 6 +-
.../Cryptography/MPTTrie/Trie.Proof.cs | 4 +-
.../Cryptography/MPTTrie/Trie.Put.cs | 30 ++--
.../Cryptography/MPTTrie/Trie.cs | 24 +--
.../Cryptography/MPTTrie/UT_Trie.cs | 163 ++++++++++--------
7 files changed, 136 insertions(+), 121 deletions(-)
diff --git a/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Delete.cs b/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Delete.cs
index 97d2b78b01..15f06ed8fa 100644
--- a/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Delete.cs
+++ b/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Delete.cs
@@ -23,7 +23,7 @@ public bool Delete(byte[] key)
throw new ArgumentException("could not be empty", nameof(key));
if (path.Length > Node.MaxKeyLength)
throw new ArgumentException("exceeds limit", nameof(key));
- return TryDelete(ref root, path);
+ return TryDelete(ref _root, path);
}
private bool TryDelete(ref Node node, ReadOnlySpan path)
@@ -34,7 +34,7 @@ private bool TryDelete(ref Node node, ReadOnlySpan path)
{
if (path.IsEmpty)
{
- if (!full) cache.DeleteNode(node.Hash);
+ if (!_full) _cache.DeleteNode(node.Hash);
node = new Node();
return true;
}
@@ -47,7 +47,7 @@ private bool TryDelete(ref Node node, ReadOnlySpan path)
var oldHash = node.Hash;
var result = TryDelete(ref node.Next, path[node.Key.Length..]);
if (!result) return false;
- if (!full) cache.DeleteNode(oldHash);
+ if (!_full) _cache.DeleteNode(oldHash);
if (node.Next.IsEmpty)
{
node = node.Next;
@@ -55,12 +55,12 @@ private bool TryDelete(ref Node node, ReadOnlySpan path)
}
if (node.Next.Type == NodeType.ExtensionNode)
{
- if (!full) cache.DeleteNode(node.Next.Hash);
+ if (!_full) _cache.DeleteNode(node.Next.Hash);
node.Key = new([.. node.Key.Span, .. node.Next.Key.Span]);
node.Next = node.Next.Next;
}
node.SetDirty();
- cache.PutNode(node);
+ _cache.PutNode(node);
return true;
}
return false;
@@ -78,7 +78,7 @@ private bool TryDelete(ref Node node, ReadOnlySpan path)
result = TryDelete(ref node.Children[path[0]], path[1..]);
}
if (!result) return false;
- if (!full) cache.DeleteNode(oldHash);
+ if (!_full) _cache.DeleteNode(oldHash);
List childrenIndexes = new List(Node.BranchChildCount);
for (int i = 0; i < Node.BranchChildCount; i++)
{
@@ -88,7 +88,7 @@ private bool TryDelete(ref Node node, ReadOnlySpan path)
if (childrenIndexes.Count > 1)
{
node.SetDirty();
- cache.PutNode(node);
+ _cache.PutNode(node);
return true;
}
var lastChildIndex = childrenIndexes[0];
@@ -100,20 +100,20 @@ private bool TryDelete(ref Node node, ReadOnlySpan path)
}
if (lastChild.Type == NodeType.HashNode)
{
- lastChild = cache.Resolve(lastChild.Hash);
+ lastChild = _cache.Resolve(lastChild.Hash);
if (lastChild is null) throw new InvalidOperationException("Internal error, can't resolve hash");
}
if (lastChild.Type == NodeType.ExtensionNode)
{
- if (!full) cache.DeleteNode(lastChild.Hash);
+ if (!_full) _cache.DeleteNode(lastChild.Hash);
lastChild.Key = new([.. childrenIndexes.ToArray(), .. lastChild.Key.Span]);
lastChild.SetDirty();
- cache.PutNode(lastChild);
+ _cache.PutNode(lastChild);
node = lastChild;
return true;
}
node = Node.NewExtension(childrenIndexes.ToArray(), lastChild);
- cache.PutNode(node);
+ _cache.PutNode(node);
return true;
}
case NodeType.Empty:
@@ -122,7 +122,7 @@ private bool TryDelete(ref Node node, ReadOnlySpan path)
}
case NodeType.HashNode:
{
- var newNode = cache.Resolve(node.Hash);
+ var newNode = _cache.Resolve(node.Hash);
if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt delete");
node = newNode;
return TryDelete(ref node, path);
diff --git a/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Find.cs b/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Find.cs
index 0132adbcb7..4becbd2496 100644
--- a/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Find.cs
+++ b/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Find.cs
@@ -34,7 +34,7 @@ private ReadOnlySpan Seek(ref Node node, ReadOnlySpan path, out Node
break;
case NodeType.HashNode:
{
- var newNode = cache.Resolve(node.Hash);
+ var newNode = _cache.Resolve(node.Hash);
if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt seek");
node = newNode;
return Seek(ref node, path, out start);
@@ -84,7 +84,7 @@ private ReadOnlySpan Seek(ref Node node, ReadOnlySpan path, out Node
}
if (path.Length > Node.MaxKeyLength || from.Length > Node.MaxKeyLength)
throw new ArgumentException("exceeds limit");
- path = Seek(ref root, path, out Node start).ToArray();
+ path = Seek(ref _root, path, out Node start).ToArray();
if (from.Length > 0)
{
for (int i = 0; i < from.Length && i < path.Length; i++)
@@ -120,7 +120,7 @@ private ReadOnlySpan Seek(ref Node node, ReadOnlySpan path, out Node
break;
case NodeType.HashNode:
{
- var newNode = cache.Resolve(node.Hash);
+ var newNode = _cache.Resolve(node.Hash);
if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt find");
node = newNode;
foreach (var item in Travers(node, path, from, offset))
diff --git a/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Get.cs b/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Get.cs
index 4ff6b431db..3193637087 100644
--- a/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Get.cs
+++ b/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Get.cs
@@ -25,7 +25,7 @@ public byte[] this[byte[] key]
throw new ArgumentException("could not be empty", nameof(key));
if (path.Length > Node.MaxKeyLength)
throw new ArgumentException("exceeds limit", nameof(key));
- var result = TryGet(ref root, path, out var value);
+ var result = TryGet(ref _root, path, out var value);
return result ? value.ToArray() : throw new KeyNotFoundException();
}
}
@@ -38,7 +38,7 @@ public bool TryGetValue(byte[] key, out byte[] value)
throw new ArgumentException("could not be empty", nameof(key));
if (path.Length > Node.MaxKeyLength)
throw new ArgumentException("exceeds limit", nameof(key));
- var result = TryGet(ref root, path, out var val);
+ var result = TryGet(ref _root, path, out var val);
if (result)
value = val.ToArray();
return result;
@@ -61,7 +61,7 @@ private bool TryGet(ref Node node, ReadOnlySpan path, out ReadOnlySpan proof)
if (path.Length > Node.MaxKeyLength)
throw new ArgumentException("exceeds limit", nameof(key));
proof = new HashSet(ByteArrayEqualityComparer.Default);
- return GetProof(ref root, path, proof);
+ return GetProof(ref _root, path, proof);
}
private bool GetProof(ref Node node, ReadOnlySpan path, HashSet set)
@@ -46,7 +46,7 @@ private bool GetProof(ref Node node, ReadOnlySpan path, HashSet se
break;
case NodeType.HashNode:
{
- var newNode = cache.Resolve(node.Hash);
+ var newNode = _cache.Resolve(node.Hash);
if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt getproof");
node = newNode;
return GetProof(ref node, path, set);
diff --git a/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Put.cs b/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Put.cs
index fb335df98f..409bbf2d15 100644
--- a/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Put.cs
+++ b/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.Put.cs
@@ -38,7 +38,7 @@ public void Put(byte[] key, byte[] value)
if (val.Length > Node.MaxValueLength)
throw new ArgumentException("exceed limit", nameof(value));
var n = Node.NewLeaf(val);
- Put(ref root, path, n);
+ Put(ref _root, path, n);
}
private void Put(ref Node node, ReadOnlySpan path, Node val)
@@ -49,15 +49,15 @@ private void Put(ref Node node, ReadOnlySpan path, Node val)
{
if (path.IsEmpty)
{
- if (!full) cache.DeleteNode(node.Hash);
+ if (!_full) _cache.DeleteNode(node.Hash);
node = val;
- cache.PutNode(node);
+ _cache.PutNode(node);
return;
}
var branch = Node.NewBranch();
branch.Children[Node.BranchChildCount - 1] = node;
Put(ref branch.Children[path[0]], path[1..], val);
- cache.PutNode(branch);
+ _cache.PutNode(branch);
node = branch;
break;
}
@@ -67,12 +67,12 @@ private void Put(ref Node node, ReadOnlySpan path, Node val)
{
var oldHash = node.Hash;
Put(ref node.Next, path[node.Key.Length..], val);
- if (!full) cache.DeleteNode(oldHash);
+ if (!_full) _cache.DeleteNode(oldHash);
node.SetDirty();
- cache.PutNode(node);
+ _cache.PutNode(node);
return;
}
- if (!full) cache.DeleteNode(node.Hash);
+ if (!_full) _cache.DeleteNode(node.Hash);
var prefix = CommonPrefix(node.Key.Span, path);
var pathRemain = path[prefix.Length..];
var keyRemain = node.Key.Span[prefix.Length..];
@@ -85,7 +85,7 @@ private void Put(ref Node node, ReadOnlySpan path, Node val)
else
{
var exNode = Node.NewExtension(keyRemain[1..].ToArray(), node.Next);
- cache.PutNode(exNode);
+ _cache.PutNode(exNode);
child.Children[keyRemain[0]] = exNode;
}
if (pathRemain.IsEmpty)
@@ -98,11 +98,11 @@ private void Put(ref Node node, ReadOnlySpan path, Node val)
Put(ref grandChild, pathRemain[1..], val);
child.Children[pathRemain[0]] = grandChild;
}
- cache.PutNode(child);
+ _cache.PutNode(child);
if (prefix.Length > 0)
{
var exNode = Node.NewExtension(prefix.ToArray(), child);
- cache.PutNode(exNode);
+ _cache.PutNode(exNode);
node = exNode;
}
else
@@ -122,9 +122,9 @@ private void Put(ref Node node, ReadOnlySpan path, Node val)
{
Put(ref node.Children[path[0]], path[1..], val);
}
- if (!full) cache.DeleteNode(oldHash);
+ if (!_full) _cache.DeleteNode(oldHash);
node.SetDirty();
- cache.PutNode(node);
+ _cache.PutNode(node);
break;
}
case NodeType.Empty:
@@ -137,15 +137,15 @@ private void Put(ref Node node, ReadOnlySpan path, Node val)
else
{
newNode = Node.NewExtension(path.ToArray(), val);
- cache.PutNode(newNode);
+ _cache.PutNode(newNode);
}
node = newNode;
- if (val.Type == NodeType.LeafNode) cache.PutNode(val);
+ if (val.Type == NodeType.LeafNode) _cache.PutNode(val);
break;
}
case NodeType.HashNode:
{
- Node newNode = cache.Resolve(node.Hash);
+ Node newNode = _cache.Resolve(node.Hash);
if (newNode is null) throw new InvalidOperationException("Internal error, can't resolve hash when mpt put");
node = newNode;
Put(ref node, path, val);
diff --git a/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.cs b/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.cs
index 4e5890f5ac..7a48f2dc0a 100644
--- a/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.cs
+++ b/src/Neo.Cryptography.MPTTrie/Cryptography/MPTTrie/Trie.cs
@@ -17,24 +17,24 @@ namespace Neo.Cryptography.MPTTrie
public partial class Trie
{
private const byte Prefix = 0xf0;
- private readonly bool full;
- private readonly IStoreSnapshot store;
- private Node root;
- private readonly Cache cache;
- public Node Root => root;
+ private readonly bool _full;
+ private readonly IStoreSnapshot _store;
+ private Node _root;
+ private readonly Cache _cache;
+ public Node Root => _root;
public Trie(IStoreSnapshot store, UInt256 root, bool full_state = false)
{
- this.store = store ?? throw new ArgumentNullException(nameof(store));
- cache = new Cache(store, Prefix);
- this.root = root is null ? new Node() : Node.NewHash(root);
- full = full_state;
+ _store = store ?? throw new ArgumentNullException(nameof(store));
+ _cache = new Cache(store, Prefix);
+ _root = root is null ? new Node() : Node.NewHash(root);
+ _full = full_state;
}
private static byte[] ToNibbles(ReadOnlySpan path)
{
var result = new byte[path.Length * 2];
- for (int i = 0; i < path.Length; i++)
+ for (var i = 0; i < path.Length; i++)
{
result[i * 2] = (byte)(path[i] >> 4);
result[i * 2 + 1] = (byte)(path[i] & 0x0F);
@@ -46,7 +46,7 @@ private static byte[] FromNibbles(ReadOnlySpan path)
{
if (path.Length % 2 != 0) throw new FormatException($"MPTTrie.FromNibbles invalid path.");
var key = new byte[path.Length / 2];
- for (int i = 0; i < key.Length; i++)
+ for (var i = 0; i < key.Length; i++)
{
key[i] = (byte)(path[i * 2] << 4);
key[i] |= path[i * 2 + 1];
@@ -56,7 +56,7 @@ private static byte[] FromNibbles(ReadOnlySpan path)
public void Commit()
{
- cache.Commit();
+ _cache.Commit();
}
}
}
diff --git a/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs
index a420bc5265..a5fe1290bb 100644
--- a/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs
+++ b/tests/Neo.Cryptography.MPTTrie.Tests/Cryptography/MPTTrie/UT_Trie.cs
@@ -22,21 +22,21 @@ namespace Neo.Cryptography.MPTTrie.Tests
{
class TestSnapshot : IStoreSnapshot
{
- public Dictionary store = new Dictionary(ByteArrayEqualityComparer.Default);
+ public Dictionary _store = new(ByteArrayEqualityComparer.Default);
- private byte[] StoreKey(byte[] key)
+ private static byte[] StoreKey(byte[] key)
{
return [.. key];
}
public void Put(byte[] key, byte[] value)
{
- store[key] = value;
+ _store[key] = value;
}
public void Delete(byte[] key)
{
- store.Remove(StoreKey(key));
+ _store.Remove(StoreKey(key));
}
public IStore Store => throw new NotImplementedException();
@@ -49,26 +49,26 @@ public void Delete(byte[] key)
public byte[] TryGet(byte[] key)
{
- var result = store.TryGetValue(StoreKey(key), out byte[] value);
+ var result = _store.TryGetValue(StoreKey(key), out byte[] value);
if (result) return value;
return null;
}
public bool TryGet(byte[] key, out byte[] value)
{
- return store.TryGetValue(StoreKey(key), out value);
+ return _store.TryGetValue(StoreKey(key), out value);
}
public void Dispose() { throw new NotImplementedException(); }
- public int Size => store.Count;
+ public int Size => _store.Count;
}
[TestClass]
public class UT_Trie
{
- private Node root;
- private IStore mptdb;
+ private Node _root;
+ private IStore _mptdb;
private void PutToStore(IStore store, Node node)
{
@@ -85,31 +85,47 @@ public void TestInit()
var v3 = Node.NewLeaf(Encoding.ASCII.GetBytes("existing"));//key=acae
var v4 = Node.NewLeaf(Encoding.ASCII.GetBytes("missing"));
var h3 = Node.NewHash(v3.Hash);
- var e1 = Node.NewExtension(new byte[] { 0x01 }, v1);
- var e3 = Node.NewExtension(new byte[] { 0x0e }, h3);
- var e4 = Node.NewExtension(new byte[] { 0x01 }, v4);
+ var e1 = Node.NewExtension([0x01], v1);
+ var e3 = Node.NewExtension([0x0e], h3);
+ var e4 = Node.NewExtension([0x01], v4);
b.Children[0] = e1;
b.Children[10] = e3;
b.Children[16] = v2;
b.Children[15] = Node.NewHash(e4.Hash);
- root = r;
- mptdb = new MemoryStore();
- PutToStore(mptdb, r);
- PutToStore(mptdb, b);
- PutToStore(mptdb, e1);
- PutToStore(mptdb, e3);
- PutToStore(mptdb, v1);
- PutToStore(mptdb, v2);
- PutToStore(mptdb, v3);
+ _root = r;
+ _mptdb = new MemoryStore();
+ PutToStore(_mptdb, r);
+ PutToStore(_mptdb, b);
+ PutToStore(_mptdb, e1);
+ PutToStore(_mptdb, e3);
+ PutToStore(_mptdb, v1);
+ PutToStore(_mptdb, v2);
+ PutToStore(_mptdb, v3);
}
[TestMethod]
public void TestTryGet()
{
- var mpt = new Trie(mptdb.GetSnapshot(), root.Hash);
- Assert.ThrowsExactly(() => _ = mpt[Array.Empty()]);
+ var mpt = new Trie(_mptdb.GetSnapshot(), _root.Hash);
+
+ // Errors
+ Assert.ThrowsExactly(() => _ = mpt[[]]);
+ Assert.ThrowsExactly(() => _ = mpt[new byte[255]]);
+ Assert.ThrowsExactly(() => _ = mpt.TryGetValue([], out _));
+ Assert.ThrowsExactly(() => _ = mpt.TryGetValue(new byte[255], out _));
+
+ //Get
Assert.AreEqual("abcd", mpt["ac01".HexToBytes()].ToHexString());
Assert.AreEqual("2222", mpt["ac".HexToBytes()].ToHexString());
+
+ //TryGet
+ Assert.IsTrue(mpt.TryGetValue("ac01".HexToBytes(), out var value));
+ Assert.AreEqual("abcd", value.ToHexString());
+ Assert.IsTrue(mpt.TryGetValue("ac".HexToBytes(), out value));
+ Assert.AreEqual("2222", value.ToHexString());
+ Assert.IsFalse(mpt.TryGetValue("000102".HexToBytes(), out value));
+ Assert.IsNull(value);
+
Assert.ThrowsExactly(() => _ = mpt["ab99".HexToBytes()]);
Assert.ThrowsExactly(() => _ = mpt["ac39".HexToBytes()]);
Assert.ThrowsExactly(() => _ = mpt["ac02".HexToBytes()]);
@@ -121,7 +137,7 @@ public void TestTryGet()
[TestMethod]
public void TestTryGetResolve()
{
- var mpt = new Trie(mptdb.GetSnapshot(), root.Hash);
+ var mpt = new Trie(_mptdb.GetSnapshot(), _root.Hash);
Assert.AreEqual(Encoding.ASCII.GetBytes("existing").ToHexString(), mpt["acae".HexToBytes()].ToHexString());
}
@@ -134,10 +150,10 @@ public void TestTryPut()
mpt.Put("ac".HexToBytes(), "2222".HexToBytes());
mpt.Put("acae".HexToBytes(), Encoding.ASCII.GetBytes("existing"));
mpt.Put("acf1".HexToBytes(), Encoding.ASCII.GetBytes("missing"));
- Assert.AreEqual(root.Hash.ToString(), mpt.Root.Hash.ToString());
- Assert.ThrowsExactly(() => mpt.Put(Array.Empty(), "01".HexToBytes()));
- mpt.Put("01".HexToBytes(), Array.Empty());
- Assert.ThrowsExactly(() => mpt.Put(new byte[Node.MaxKeyLength / 2 + 1], Array.Empty()));
+ Assert.AreEqual(_root.Hash.ToString(), mpt.Root.Hash.ToString());
+ Assert.ThrowsExactly(() => mpt.Put([], "01".HexToBytes()));
+ mpt.Put("01".HexToBytes(), []);
+ Assert.ThrowsExactly(() => mpt.Put(new byte[Node.MaxKeyLength / 2 + 1], []));
Assert.ThrowsExactly(() => mpt.Put("01".HexToBytes(), new byte[Node.MaxValueLength + 1]));
mpt.Put("ac01".HexToBytes(), "ab".HexToBytes());
}
@@ -145,17 +161,17 @@ public void TestTryPut()
[TestMethod]
public void TestPutCantResolve()
{
- var mpt = new Trie(mptdb.GetSnapshot(), root.Hash);
- Assert.ThrowsExactly(() => mpt.Put("acf111".HexToBytes(), new byte[] { 1 }));
+ var mpt = new Trie(_mptdb.GetSnapshot(), _root.Hash);
+ Assert.ThrowsExactly(() => mpt.Put("acf111".HexToBytes(), [1]));
}
[TestMethod]
public void TestTryDelete()
{
- var mpt = new Trie(mptdb.GetSnapshot(), root.Hash);
+ var mpt = new Trie(_mptdb.GetSnapshot(), _root.Hash);
Assert.IsNotNull(mpt["ac".HexToBytes()]);
Assert.IsFalse(mpt.Delete("0c99".HexToBytes()));
- Assert.ThrowsExactly(() => _ = mpt.Delete(Array.Empty()));
+ Assert.ThrowsExactly(() => _ = mpt.Delete([]));
Assert.IsFalse(mpt.Delete("ac20".HexToBytes()));
Assert.ThrowsExactly(() => _ = mpt.Delete("acf1".HexToBytes()));
Assert.IsTrue(mpt.Delete("ac".HexToBytes()));
@@ -189,8 +205,8 @@ public void TestDeleteRemainCantResolve()
var r = Node.NewExtension("0a0c".HexToBytes(), b);
var v1 = Node.NewLeaf("abcd".HexToBytes());//key=ac01
var v4 = Node.NewLeaf(Encoding.ASCII.GetBytes("missing"));
- var e1 = Node.NewExtension(new byte[] { 0x01 }, v1);
- var e4 = Node.NewExtension(new byte[] { 0x01 }, v4);
+ var e1 = Node.NewExtension([0x01], v1);
+ var e4 = Node.NewExtension([0x01], v4);
b.Children[0] = e1;
b.Children[15] = Node.NewHash(e4.Hash);
var store = new MemoryStore();
@@ -204,7 +220,6 @@ public void TestDeleteRemainCantResolve()
Assert.ThrowsExactly(() => _ = mpt.Delete("ac01".HexToBytes()));
}
-
[TestMethod]
public void TestDeleteSameValue()
{
@@ -252,15 +267,15 @@ public void TestGetProof()
var v3 = Node.NewLeaf(Encoding.ASCII.GetBytes("existing"));//key=acae
var v4 = Node.NewLeaf(Encoding.ASCII.GetBytes("missing"));
var h3 = Node.NewHash(v3.Hash);
- var e1 = Node.NewExtension(new byte[] { 0x01 }, v1);
- var e3 = Node.NewExtension(new byte[] { 0x0e }, h3);
- var e4 = Node.NewExtension(new byte[] { 0x01 }, v4);
+ var e1 = Node.NewExtension([0x01], v1);
+ var e3 = Node.NewExtension([0x0e], h3);
+ var e4 = Node.NewExtension([0x01], v4);
b.Children[0] = e1;
b.Children[10] = e3;
b.Children[16] = v2;
b.Children[15] = Node.NewHash(e4.Hash);
- var mpt = new Trie(mptdb.GetSnapshot(), r.Hash);
+ var mpt = new Trie(_mptdb.GetSnapshot(), r.Hash);
Assert.AreEqual(r.Hash.ToString(), mpt.Root.Hash.ToString());
var result = mpt.TryGetProof("ac01".HexToBytes(), out var proof);
Assert.IsTrue(result);
@@ -279,7 +294,7 @@ public void TestGetProof()
result = mpt.TryGetProof("acae".HexToBytes(), out proof);
Assert.AreEqual(4, proof.Count);
- Assert.ThrowsExactly(() => _ = mpt.TryGetProof(Array.Empty(), out proof));
+ Assert.ThrowsExactly(() => _ = mpt.TryGetProof([], out proof));
result = mpt.TryGetProof("ac0100".HexToBytes(), out proof);
Assert.IsFalse(result);
@@ -290,12 +305,12 @@ public void TestGetProof()
[TestMethod]
public void TestVerifyProof()
{
- var mpt = new Trie(mptdb.GetSnapshot(), root.Hash);
+ var mpt = new Trie(_mptdb.GetSnapshot(), _root.Hash);
var result = mpt.TryGetProof("ac01".HexToBytes(), out var proof);
Assert.IsTrue(result);
- var value = Trie.VerifyProof(root.Hash, "ac01".HexToBytes(), proof);
+ var value = Trie.VerifyProof(_root.Hash, "ac01".HexToBytes(), proof);
Assert.IsNotNull(value);
- Assert.AreEqual(value.ToHexString(), "abcd");
+ Assert.AreEqual("abcd", value.ToHexString());
}
[TestMethod]
@@ -304,9 +319,9 @@ public void TestAddLongerKey()
var store = new MemoryStore();
var snapshot = store.GetSnapshot();
var mpt = new Trie(snapshot, null);
- mpt.Put(new byte[] { 0xab }, new byte[] { 0x01 });
- mpt.Put(new byte[] { 0xab, 0xcd }, new byte[] { 0x02 });
- Assert.AreEqual("01", mpt[new byte[] { 0xab }].ToHexString());
+ mpt.Put([0xab], [0x01]);
+ mpt.Put([0xab, 0xcd], [0x02]);
+ Assert.AreEqual("01", mpt[[0xab]].ToHexString());
}
[TestMethod]
@@ -315,15 +330,15 @@ public void TestSplitKey()
var store = new MemoryStore();
var snapshot = store.GetSnapshot();
var mpt1 = new Trie(snapshot, null);
- mpt1.Put(new byte[] { 0xab, 0xcd }, new byte[] { 0x01 });
- mpt1.Put(new byte[] { 0xab }, new byte[] { 0x02 });
- var r = mpt1.TryGetProof(new byte[] { 0xab, 0xcd }, out var set1);
+ mpt1.Put([0xab, 0xcd], [0x01]);
+ mpt1.Put([0xab], [0x02]);
+ var r = mpt1.TryGetProof([0xab, 0xcd], out var set1);
Assert.IsTrue(r);
Assert.AreEqual(4, set1.Count);
var mpt2 = new Trie(snapshot, null);
- mpt2.Put(new byte[] { 0xab }, new byte[] { 0x02 });
- mpt2.Put(new byte[] { 0xab, 0xcd }, new byte[] { 0x01 });
- r = mpt2.TryGetProof(new byte[] { 0xab, 0xcd }, out var set2);
+ mpt2.Put([0xab], [0x02]);
+ mpt2.Put([0xab, 0xcd], [0x01]);
+ r = mpt2.TryGetProof([0xab, 0xcd], out var set2);
Assert.IsTrue(r);
Assert.AreEqual(4, set2.Count);
Assert.AreEqual(mpt1.Root.Hash, mpt2.Root.Hash);
@@ -335,21 +350,21 @@ public void TestFind()
var store = new MemoryStore();
var snapshot = store.GetSnapshot();
var mpt1 = new Trie(snapshot, null);
- var results = mpt1.Find(ReadOnlySpan.Empty).ToArray();
+ var results = mpt1.Find([]).ToArray();
Assert.AreEqual(0, results.Length);
var mpt2 = new Trie(snapshot, null);
- mpt2.Put(new byte[] { 0xab, 0xcd, 0xef }, new byte[] { 0x01 });
- mpt2.Put(new byte[] { 0xab, 0xcd, 0xe1 }, new byte[] { 0x02 });
- mpt2.Put(new byte[] { 0xab }, new byte[] { 0x03 });
- results = mpt2.Find(ReadOnlySpan.Empty).ToArray();
+ mpt2.Put([0xab, 0xcd, 0xef], [0x01]);
+ mpt2.Put([0xab, 0xcd, 0xe1], [0x02]);
+ mpt2.Put([0xab], [0x03]);
+ results = [.. mpt2.Find([])];
Assert.AreEqual(3, results.Length);
- results = mpt2.Find(new byte[] { 0xab }).ToArray();
+ results = [.. mpt2.Find([0xab])];
Assert.AreEqual(3, results.Length);
- results = mpt2.Find(new byte[] { 0xab, 0xcd }).ToArray();
+ results = [.. mpt2.Find([0xab, 0xcd])];
Assert.AreEqual(2, results.Length);
- results = mpt2.Find(new byte[] { 0xac }).ToArray();
+ results = [.. mpt2.Find([0xac])];
Assert.AreEqual(0, results.Length);
- results = mpt2.Find(new byte[] { 0xab, 0xcd, 0xef, 0x00 }).ToArray();
+ results = [.. mpt2.Find([0xab, 0xcd, 0xef, 0x00])];
Assert.AreEqual(0, results.Length);
}
@@ -360,8 +375,8 @@ public void TestFindCantResolve()
var r = Node.NewExtension("0a0c".HexToBytes(), b);
var v1 = Node.NewLeaf("abcd".HexToBytes());//key=ac01
var v4 = Node.NewLeaf(Encoding.ASCII.GetBytes("missing"));
- var e1 = Node.NewExtension(new byte[] { 0x01 }, v1);
- var e4 = Node.NewExtension(new byte[] { 0x01 }, v4);
+ var e1 = Node.NewExtension([0x01], v1);
+ var e4 = Node.NewExtension([0x01], v4);
b.Children[0] = e1;
b.Children[15] = Node.NewHash(e4.Hash);
var store = new MemoryStore();
@@ -381,12 +396,12 @@ public void TestFindLeadNode()
// r.Key = 0x0a0c
// b.Key = 0x00
// l1.Key = 0x01
- var mpt = new Trie(mptdb.GetSnapshot(), root.Hash);
+ var mpt = new Trie(_mptdb.GetSnapshot(), _root.Hash);
var prefix = new byte[] { 0xac, 0x01 }; // = FromNibbles(path = { 0x0a, 0x0c, 0x00, 0x01 });
var results = mpt.Find(prefix).ToArray();
- Assert.AreEqual(1, results.Count());
+ Assert.AreEqual(1, results.Length);
- prefix = new byte[] { 0xac }; // = FromNibbles(path = { 0x0a, 0x0c });
+ prefix = [0xac]; // = FromNibbles(path = { 0x0a, 0x0c });
Assert.ThrowsExactly(() => _ = mpt.Find(prefix).ToArray());
}
@@ -397,7 +412,7 @@ public void TestFromNibblesException()
var r = Node.NewExtension("0c".HexToBytes(), b);
var v1 = Node.NewLeaf("abcd".HexToBytes());//key=ac01
var v2 = Node.NewLeaf("2222".HexToBytes());//key=ac
- var e1 = Node.NewExtension(new byte[] { 0x01 }, v1);
+ var e1 = Node.NewExtension([0x01], v1);
b.Children[0] = e1;
b.Children[16] = v2;
var store = new MemoryStore();
@@ -409,7 +424,7 @@ public void TestFromNibblesException()
var snapshot = store.GetSnapshot();
var mpt = new Trie(snapshot, r.Hash);
- Assert.ThrowsExactly(() => _ = mpt.Find(Array.Empty()).Count());
+ Assert.ThrowsExactly(() => _ = mpt.Find([]).Count());
}
[TestMethod]
@@ -533,7 +548,7 @@ public void TestEmptyValueIssue633()
var key = "01".HexToBytes();
var snapshot = new TestSnapshot();
var mpt = new Trie(snapshot, null);
- mpt.Put(key, Array.Empty());
+ mpt.Put(key, []);
var val = mpt[key];
Assert.IsNotNull(val);
Assert.AreEqual(0, val.Length);
@@ -554,11 +569,11 @@ public void TestFindWithFrom()
mpt.Put("aa50".HexToBytes(), "04".HexToBytes());
var r = mpt.Find("aa".HexToBytes()).ToList();
Assert.AreEqual(3, r.Count);
- r = mpt.Find("aa".HexToBytes(), "aa30".HexToBytes()).ToList();
+ r = [.. mpt.Find("aa".HexToBytes(), "aa30".HexToBytes())];
Assert.AreEqual(1, r.Count);
- r = mpt.Find("aa".HexToBytes(), "aa60".HexToBytes()).ToList();
+ r = [.. mpt.Find("aa".HexToBytes(), "aa60".HexToBytes())];
Assert.AreEqual(0, r.Count);
- r = mpt.Find("aa".HexToBytes(), "aa10".HexToBytes()).ToList();
+ r = [.. mpt.Find("aa".HexToBytes(), "aa10".HexToBytes())];
Assert.AreEqual(1, r.Count);
}
@@ -571,9 +586,9 @@ public void TestFindStatesIssue652()
mpt.Put("abc3".HexToBytes(), "02".HexToBytes());
var r = mpt.Find("ab".HexToBytes(), "abd2".HexToBytes()).ToList();
Assert.AreEqual(0, r.Count);
- r = mpt.Find("ab".HexToBytes(), "abb2".HexToBytes()).ToList();
+ r = [.. mpt.Find("ab".HexToBytes(), "abb2".HexToBytes())];
Assert.AreEqual(2, r.Count);
- r = mpt.Find("ab".HexToBytes(), "abc2".HexToBytes()).ToList();
+ r = [.. mpt.Find("ab".HexToBytes(), "abc2".HexToBytes())];
Assert.AreEqual(1, r.Count);
}
}
From d3b943aa52d0a03e884e0e80de9d10230915f316 Mon Sep 17 00:00:00 2001
From: Owen <38493437+superboyiii@users.noreply.github.com>
Date: Fri, 23 May 2025 10:58:36 +0800
Subject: [PATCH 002/158] New Dockerfile, New Makefile (#3954)
* Makefile support for mainnet full node Dockerfile
* add docker run
* add container name
* update doc
* improve
* some improvement
---------
Co-authored-by: Shargon
---
README.md | 4 +++
src/Neo.CLI/Dockerfile | 53 ++++++++++++++++++++++++++-----------
src/Neo.CLI/Makefile | 12 +++++++++
src/Neo.CLI/prepare-node.sh | 25 +++++++++++++++++
src/Neo.CLI/start.sh | 11 ++++++++
5 files changed, 90 insertions(+), 15 deletions(-)
create mode 100644 src/Neo.CLI/Makefile
create mode 100644 src/Neo.CLI/prepare-node.sh
create mode 100644 src/Neo.CLI/start.sh
diff --git a/README.md b/README.md
index a40dbcfaa7..649c86fbfd 100644
--- a/README.md
+++ b/README.md
@@ -94,6 +94,10 @@
+## Quick Start A Mainnet Node
+1. git clone https://github.com/neo-project/neo.git
+2. cd neo/src/Neo.CLI
+3. make build
## Table of Contents
1. [Overview](#overview)
diff --git a/src/Neo.CLI/Dockerfile b/src/Neo.CLI/Dockerfile
index 100056b6f7..848ca5daf6 100644
--- a/src/Neo.CLI/Dockerfile
+++ b/src/Neo.CLI/Dockerfile
@@ -1,20 +1,43 @@
-FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS Build
+FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble
-# Run this from the repository root folder
-COPY src .
-COPY NuGet.Config /Neo.CLI
+# Install all dependencies in a single RUN to reduce layers and speed up build
+RUN apt-get update && \
+ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+ net-tools telnet bash-completion wget lrzsz zip \
+ software-properties-common \
+ apt-transport-https \
+ build-essential \
+ unzip \
+ sqlite3 libsqlite3-dev libunwind8-dev \
+ screen vim ca-certificates gnupg && \
+ add-apt-repository ppa:dotnet/backports && \
+ apt-get update && \
+ apt-get install -y dotnet-sdk-9.0 && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
-WORKDIR /Neo.CLI
-RUN dotnet restore && dotnet publish -f net9.0 -c Release -o /app
+# Set working directory
+WORKDIR /neo
-FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:9.0 AS Final
-RUN apt-get update && apt-get install -y \
- screen \
- libleveldb-dev \
- sqlite3
-RUN rm -rf /var/lib/apt/lists/*
+# Copy and run scripts
+COPY prepare-node.sh .
+RUN chmod +x prepare-node.sh
-WORKDIR /Neo.CLI
-COPY --from=Build /app .
+# Accept build arg and set environment
+ARG NEO_VERSION
+ENV NEO_VERSION=${NEO_VERSION}
-ENTRYPOINT ["screen","-DmS","node","dotnet","neo-cli.dll","-r"]
+RUN ./prepare-node.sh v${NEO_VERSION}
+
+# Modify config in-place
+RUN sed -i 's/"BindAddress":[^,]*/"BindAddress": "0.0.0.0"/' neo-cli/Plugins/RpcServer/RpcServer.json
+
+# Copy and set permissions
+COPY start.sh .
+RUN chmod -R +x ./neo-cli
+
+# Expose port (optional but informative)
+EXPOSE 10332
+
+# Define entrypoint
+ENTRYPOINT ["sh", "./start.sh"]
\ No newline at end of file
diff --git a/src/Neo.CLI/Makefile b/src/Neo.CLI/Makefile
new file mode 100644
index 0000000000..610ca95822
--- /dev/null
+++ b/src/Neo.CLI/Makefile
@@ -0,0 +1,12 @@
+# Extract version from Directory.Build.props using shell command
+VERSION := $(shell grep "" ../Directory.Build.props | head -1 | sed -E 's/.*([^<]+)<\/VersionPrefix>.*/\1/')
+
+.PHONY: build
+
+build:
+ifeq ($(strip $(VERSION)),)
+ $(error VersionPrefix not found in ../Directory.Build.props)
+endif
+ docker build --build-arg NEO_VERSION=$(VERSION) -t neo-node:$(VERSION) .
+ @if [ $$(docker ps -a -q -f name=neo-cli) ]; then docker rm -f neo-cli; fi
+ docker run --name neo-cli -p 10332:10332 neo-node:$(VERSION)
\ No newline at end of file
diff --git a/src/Neo.CLI/prepare-node.sh b/src/Neo.CLI/prepare-node.sh
new file mode 100644
index 0000000000..a80f92b630
--- /dev/null
+++ b/src/Neo.CLI/prepare-node.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+echo "Downloading neo node $1"
+wget https://github.com/neo-project/neo/releases/download/$1/neo-cli.$1-linux-x64.tar.gz
+mkdir neo-cli-linux-x64
+tar -zxvf neo-cli.$1-linux-x64.tar.gz -C neo-cli-linux-x64/
+mv neo-cli-linux-x64 neo-cli
+
+# Allow CLI and Plugins in different versions in case only CLI is released or for any other test usage
+if [ -z "$2" ]; then
+ echo "Downloading plugins $1"
+ wget https://github.com/neo-project/neo/releases/download/$1/ApplicationLogs.zip
+ wget https://github.com/neo-project/neo/releases/download/$1/RpcServer.zip
+ wget https://github.com/neo-project/neo/releases/download/$1/TokensTracker.zip
+else
+ echo "Downloading plugins $2"
+ wget https://github.com/neo-project/neo/releases/download/$2/ApplicationLogs.zip
+ wget https://github.com/neo-project/neo/releases/download/$2/RpcServer.zip
+ wget https://github.com/neo-project/neo/releases/download/$2/TokensTracker.zip
+fi
+
+unzip -n ApplicationLogs.zip -d ./neo-cli/
+unzip -n RpcServer.zip -d ./neo-cli/
+unzip -n TokensTracker.zip -d ./neo-cli/
+
+echo "Node Is Ready!"
\ No newline at end of file
diff --git a/src/Neo.CLI/start.sh b/src/Neo.CLI/start.sh
new file mode 100644
index 0000000000..73890cfc92
--- /dev/null
+++ b/src/Neo.CLI/start.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+# start neo-cli,output log into neo.log
+screen -dmS neo bash -c "./neo-cli/neo-cli > neo.log 2>&1"
+
+# wait for neo.log
+while [ ! -f neo.log ]; do
+ sleep 0.5
+done
+
+tail -f neo.log
\ No newline at end of file
From 39b37b5c0072954794da1c59c8c0562019704c39 Mon Sep 17 00:00:00 2001
From: Will <201105916+Wi1l-B0t@users.noreply.github.com>
Date: Fri, 23 May 2025 19:34:06 +0800
Subject: [PATCH 003/158] [`Optimize`]: command tokenizer (#3918)
* optimize: command tokenizer
* Fix: keep consistency with legacy behaviour when quote char not at first char
---------
Co-authored-by: Shargon
Co-authored-by: NGD Admin <154295625+NGDAdmin@users.noreply.github.com>
---
src/Neo.ConsoleService/CommandQuoteToken.cs | 54 -----
src/Neo.ConsoleService/CommandSpaceToken.cs | 65 ------
src/Neo.ConsoleService/CommandStringToken.cs | 91 --------
src/Neo.ConsoleService/CommandToken.cs | 211 +-----------------
src/Neo.ConsoleService/CommandTokenType.cs | 20 --
src/Neo.ConsoleService/CommandTokenizer.cs | 163 ++++++++++++++
.../ConsoleCommandMethod.cs | 56 +----
src/Neo.ConsoleService/ConsoleServiceBase.cs | 97 ++++----
.../CommandTokenTest.cs | 103 ---------
.../CommandTokenizerTest.cs | 117 ++++++++++
10 files changed, 341 insertions(+), 636 deletions(-)
delete mode 100644 src/Neo.ConsoleService/CommandQuoteToken.cs
delete mode 100644 src/Neo.ConsoleService/CommandSpaceToken.cs
delete mode 100644 src/Neo.ConsoleService/CommandStringToken.cs
delete mode 100644 src/Neo.ConsoleService/CommandTokenType.cs
create mode 100644 src/Neo.ConsoleService/CommandTokenizer.cs
delete mode 100644 tests/Neo.ConsoleService.Tests/CommandTokenTest.cs
create mode 100644 tests/Neo.ConsoleService.Tests/CommandTokenizerTest.cs
diff --git a/src/Neo.ConsoleService/CommandQuoteToken.cs b/src/Neo.ConsoleService/CommandQuoteToken.cs
deleted file mode 100644
index a4de9651d6..0000000000
--- a/src/Neo.ConsoleService/CommandQuoteToken.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2015-2025 The Neo Project.
-//
-// CommandQuoteToken.cs file belongs to the neo project and is free
-// software distributed under the MIT software license, see the
-// accompanying file LICENSE in the main directory of the
-// repository or http://www.opensource.org/licenses/mit-license.php
-// for more details.
-//
-// Redistribution and use in source and binary forms with or without
-// modifications are permitted.
-
-using System;
-using System.Diagnostics;
-
-namespace Neo.ConsoleService
-{
- [DebuggerDisplay("Value={Value}, Value={Value}")]
- internal class CommandQuoteToken : CommandToken
- {
- ///
- /// Constructor
- ///
- /// Offset
- /// Value
- public CommandQuoteToken(int offset, char value) : base(CommandTokenType.Quote, offset)
- {
- if (value != '\'' && value != '"')
- {
- throw new ArgumentException("Not valid quote");
- }
-
- Value = value.ToString();
- }
-
- ///
- /// Parse command line quotes
- ///
- /// Command line
- /// Index
- /// CommandQuoteToken
- internal static CommandQuoteToken Parse(string commandLine, ref int index)
- {
- var c = commandLine[index];
-
- if (c == '\'' || c == '"')
- {
- index++;
- return new CommandQuoteToken(index - 1, c);
- }
-
- throw new ArgumentException("No quote found");
- }
- }
-}
diff --git a/src/Neo.ConsoleService/CommandSpaceToken.cs b/src/Neo.ConsoleService/CommandSpaceToken.cs
deleted file mode 100644
index fc2c446611..0000000000
--- a/src/Neo.ConsoleService/CommandSpaceToken.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright (C) 2015-2025 The Neo Project.
-//
-// CommandSpaceToken.cs file belongs to the neo project and is free
-// software distributed under the MIT software license, see the
-// accompanying file LICENSE in the main directory of the
-// repository or http://www.opensource.org/licenses/mit-license.php
-// for more details.
-//
-// Redistribution and use in source and binary forms with or without
-// modifications are permitted.
-
-using System;
-using System.Diagnostics;
-
-namespace Neo.ConsoleService
-{
- [DebuggerDisplay("Value={Value}, Count={Count}")]
- internal class CommandSpaceToken : CommandToken
- {
- ///
- /// Count
- ///
- public int Count { get; }
-
- ///
- /// Constructor
- ///
- /// Offset
- /// Count
- public CommandSpaceToken(int offset, int count) : base(CommandTokenType.Space, offset)
- {
- Value = "".PadLeft(count, ' ');
- Count = count;
- }
-
- ///
- /// Parse command line spaces
- ///
- /// Command line
- /// Index
- /// CommandSpaceToken
- internal static CommandSpaceToken Parse(string commandLine, ref int index)
- {
- int offset = index;
- int count = 0;
-
- for (int ix = index, max = commandLine.Length; ix < max; ix++)
- {
- if (commandLine[ix] == ' ')
- {
- count++;
- }
- else
- {
- break;
- }
- }
-
- if (count == 0) throw new ArgumentException("No spaces found");
-
- index += count;
- return new CommandSpaceToken(offset, count);
- }
- }
-}
diff --git a/src/Neo.ConsoleService/CommandStringToken.cs b/src/Neo.ConsoleService/CommandStringToken.cs
deleted file mode 100644
index a8bf67e2db..0000000000
--- a/src/Neo.ConsoleService/CommandStringToken.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (C) 2015-2025 The Neo Project.
-//
-// CommandStringToken.cs file belongs to the neo project and is free
-// software distributed under the MIT software license, see the
-// accompanying file LICENSE in the main directory of the
-// repository or http://www.opensource.org/licenses/mit-license.php
-// for more details.
-//
-// Redistribution and use in source and binary forms with or without
-// modifications are permitted.
-
-using System;
-using System.Diagnostics;
-
-namespace Neo.ConsoleService
-{
- [DebuggerDisplay("Value={Value}, RequireQuotes={RequireQuotes}")]
- internal class CommandStringToken : CommandToken
- {
- ///
- /// Require quotes
- ///
- public bool RequireQuotes { get; }
-
- ///
- /// Constructor
- ///
- /// Offset
- /// Value
- public CommandStringToken(int offset, string value) : base(CommandTokenType.String, offset)
- {
- Value = value;
- RequireQuotes = value.IndexOfAny(new char[] { '\'', '"' }) != -1;
- }
-
- ///
- /// Parse command line spaces
- ///
- /// Command line
- /// Index
- /// Quote (could be null)
- /// CommandSpaceToken
- internal static CommandStringToken Parse(string commandLine, ref int index, CommandQuoteToken? quote)
- {
- int end;
- int offset = index;
-
- if (quote != null)
- {
- var ix = index;
-
- do
- {
- end = commandLine.IndexOf(quote.Value[0], ix + 1);
-
- if (end == -1)
- {
- throw new ArgumentException("String not closed");
- }
-
- if (IsScaped(commandLine, end - 1))
- {
- ix = end;
- end = -1;
- }
- }
- while (end < 0);
- }
- else
- {
- end = commandLine.IndexOf(' ', index + 1);
- }
-
- if (end == -1)
- {
- end = commandLine.Length;
- }
-
- var ret = new CommandStringToken(offset, commandLine.Substring(index, end - index));
- index += end - index;
- return ret;
- }
-
- private static bool IsScaped(string commandLine, int index)
- {
- // TODO: Scape the scape
-
- return (commandLine[index] == '\\');
- }
- }
-}
diff --git a/src/Neo.ConsoleService/CommandToken.cs b/src/Neo.ConsoleService/CommandToken.cs
index 9ac1d90458..5f1e8b8035 100644
--- a/src/Neo.ConsoleService/CommandToken.cs
+++ b/src/Neo.ConsoleService/CommandToken.cs
@@ -9,221 +9,32 @@
// Redistribution and use in source and binary forms with or without
// modifications are permitted.
-using System;
-using System.Collections.Generic;
-using System.Text;
-
namespace Neo.ConsoleService
{
- internal abstract class CommandToken
+ public readonly struct CommandToken(int offset, string value, char quoteChar)
{
- ///
- /// Offset
- ///
- public int Offset { get; }
+ public const char NoQuoteChar = '\0';
///
- /// Type
+ /// The start offset of the token in the command line
///
- public CommandTokenType Type { get; }
+ public readonly int Offset { get; } = offset;
///
- /// Value
+ /// The value of the token
///
- public string Value { get; protected init; } = string.Empty;
+ public readonly string Value { get; } = value;
- ///
- /// Constructor
- ///
- /// Type
- /// Offset
- protected CommandToken(CommandTokenType type, int offset)
- {
- Type = type;
- Offset = offset;
- }
+ private readonly char _quoteChar = quoteChar;
///
- /// Parse command line
+ /// The raw value of the token(includes quote character if raw value is quoted)
///
- /// Command line
- ///
- public static IEnumerable Parse(string commandLine)
- {
- CommandToken? lastToken = null;
-
- for (int index = 0, count = commandLine.Length; index < count;)
- {
- switch (commandLine[index])
- {
- case ' ':
- {
- lastToken = CommandSpaceToken.Parse(commandLine, ref index);
- yield return lastToken;
- break;
- }
- case '"':
- case '\'':
- {
- // "'"
- if (lastToken is CommandQuoteToken quote && quote.Value[0] != commandLine[index])
- {
- goto default;
- }
-
- lastToken = CommandQuoteToken.Parse(commandLine, ref index);
- yield return lastToken;
- break;
- }
- default:
- {
- lastToken = CommandStringToken.Parse(commandLine, ref index,
- lastToken is CommandQuoteToken quote ? quote : null);
-
- if (lastToken is not null)
- {
- yield return lastToken;
- }
- break;
- }
- }
- }
- }
+ public readonly string RawValue => _quoteChar == NoQuoteChar ? Value : $"{_quoteChar}{Value}{_quoteChar}";
///
- /// Create string arguments
+ /// Whether the token is white spaces(includes empty) or not
///
- /// Tokens
- /// Remove escape
- /// Arguments
- public static string[] ToArguments(IEnumerable tokens, bool removeEscape = true)
- {
- var list = new List();
-
- CommandToken? lastToken = null;
-
- foreach (var token in tokens)
- {
- if (token is CommandStringToken str)
- {
- if (removeEscape && lastToken is CommandQuoteToken quote)
- {
- // Remove escape
-
- list.Add(str.Value.Replace("\\" + quote.Value, quote.Value));
- }
- else
- {
- list.Add(str.Value);
- }
- }
-
- lastToken = token;
- }
-
- return list.ToArray();
- }
-
- ///
- /// Create a string from token list
- ///
- /// Tokens
- /// String
- public static string ToString(IEnumerable tokens)
- {
- var sb = new StringBuilder();
-
- foreach (var token in tokens)
- {
- sb.Append(token.Value);
- }
-
- return sb.ToString();
- }
-
- ///
- /// Trim
- ///
- /// Args
- public static void Trim(List args)
- {
- // Trim start
-
- while (args.Count > 0 && args[0].Type == CommandTokenType.Space)
- {
- args.RemoveAt(0);
- }
-
- // Trim end
-
- while (args.Count > 0 && args[^1].Type == CommandTokenType.Space)
- {
- args.RemoveAt(args.Count - 1);
- }
- }
-
- ///
- /// Read String
- ///
- /// Args
- /// Consume all if not quoted
- /// String
- public static string? ReadString(List args, bool consumeAll)
- {
- Trim(args);
-
- var quoted = false;
-
- if (args.Count > 0 && args[0].Type == CommandTokenType.Quote)
- {
- quoted = true;
- args.RemoveAt(0);
- }
- else
- {
- if (consumeAll)
- {
- // Return all if it's not quoted
-
- var ret = ToString(args);
- args.Clear();
-
- return ret;
- }
- }
-
- if (args.Count > 0)
- {
- switch (args[0])
- {
- case CommandQuoteToken _:
- {
- if (quoted)
- {
- args.RemoveAt(0);
- return "";
- }
-
- throw new ArgumentException("Unmatched quote");
- }
- case CommandSpaceToken _: throw new ArgumentException("Unmatched space");
- case CommandStringToken str:
- {
- args.RemoveAt(0);
-
- if (quoted && args.Count > 0 && args[0].Type == CommandTokenType.Quote)
- {
- // Remove last quote
-
- args.RemoveAt(0);
- }
-
- return str.Value;
- }
- }
- }
-
- return null;
- }
+ public readonly bool IsWhiteSpace => _quoteChar == NoQuoteChar && string.IsNullOrWhiteSpace(Value);
}
}
diff --git a/src/Neo.ConsoleService/CommandTokenType.cs b/src/Neo.ConsoleService/CommandTokenType.cs
deleted file mode 100644
index 0a6bf99df6..0000000000
--- a/src/Neo.ConsoleService/CommandTokenType.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (C) 2015-2025 The Neo Project.
-//
-// CommandTokenType.cs file belongs to the neo project and is free
-// software distributed under the MIT software license, see the
-// accompanying file LICENSE in the main directory of the
-// repository or http://www.opensource.org/licenses/mit-license.php
-// for more details.
-//
-// Redistribution and use in source and binary forms with or without
-// modifications are permitted.
-
-namespace Neo.ConsoleService
-{
- internal enum CommandTokenType : byte
- {
- String,
- Space,
- Quote,
- }
-}
diff --git a/src/Neo.ConsoleService/CommandTokenizer.cs b/src/Neo.ConsoleService/CommandTokenizer.cs
new file mode 100644
index 0000000000..3b51da0a63
--- /dev/null
+++ b/src/Neo.ConsoleService/CommandTokenizer.cs
@@ -0,0 +1,163 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// CommandTokenizer.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Neo.ConsoleService
+{
+ public static class CommandTokenizer
+ {
+ private static char EscapedChar(char ch)
+ {
+ return ch switch
+ {
+ '\\' => '\\',
+ '"' => '"',
+ '\'' => '\'',
+ 'n' => '\n',
+ 'r' => '\r',
+ 't' => '\t',
+ 'v' => '\v',
+ 'b' => '\b',
+ 'f' => '\f',
+ 'a' => '\a',
+ 'e' => '\e',
+ '0' => '\0',
+ ' ' => ' ',
+ _ => throw new ArgumentException($"Invalid escaped character: {ch}")
+ };
+ }
+
+ ///
+ /// Tokenize a command line
+ ///
+ /// The command line to tokenize
+ /// The tokens
+ public static List Tokenize(this string commandLine)
+ {
+ var tokens = new List();
+ var token = new StringBuilder();
+ var quoteChar = CommandToken.NoQuoteChar;
+ var addToken = (int index, char quote) =>
+ {
+ var value = token.ToString();
+ tokens.Add(new CommandToken(index - value.Length, value, quote));
+ token.Clear();
+ };
+
+ for (var index = 0; index < commandLine.Length; index++)
+ {
+ var ch = commandLine[index];
+ if (ch == '\\')
+ {
+ index++;
+ if (index >= commandLine.Length) throw new ArgumentException("Unexpected end of command line");
+ token.Append(EscapedChar(commandLine[index]));
+ }
+ else if (quoteChar != CommandToken.NoQuoteChar)
+ {
+ if (ch == quoteChar)
+ {
+ addToken(index, quoteChar);
+ quoteChar = CommandToken.NoQuoteChar;
+ }
+ else
+ {
+ token.Append(ch);
+ }
+ }
+ else if (ch == '"' || ch == '\'')
+ {
+ if (token.Length == 0) // If ch is the first char. To keep consistency with legacy behavior
+ {
+ quoteChar = ch;
+ }
+ else
+ {
+ token.Append(ch); // If ch is not the first char, append it as a normal char
+ }
+ }
+ else if (char.IsWhiteSpace(ch))
+ {
+ if (token.Length > 0) addToken(index, quoteChar);
+
+ token.Append(ch);
+ while (index + 1 < commandLine.Length && char.IsWhiteSpace(commandLine[index + 1]))
+ {
+ token.Append(commandLine[++index]);
+ }
+ addToken(index, quoteChar);
+ }
+ else
+ {
+ token.Append(ch);
+ }
+ }
+
+ if (quoteChar != CommandToken.NoQuoteChar) // uncompleted quote
+ throw new ArgumentException($"Unmatched quote({quoteChar})");
+ if (token.Length > 0) addToken(commandLine.Length, quoteChar);
+ return tokens;
+ }
+
+ ///
+ /// Join the raw token values into a single string without prefix and suffix white spaces
+ ///
+ /// The list of tokens
+ /// The joined string
+ public static string JoinRaw(this IList tokens)
+ {
+ return string.Join("", tokens.Trim().Select(t => t.RawValue));
+ }
+
+ ///
+ /// Consume the first token from the list without prefix and suffix white spaces
+ ///
+ /// The list of tokens
+ /// The value of the first non-white space token
+ public static string Consume(this IList tokens)
+ {
+ tokens.Trim();
+ if (tokens.Count == 0) return "";
+
+ var token = tokens[0];
+ tokens.RemoveAt(0);
+ return token.Value;
+ }
+
+ ///
+ /// Consume all tokens from the list and join them without prefix and suffix white spaces
+ ///
+ /// The list of tokens
+ /// The joined value of all tokens without prefix and suffix white spaces
+ public static string ConsumeAll(this IList tokens)
+ {
+ var result = tokens.Trim().JoinRaw();
+ tokens.Clear();
+ return result;
+ }
+
+ ///
+ /// Remove the prefix and suffix white spaces from the list of tokens
+ ///
+ /// The list of tokens
+ /// The trimmed list of tokens
+ public static IList Trim(this IList tokens)
+ {
+ while (tokens.Count > 0 && tokens[0].IsWhiteSpace) tokens.RemoveAt(0);
+ while (tokens.Count > 0 && tokens[^1].IsWhiteSpace) tokens.RemoveAt(tokens.Count - 1);
+ return tokens;
+ }
+ }
+}
diff --git a/src/Neo.ConsoleService/ConsoleCommandMethod.cs b/src/Neo.ConsoleService/ConsoleCommandMethod.cs
index 4df5469c10..2fbdf9c29c 100644
--- a/src/Neo.ConsoleService/ConsoleCommandMethod.cs
+++ b/src/Neo.ConsoleService/ConsoleCommandMethod.cs
@@ -64,58 +64,20 @@ public ConsoleCommandMethod(object instance, MethodInfo method, ConsoleCommandAt
}
///
- /// Is this command
+ /// Match this command or not
///
/// Tokens
- /// Consumed Arguments
- /// True if is this command
- public bool IsThisCommand(CommandToken[] tokens, out int consumedArgs)
+ /// Tokens consumed, 0 if not match
+ public int IsThisCommand(IReadOnlyList tokens)
{
- int checks = Verbs.Length;
- bool quoted = false;
- var tokenList = new List(tokens);
-
- while (checks > 0 && tokenList.Count > 0)
- {
- switch (tokenList[0])
- {
- case CommandSpaceToken _:
- {
- tokenList.RemoveAt(0);
- break;
- }
- case CommandQuoteToken _:
- {
- quoted = !quoted;
- tokenList.RemoveAt(0);
- break;
- }
- case CommandStringToken str:
- {
- if (Verbs[^checks] != str.Value.ToLowerInvariant())
- {
- consumedArgs = 0;
- return false;
- }
-
- checks--;
- tokenList.RemoveAt(0);
- break;
- }
- }
- }
-
- if (quoted && tokenList.Count > 0 && tokenList[0].Type == CommandTokenType.Quote)
+ int matched = 0, consumed = 0;
+ for (; matched < Verbs.Length && consumed < tokens.Count; consumed++)
{
- tokenList.RemoveAt(0);
+ if (tokens[consumed].IsWhiteSpace) continue;
+ if (tokens[consumed].Value != Verbs[matched]) return 0;
+ matched++;
}
-
- // Trim start
-
- while (tokenList.Count > 0 && tokenList[0].Type == CommandTokenType.Space) tokenList.RemoveAt(0);
-
- consumedArgs = tokens.Length - tokenList.Count;
- return checks == 0;
+ return matched == Verbs.Length ? consumed : 0;
}
}
}
diff --git a/src/Neo.ConsoleService/ConsoleServiceBase.cs b/src/Neo.ConsoleService/ConsoleServiceBase.cs
index 79af19bee3..6bcd72c283 100644
--- a/src/Neo.ConsoleService/ConsoleServiceBase.cs
+++ b/src/Neo.ConsoleService/ConsoleServiceBase.cs
@@ -40,7 +40,7 @@ public abstract class ConsoleServiceBase
private readonly CountdownEvent _shutdownAcknowledged = new(1);
private readonly Dictionary> _verbs = new();
private readonly Dictionary _instances = new();
- private readonly Dictionary, bool, object>> _handlers = new();
+ private readonly Dictionary, bool, object>> _handlers = new();
private readonly List _commandHistory = new();
@@ -49,19 +49,17 @@ private bool OnCommand(string commandLine)
if (string.IsNullOrEmpty(commandLine)) return true;
var possibleHelp = "";
- var commandArgs = CommandToken.Parse(commandLine).ToArray();
+ var tokens = commandLine.Tokenize();
var availableCommands = new List<(ConsoleCommandMethod Command, object?[] Arguments)>();
-
foreach (var entries in _verbs.Values)
{
foreach (var command in entries)
{
- if (!command.IsThisCommand(commandArgs, out var consumedArgs)) continue;
+ var consumed = command.IsThisCommand(tokens);
+ if (consumed <= 0) continue;
var arguments = new List