From d806d57fe0eb0a32c7a818ec5f0fc34b17eab9d1 Mon Sep 17 00:00:00 2001 From: Wi1l-B0t <201105916+Wi1l-B0t@users.noreply.github.com> Date: Sun, 1 Jun 2025 15:35:46 +0800 Subject: [PATCH 1/4] Optimize: keep more exception info when UInt160.Parse and UInt256.Parse throw exception --- src/Neo.Extensions/StringExtensions.cs | 15 ++++++- src/Neo/UInt160.cs | 42 ++++++++++--------- src/Neo/UInt256.cs | 41 +++++++++--------- .../UT_StringExtensions.cs | 8 ++++ 4 files changed, 65 insertions(+), 41 deletions(-) diff --git a/src/Neo.Extensions/StringExtensions.cs b/src/Neo.Extensions/StringExtensions.cs index 0a4c2ecc1b..3e1b0010ee 100644 --- a/src/Neo.Extensions/StringExtensions.cs +++ b/src/Neo.Extensions/StringExtensions.cs @@ -149,7 +149,7 @@ public static byte[] HexToBytes(this ReadOnlySpan value) if (value.IsEmpty) return []; if (value.Length % 2 == 1) - throw new FormatException(); + throw new FormatException($"value.Length({value.Length}) not multiple of 2"); var result = new byte[value.Length / 2]; for (var i = 0; i < result.Length; i++) result[i] = byte.Parse(value.Slice(i * 2, 2), NumberStyles.AllowHexSpecifier); @@ -169,5 +169,18 @@ public static int GetVarSize(this string value) var size = value.GetStrictUtf8ByteCount(); return size.GetVarSize() + size; } + + /// + /// Trims the specified prefix from the start of the , ignoring case. + /// + /// The to trim. + /// The prefix to trim. + /// The trimmed . + public static ReadOnlySpan TrimStartIgnoreCase(this ReadOnlySpan value, ReadOnlySpan prefix) + { + if (value.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) + return value[prefix.Length..]; + return value; + } } } diff --git a/src/Neo/UInt160.cs b/src/Neo/UInt160.cs index 55a716dffe..c437e189df 100644 --- a/src/Neo/UInt160.cs +++ b/src/Neo/UInt160.cs @@ -147,18 +147,6 @@ internal void SafeSerialize(Span destination) BinaryPrimitives.WriteUInt32LittleEndian(destination[IxValue3..], _value3); } - /// - /// Parses an from the specified . - /// - /// An represented by a . - /// The parsed . - /// is not in the correct format. - public static UInt160 Parse(string value) - { - if (!TryParse(value, out var result)) throw new FormatException(); - return result; - } - public void Serialize(BinaryWriter writer) { writer.Write(_value1); @@ -171,21 +159,20 @@ public override string ToString() return "0x" + this.ToArray().ToHexString(reverse: true); } + /// /// Parses an from the specified . /// - /// An represented by a . + /// An represented by a . /// The parsed . - /// if an is successfully parsed; otherwise, . - public static bool TryParse(string str, [NotNullWhen(true)] out UInt160 result) + /// + /// if an is successfully parsed; otherwise, . + /// + public static bool TryParse(string value, [NotNullWhen(true)] out UInt160 result) { result = null; - var data = str.AsSpan(); // AsSpan is null safe - if (data.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)) - data = data[2..]; - + var data = value.AsSpan().TrimStartIgnoreCase("0x"); if (data.Length != Length * 2) return false; - try { result = new UInt160(data.HexToBytesReversed()); @@ -197,6 +184,21 @@ public static bool TryParse(string str, [NotNullWhen(true)] out UInt160 result) } } + + /// + /// Parses an from the specified . + /// + /// An represented by a . + /// The parsed . + /// is not in the correct format. + public static UInt160 Parse(string value) + { + var data = value.AsSpan().TrimStartIgnoreCase("0x"); + if (data.Length != Length * 2) + throw new FormatException($"value.Length({data.Length}) != {Length * 2}"); + return new UInt160(data.HexToBytesReversed()); + } + public static implicit operator UInt160(string s) { return Parse(s); diff --git a/src/Neo/UInt256.cs b/src/Neo/UInt256.cs index 5c48e227de..59e151da39 100644 --- a/src/Neo/UInt256.cs +++ b/src/Neo/UInt256.cs @@ -124,18 +124,6 @@ internal Span GetSpanLittleEndian() return buffer; // Keep the same output as Serialize when BigEndian } - /// - /// Parses an from the specified . - /// - /// An represented by a . - /// The parsed . - /// is not in the correct format. - public static UInt256 Parse(string value) - { - if (!TryParse(value, out var result)) throw new FormatException(); - return result; - } - public void Serialize(BinaryWriter writer) { writer.Write(_value1); @@ -183,18 +171,17 @@ public override string ToString() /// /// Parses an from the specified . /// - /// An represented by a . + /// An represented by a . /// The parsed . - /// if an is successfully parsed; otherwise, . - public static bool TryParse(string s, [NotNullWhen(true)] out UInt256 result) + /// + /// if an is successfully parsed; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParse(string value, [NotNullWhen(true)] out UInt256 result) { result = null; - var data = s.AsSpan(); // AsSpan is null safe - if (data.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)) - data = data[2..]; - + var data = value.AsSpan().TrimStartIgnoreCase("0x"); if (data.Length != Length * 2) return false; - try { result = new UInt256(data.HexToBytesReversed()); @@ -206,6 +193,20 @@ public static bool TryParse(string s, [NotNullWhen(true)] out UInt256 result) } } + /// + /// Parses an from the specified . + /// + /// An represented by a . + /// The parsed . + /// is not in the correct format. + public static UInt256 Parse(string value) + { + var data = value.AsSpan().TrimStartIgnoreCase("0x"); + if (data.Length != Length * 2) + throw new FormatException($"value.Length({data.Length}) != {Length * 2}"); + return new UInt256(data.HexToBytesReversed()); + } + public static bool operator ==(UInt256 left, UInt256 right) { if (ReferenceEquals(left, right)) return true; diff --git a/tests/Neo.Extensions.Tests/UT_StringExtensions.cs b/tests/Neo.Extensions.Tests/UT_StringExtensions.cs index bbf4fe399c..3ce6a45e27 100644 --- a/tests/Neo.Extensions.Tests/UT_StringExtensions.cs +++ b/tests/Neo.Extensions.Tests/UT_StringExtensions.cs @@ -93,6 +93,14 @@ public void TestIsHex() Assert.IsTrue("".IsHex()); } + [TestMethod] + public void TestTrimStartIgnoreCase() + { + Assert.AreEqual("010203", "0x010203".AsSpan().TrimStartIgnoreCase("0x").ToString()); + Assert.AreEqual("010203", "0x010203".AsSpan().TrimStartIgnoreCase("0X").ToString()); + Assert.AreEqual("010203", "0X010203".AsSpan().TrimStartIgnoreCase("0x").ToString()); + } + [TestMethod] public void TestGetVarSizeGeneric() { From d61b6eccf78000171cd30d9efc92dc16505550ab Mon Sep 17 00:00:00 2001 From: Will <201105916+Wi1l-B0t@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:45:24 +0800 Subject: [PATCH 2/4] Update src/Neo.Extensions/StringExtensions.cs Co-authored-by: Christopher Schuchardt --- src/Neo.Extensions/StringExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo.Extensions/StringExtensions.cs b/src/Neo.Extensions/StringExtensions.cs index 3e1b0010ee..d68cdd1c0d 100644 --- a/src/Neo.Extensions/StringExtensions.cs +++ b/src/Neo.Extensions/StringExtensions.cs @@ -175,7 +175,7 @@ public static int GetVarSize(this string value) /// /// The to trim. /// The prefix to trim. - /// The trimmed . + /// The trimmed without prefix. If no prefix is found than the input is returned unmodified. public static ReadOnlySpan TrimStartIgnoreCase(this ReadOnlySpan value, ReadOnlySpan prefix) { if (value.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) From 67f86f54e2048769601489da2dd54d0289b48572 Mon Sep 17 00:00:00 2001 From: Wi1l-B0t <201105916+Wi1l-B0t@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:48:11 +0800 Subject: [PATCH 3/4] Optimize: keep more exception info when UInt160.Parse and UInt256.Parse throw exception --- src/Neo.Extensions/StringExtensions.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Neo.Extensions/StringExtensions.cs b/src/Neo.Extensions/StringExtensions.cs index d68cdd1c0d..bea3cfa26b 100644 --- a/src/Neo.Extensions/StringExtensions.cs +++ b/src/Neo.Extensions/StringExtensions.cs @@ -175,7 +175,10 @@ public static int GetVarSize(this string value) /// /// The to trim. /// The prefix to trim. - /// The trimmed without prefix. If no prefix is found than the input is returned unmodified. + /// + /// The trimmed ReadOnlySpan without prefix. If no prefix is found, the input is returned unmodified. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan TrimStartIgnoreCase(this ReadOnlySpan value, ReadOnlySpan prefix) { if (value.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) From 6f85a395ed70d0fac524621c8462005adb37d08b Mon Sep 17 00:00:00 2001 From: Shargon Date: Mon, 2 Jun 2025 16:33:38 +0200 Subject: [PATCH 4/4] Update src/Neo/UInt160.cs --- src/Neo/UInt160.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Neo/UInt160.cs b/src/Neo/UInt160.cs index c437e189df..5f22f3a508 100644 --- a/src/Neo/UInt160.cs +++ b/src/Neo/UInt160.cs @@ -159,7 +159,6 @@ public override string ToString() return "0x" + this.ToArray().ToHexString(reverse: true); } - /// /// Parses an from the specified . ///