diff --git a/src/Neo/SmartContract/Native/StdLib.cs b/src/Neo/SmartContract/Native/StdLib.cs index 709a85724f..cb540d36a1 100644 --- a/src/Neo/SmartContract/Native/StdLib.cs +++ b/src/Neo/SmartContract/Native/StdLib.cs @@ -16,6 +16,7 @@ using System; using System.Globalization; using System.Numerics; +using System.Text; namespace Neo.SmartContract.Native { @@ -222,5 +223,22 @@ private static string[] StringSplit([MaxLength(MaxInputLength)] string str, stri StringSplitOptions options = removeEmptyEntries ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None; return str.Split(separator, options); } + + [ContractMethod(CpuFee = 1 << 8)] + private static int StrLen([MaxLength(MaxInputLength)] string str) + { + // return the length of the string in elements + // it should return 1 for both "🦆" and "ã" + + TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(str); + int count = 0; + + while (enumerator.MoveNext()) + { + count++; + } + + return count; + } } } diff --git a/tests/Neo.UnitTests/SmartContract/Native/UT_StdLib.cs b/tests/Neo.UnitTests/SmartContract/Native/UT_StdLib.cs index 8342e15824..3251f67014 100644 --- a/tests/Neo.UnitTests/SmartContract/Native/UT_StdLib.cs +++ b/tests/Neo.UnitTests/SmartContract/Native/UT_StdLib.cs @@ -209,6 +209,47 @@ public void StringSplit() Assert.AreEqual("b", arr[1].GetString()); } + [TestMethod] + public void StringElementLength() + { + var snapshot = TestBlockchain.GetTestSnapshot(); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "strLen", "🦆"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "strLen", "ã"); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "strLen", "a"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestBlockchain.TheNeoSystem.Settings); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(engine.Execute(), VMState.HALT); + Assert.AreEqual(3, engine.ResultStack.Count); + Assert.AreEqual(1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(1, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(1, engine.ResultStack.Pop().GetInteger()); + } + + [TestMethod] + public void TestInvalidUtf8Sequence() + { + // Simulating invalid UTF-8 byte (0xff) decoded as a UTF-16 char + const char badChar = (char)0xff; + var badStr = badChar.ToString(); + var snapshot = TestBlockchain.GetTestSnapshot(); + + using var script = new ScriptBuilder(); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "strLen", badStr); + script.EmitDynamicCall(NativeContract.StdLib.Hash, "strLen", badStr + "ab"); + + using var engine = ApplicationEngine.Create(TriggerType.Application, null, snapshot, settings: TestBlockchain.TheNeoSystem.Settings); + engine.LoadScript(script.ToArray()); + + Assert.AreEqual(engine.Execute(), VMState.HALT); + Assert.AreEqual(2, engine.ResultStack.Count); + Assert.AreEqual(3, engine.ResultStack.Pop().GetInteger()); + Assert.AreEqual(1, engine.ResultStack.Pop().GetInteger()); + } + [TestMethod] public void Json_Deserialize() {