diff --git a/src/libraries/Common/src/Interop/Browser/Interop.CompareInfo.cs b/src/libraries/Common/src/Interop/Browser/Interop.CompareInfo.cs index a536d9dfbe65e3..cb7edf5b380a26 100644 --- a/src/libraries/Common/src/Interop/Browser/Interop.CompareInfo.cs +++ b/src/libraries/Common/src/Interop/Browser/Interop.CompareInfo.cs @@ -8,15 +8,15 @@ internal static partial class Interop internal static unsafe partial class JsGlobalization { [MethodImplAttribute(MethodImplOptions.InternalCall)] - internal static extern unsafe int CompareString(out string exceptionMessage, in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options); + internal static extern unsafe int CompareString(in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options, out int exceptionalResult, out object result); [MethodImplAttribute(MethodImplOptions.InternalCall)] - internal static extern unsafe bool StartsWith(out string exceptionMessage, in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options); + internal static extern unsafe bool StartsWith(in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options, out int exceptionalResult, out object result); [MethodImplAttribute(MethodImplOptions.InternalCall)] - internal static extern unsafe bool EndsWith(out string exceptionMessage, in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options); + internal static extern unsafe bool EndsWith(in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options, out int exceptionalResult, out object result); [MethodImplAttribute(MethodImplOptions.InternalCall)] - internal static extern unsafe int IndexOf(out string exceptionMessage, in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options, bool fromBeginning); + internal static extern unsafe int IndexOf(in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options, bool fromBeginning, out int exceptionalResult, out object result); } } diff --git a/src/libraries/Common/src/Interop/Browser/Interop.Normalization.cs b/src/libraries/Common/src/Interop/Browser/Interop.Normalization.cs new file mode 100644 index 00000000000000..b55cf5307d704f --- /dev/null +++ b/src/libraries/Common/src/Interop/Browser/Interop.Normalization.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Text; + +internal static partial class Interop +{ + internal static unsafe partial class JsGlobalization + { + [MethodImplAttribute(MethodImplOptions.InternalCall)] + internal static extern unsafe int IsNormalized(NormalizationForm normalizationForm, in string source, out int exceptionalResult, out object result); + + [MethodImplAttribute(MethodImplOptions.InternalCall)] + internal static extern unsafe int NormalizeString(NormalizationForm normalizationForm, in string source, char* dstBuffer, int dstBufferCapacity, out int exceptionalResult, out object result); + } +} diff --git a/src/libraries/Common/src/Interop/Browser/Interop.TextInfo.cs b/src/libraries/Common/src/Interop/Browser/Interop.TextInfo.cs index 8ab038b851e63d..ff157eb45f26cf 100644 --- a/src/libraries/Common/src/Interop/Browser/Interop.TextInfo.cs +++ b/src/libraries/Common/src/Interop/Browser/Interop.TextInfo.cs @@ -8,8 +8,8 @@ internal static partial class Interop internal static unsafe partial class JsGlobalization { [MethodImplAttribute(MethodImplOptions.InternalCall)] - internal static extern unsafe void ChangeCaseInvariant(out string exceptionMessage, char* src, int srcLen, char* dstBuffer, int dstBufferCapacity, bool bToUpper); + internal static extern unsafe void ChangeCaseInvariant(char* src, int srcLen, char* dstBuffer, int dstBufferCapacity, bool bToUpper, out int exceptionalResult, out object result); [MethodImplAttribute(MethodImplOptions.InternalCall)] - internal static extern unsafe void ChangeCase(out string exceptionMessage, in string culture, char* src, int srcLen, char* dstBuffer, int dstBufferCapacity, bool bToUpper); + internal static extern unsafe void ChangeCase(in string culture, char* src, int srcLen, char* dstBuffer, int dstBufferCapacity, bool bToUpper, out int exceptionalResult, out object result); } } diff --git a/src/libraries/System.Globalization.Extensions/tests/Hybrid/System.Globalization.Extensions.Hybrid.WASM.Tests.csproj b/src/libraries/System.Globalization.Extensions/tests/Hybrid/System.Globalization.Extensions.Hybrid.WASM.Tests.csproj new file mode 100644 index 00000000000000..0e7c98c19436f5 --- /dev/null +++ b/src/libraries/System.Globalization.Extensions/tests/Hybrid/System.Globalization.Extensions.Hybrid.WASM.Tests.csproj @@ -0,0 +1,20 @@ + + + $(NetCoreAppCurrent)-browser + true + true + + + + + + + + + NormalizationDataWin8 + + + NormalizationDataWin7 + + + diff --git a/src/libraries/System.Globalization/System.Globalization.sln b/src/libraries/System.Globalization/System.Globalization.sln index 16427cb5f577e8..d709c1ea3ffc45 100644 --- a/src/libraries/System.Globalization/System.Globalization.sln +++ b/src/libraries/System.Globalization/System.Globalization.sln @@ -1,5 +1,5 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# +# Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Private.CoreLib", "..\..\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj", "{E269F8BB-F629-4C96-B9B2-03A00D8B1BFB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities.Unicode", "..\Common\tests\TestUtilities.Unicode\TestUtilities.Unicode.csproj", "{79613DED-481D-44EF-BB89-7AC6BD53026B}" @@ -34,9 +34,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{A93AFF96-DB2 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{0378EF1C-9838-4AD0-867D-506FB02F8BBB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hybrid.IOS.Tests", "tests\Hybrid\Hybrid.IOS.Tests.csproj", "{16D9996B-A4E1-440B-8D74-C9ED3715158D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Globalization.Hybrid.IOS.Tests", "tests\Hybrid\System.Globalization.Hybrid.IOS.Tests.csproj", "{16D9996B-A4E1-440B-8D74-C9ED3715158D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hybrid.WASM.Tests", "tests\Hybrid\Hybrid.WASM.Tests.csproj", "{CAA35471-75A3-41A8-B09D-0CC9822A8E3B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Globalization.Hybrid.WASM.Tests", "tests\Hybrid\System.Globalization.Hybrid.WASM.Tests.csproj", "{CAA35471-75A3-41A8-B09D-0CC9822A8E3B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/libraries/System.Globalization/tests/Hybrid/Hybrid.WASM.Tests.csproj b/src/libraries/System.Globalization/tests/Hybrid/System.Globalization.Hybrid.WASM.Tests.csproj similarity index 100% rename from src/libraries/System.Globalization/tests/Hybrid/Hybrid.WASM.Tests.csproj rename to src/libraries/System.Globalization/tests/Hybrid/System.Globalization.Hybrid.WASM.Tests.csproj diff --git a/src/libraries/System.Globalization/tests/Hybrid/Hybrid.IOS.Tests.csproj b/src/libraries/System.Globalization/tests/Hybrid/System.Globalization.IOS.Tests.csproj similarity index 100% rename from src/libraries/System.Globalization/tests/Hybrid/Hybrid.IOS.Tests.csproj rename to src/libraries/System.Globalization/tests/Hybrid/System.Globalization.IOS.Tests.csproj diff --git a/src/libraries/System.Globalization/tests/System/Globalization/TextInfoTests.cs b/src/libraries/System.Globalization/tests/System/Globalization/TextInfoTests.cs index 4fba5e19ff37d3..11b4dd5249843e 100644 --- a/src/libraries/System.Globalization/tests/System/Globalization/TextInfoTests.cs +++ b/src/libraries/System.Globalization/tests/System/Globalization/TextInfoTests.cs @@ -325,14 +325,12 @@ public void ToLower_Netcore(string name, string str, string expected) [Fact] public void ToLower_InvalidSurrogates() { - bool usesTextDecoder = PlatformDetection.IsHybridGlobalizationOnBrowser && PlatformDetection.IsBrowserDomSupportedOrNodeJS; - // Invalid UTF-16 in a string (mismatched surrogate pairs) should be unchanged. foreach (string cultureName in new string[] { "", "en-US", "fr" }) { - ToLower(cultureName, "\uD83C\uD83C", usesTextDecoder ? "\uFFFD\uFFFD" : "\uD83C\uD83C"); - ToLower(cultureName, "BE CAREFUL, \uDF08\uD83C, THIS ONE IS TRICKY", usesTextDecoder ? "be careful, \uFFFD\uFFFD, this one is tricky" : "be careful, \uDF08\uD83C, this one is tricky"); - ToLower(cultureName, "BE CAREFUL, \uDF08\uDF08, THIS ONE IS TRICKY", usesTextDecoder ? "be careful, \uFFFD\uFFFD, this one is tricky" : "be careful, \uDF08\uDF08, this one is tricky"); + ToLower(cultureName, "BE CAREFUL, \uD83C\uD83C, THIS ONE IS TRICKY", "be careful, \uD83C\uD83C, this one is tricky"); + ToLower(cultureName, "BE CAREFUL, \uDF08\uD83C, THIS ONE IS TRICKY", "be careful, \uDF08\uD83C, this one is tricky"); + ToLower(cultureName, "BE CAREFUL, \uDF08\uDF08, THIS ONE IS TRICKY", "be careful, \uDF08\uDF08, this one is tricky"); } } @@ -454,14 +452,12 @@ public void ToUpper_netcore(string name, string str, string expected) [Fact] public void ToUpper_InvalidSurrogates() { - bool usesTextDecoder = PlatformDetection.IsHybridGlobalizationOnBrowser && PlatformDetection.IsBrowserDomSupportedOrNodeJS; - // Invalid UTF-16 in a string (mismatched surrogate pairs) should be unchanged. foreach (string cultureName in new string[] { "", "en-US", "fr"}) { - ToUpper(cultureName, "be careful, \uD83C\uD83C, this one is tricky", usesTextDecoder ? "BE CAREFUL, \uFFFD\uFFFD, THIS ONE IS TRICKY" : "BE CAREFUL, \uD83C\uD83C, THIS ONE IS TRICKY"); - ToUpper(cultureName, "be careful, \uDF08\uD83C, this one is tricky", usesTextDecoder ? "BE CAREFUL, \uFFFD\uFFFD, THIS ONE IS TRICKY" : "BE CAREFUL, \uDF08\uD83C, THIS ONE IS TRICKY"); - ToUpper(cultureName, "be careful, \uDF08\uDF08, this one is tricky", usesTextDecoder ? "BE CAREFUL, \uFFFD\uFFFD, THIS ONE IS TRICKY" : "BE CAREFUL, \uDF08\uDF08, THIS ONE IS TRICKY"); + ToUpper(cultureName, "be careful, \uD83C\uD83C, this one is tricky", "BE CAREFUL, \uD83C\uD83C, THIS ONE IS TRICKY"); + ToUpper(cultureName, "be careful, \uDF08\uD83C, this one is tricky", "BE CAREFUL, \uDF08\uD83C, THIS ONE IS TRICKY"); + ToUpper(cultureName, "be careful, \uDF08\uDF08, this one is tricky", "BE CAREFUL, \uDF08\uDF08, THIS ONE IS TRICKY"); } } diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 10b27e30eb6b04..23d8a0512bd8fb 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -370,6 +370,7 @@ + @@ -1253,6 +1254,9 @@ + + Common\Interop\Interop.Normalization.cs + Common\Interop\Interop.CompareInfo.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs index a31c6a71b165b0..0d15aa194f9e05 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs @@ -188,11 +188,9 @@ private unsafe int IndexOfOrdinalIgnoreCaseHelper(ReadOnlySpan source, Rea #if TARGET_BROWSER if (GlobalizationMode.Hybrid) { - int result = Interop.JsGlobalization.IndexOf(out string exceptionMessage, m_name, b, target.Length, a, source.Length, options, fromBeginning); - if (!string.IsNullOrEmpty(exceptionMessage)) - { - throw new Exception(exceptionMessage); - } + int result = Interop.JsGlobalization.IndexOf(m_name, b, target.Length, a, source.Length, options, fromBeginning, out int exception, out object ex_result); + if (exception != 0) + throw new Exception((string)ex_result); return result; } #endif @@ -288,7 +286,12 @@ private unsafe int IndexOfOrdinalHelper(ReadOnlySpan source, ReadOnlySpan< InteropCall: #if TARGET_BROWSER if (GlobalizationMode.Hybrid) - return Interop.JsGlobalization.IndexOf(out string exceptionMessage, m_name, b, target.Length, a, source.Length, options, fromBeginning); + { + int result = Interop.JsGlobalization.IndexOf(m_name, b, target.Length, a, source.Length, options, fromBeginning, out int exception, out object ex_result); + if (exception != 0) + throw new Exception((string)ex_result); + return result; + } #endif if (fromBeginning) return Interop.Globalization.IndexOf(_sortHandle, b, target.Length, a, source.Length, options, matchLengthPtr); diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs index d0258a2f43d9a4..08256545244c36 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs @@ -49,10 +49,9 @@ private unsafe int JsCompareString(ReadOnlySpan string1, ReadOnlySpan source, ReadOnlySpan p fixed (char* pSource = &MemoryMarshal.GetReference(source)) fixed (char* pPrefix = &MemoryMarshal.GetReference(prefix)) { - result = Interop.JsGlobalization.StartsWith(out string exceptionMessage, cultureName, pSource, source.Length, pPrefix, prefix.Length, options); - - if (!string.IsNullOrEmpty(exceptionMessage)) - throw new Exception(exceptionMessage); + result = Interop.JsGlobalization.StartsWith(cultureName, pSource, source.Length, pPrefix, prefix.Length, options, out int exception, out object ex_result); + if (exception != 0) + throw new Exception((string)ex_result); } @@ -90,10 +88,9 @@ private unsafe bool JsEndsWith(ReadOnlySpan source, ReadOnlySpan pre fixed (char* pSource = &MemoryMarshal.GetReference(source)) fixed (char* pPrefix = &MemoryMarshal.GetReference(prefix)) { - result = Interop.JsGlobalization.EndsWith(out string exceptionMessage, cultureName, pSource, source.Length, pPrefix, prefix.Length, options); - - if (!string.IsNullOrEmpty(exceptionMessage)) - throw new Exception(exceptionMessage); + result = Interop.JsGlobalization.EndsWith(cultureName, pSource, source.Length, pPrefix, prefix.Length, options, out int exception, out object ex_result); + if (exception != 0) + throw new Exception((string)ex_result); } return result; @@ -118,10 +115,9 @@ private unsafe int JsIndexOfCore(ReadOnlySpan source, ReadOnlySpan t fixed (char* pSource = &MemoryMarshal.GetReference(source)) fixed (char* pTarget = &MemoryMarshal.GetReference(target)) { - idx = Interop.JsGlobalization.IndexOf(out string exceptionMessage, m_name, pTarget, target.Length, pSource, source.Length, options, fromBeginning); - - if (!string.IsNullOrEmpty(exceptionMessage)) - throw new Exception(exceptionMessage); + idx = Interop.JsGlobalization.IndexOf(m_name, pTarget, target.Length, pSource, source.Length, options, fromBeginning, out int exception, out object ex_result); + if (exception != 0) + throw new Exception((string)ex_result); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/Normalization.WebAssembly.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/Normalization.WebAssembly.cs new file mode 100644 index 00000000000000..b930f6812904af --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/Normalization.WebAssembly.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace System.Globalization +{ + internal static partial class Normalization + { + private static unsafe bool JsIsNormalized(string strInput, NormalizationForm normalizationForm) + { + Debug.Assert(!GlobalizationMode.Invariant); + Debug.Assert(!GlobalizationMode.UseNls); + + ValidateArguments(strInput, normalizationForm); + + int ret = Interop.JsGlobalization.IsNormalized(normalizationForm, strInput, out int exception, out object ex_result); + if (exception != 0) + throw new Exception((string)ex_result); + + return ret == 1; + } + + private static unsafe string JsNormalize(string strInput, NormalizationForm normalizationForm) + { + Debug.Assert(!GlobalizationMode.Invariant); + Debug.Assert(!GlobalizationMode.UseNls); + + ValidateArguments(strInput, normalizationForm); + + char[]? toReturn = null; + try + { + const int StackallocThreshold = 512; + + Span buffer = strInput.Length <= StackallocThreshold + ? stackalloc char[StackallocThreshold] + : (toReturn = ArrayPool.Shared.Rent(strInput.Length)); + + for (int attempt = 0; attempt < 2; attempt++) + { + int realLen; + fixed (char* pDest = &MemoryMarshal.GetReference(buffer)) + { + realLen = Interop.JsGlobalization.NormalizeString(normalizationForm, strInput, pDest, buffer.Length, out int exception, out object ex_result); + if (exception != 0) + throw new Exception((string)ex_result); + } + + if (realLen <= buffer.Length) + { + ReadOnlySpan result = buffer.Slice(0, realLen); + return result.SequenceEqual(strInput) + ? strInput + : new string(result); + } + + Debug.Assert(realLen > StackallocThreshold); + + if (attempt == 0) + { + if (toReturn != null) + { + // Clear toReturn first to ensure we don't return the same buffer twice + char[] temp = toReturn; + toReturn = null; + ArrayPool.Shared.Return(temp); + } + + buffer = toReturn = ArrayPool.Shared.Rent(realLen); + } + } + + throw new ArgumentException(SR.Argument_InvalidCharSequenceNoIndex, nameof(strInput)); + } + finally + { + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/Normalization.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/Normalization.cs index d120302a8aa8e7..2bc6107b5dad6c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/Normalization.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/Normalization.cs @@ -19,6 +19,10 @@ internal static bool IsNormalized(string strInput, NormalizationForm normalizati return GlobalizationMode.UseNls ? NlsIsNormalized(strInput, normalizationForm) : +#if TARGET_BROWSER + GlobalizationMode.Hybrid ? + JsIsNormalized(strInput, normalizationForm) : +#endif IcuIsNormalized(strInput, normalizationForm); } @@ -33,6 +37,10 @@ internal static string Normalize(string strInput, NormalizationForm normalizatio return GlobalizationMode.UseNls ? NlsNormalize(strInput, normalizationForm) : +#if TARGET_BROWSER + GlobalizationMode.Hybrid ? + JsNormalize(strInput, normalizationForm) : +#endif IcuNormalize(strInput, normalizationForm); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.WebAssembly.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.WebAssembly.cs index e5ad1ec6420a45..a2dd2a02c05f18 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.WebAssembly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.WebAssembly.cs @@ -13,17 +13,18 @@ internal unsafe void JsChangeCase(char* src, int srcLen, char* dstBuffer, int ds Debug.Assert(!GlobalizationMode.UseNls); Debug.Assert(GlobalizationMode.Hybrid); - string exceptionMessage; + int exception; + object ex_result; if (HasEmptyCultureName) { - Interop.JsGlobalization.ChangeCaseInvariant(out exceptionMessage, src, srcLen, dstBuffer, dstBufferCapacity, toUpper); + Interop.JsGlobalization.ChangeCaseInvariant(src, srcLen, dstBuffer, dstBufferCapacity, toUpper, out exception, out ex_result); } else { - Interop.JsGlobalization.ChangeCase(out exceptionMessage, _cultureName, src, srcLen, dstBuffer, dstBufferCapacity, toUpper); + Interop.JsGlobalization.ChangeCase(_cultureName, src, srcLen, dstBuffer, dstBufferCapacity, toUpper, out exception, out ex_result); } - if (!string.IsNullOrEmpty(exceptionMessage)) - throw new Exception(exceptionMessage); + if (exception != 0) + throw new Exception((string)ex_result); } } } diff --git a/src/mono/sample/wasm/browser-bench/String.cs b/src/mono/sample/wasm/browser-bench/String.cs index d57ec2063363fb..2ac38b481d8e43 100644 --- a/src/mono/sample/wasm/browser-bench/String.cs +++ b/src/mono/sample/wasm/browser-bench/String.cs @@ -17,6 +17,7 @@ public StringTask() { measurements = new Measurement[] { new NormalizeMeasurement(), + new IsNormalizedMeasurement(), new NormalizeMeasurementASCII(), new TextInfoToLower(), new TextInfoToUpper(), @@ -79,6 +80,12 @@ public class NormalizeMeasurement : StringMeasurement public override void RunStep() => str.Normalize(); } + public class IsNormalizedMeasurement : StringMeasurement + { + public override string Name => "IsNormalized"; + public override void RunStep() => str.IsNormalized(); + } + public abstract class ASCIIStringMeasurement : StringMeasurement { public override Task BeforeBatch() diff --git a/src/mono/wasm/runtime/corebindings.c b/src/mono/wasm/runtime/corebindings.c index 8d92fdc5e0ea73..1991b4c930c1d0 100644 --- a/src/mono/wasm/runtime/corebindings.c +++ b/src/mono/wasm/runtime/corebindings.c @@ -43,12 +43,14 @@ extern void* mono_wasm_invoke_js_blazor (MonoString **exceptionMessage, void *ca #endif /* DISABLE_LEGACY_JS_INTEROP */ // HybridGlobalization -extern void mono_wasm_change_case_invariant(MonoString **exceptionMessage, const uint16_t* src, int32_t srcLength, uint16_t* dst, int32_t dstLength, mono_bool bToUpper); -extern void mono_wasm_change_case(MonoString **exceptionMessage, MonoString **culture, const uint16_t* src, int32_t srcLength, uint16_t* dst, int32_t dstLength, mono_bool bToUpper); -extern int mono_wasm_compare_string(MonoString **exceptionMessage, MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options); -extern mono_bool mono_wasm_starts_with(MonoString **exceptionMessage, MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options); -extern mono_bool mono_wasm_ends_with(MonoString **exceptionMessage, MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options); -extern int mono_wasm_index_of(MonoString **exceptionMessage, MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options, mono_bool fromBeginning); +extern void mono_wasm_change_case_invariant(const uint16_t* src, int32_t srcLength, uint16_t* dst, int32_t dstLength, mono_bool bToUpper, int *is_exception, MonoObject** ex_result); +extern void mono_wasm_change_case(MonoString **culture, const uint16_t* src, int32_t srcLength, uint16_t* dst, int32_t dstLength, mono_bool bToUpper, int *is_exception, MonoObject** ex_result); +extern int mono_wasm_compare_string(MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options, int *is_exception, MonoObject** ex_result); +extern mono_bool mono_wasm_starts_with(MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options, int *is_exception, MonoObject** ex_result); +extern mono_bool mono_wasm_ends_with(MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options, int *is_exception, MonoObject** ex_result); +extern int mono_wasm_index_of(MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options, mono_bool fromBeginning, int *is_exception, MonoObject** ex_result); +extern mono_bool mono_wasm_is_normalized(int32_t normalizationForm, MonoString **src, int *is_exception, MonoObject** ex_result); +extern int mono_wasm_normalize_string(int32_t normalizationForm, MonoString **src, uint16_t* dst, int32_t dstLength, int *is_exception, MonoObject** ex_result); void bindings_initialize_internals (void) { @@ -83,4 +85,6 @@ void bindings_initialize_internals (void) mono_add_internal_call ("Interop/JsGlobalization::StartsWith", mono_wasm_starts_with); mono_add_internal_call ("Interop/JsGlobalization::EndsWith", mono_wasm_ends_with); mono_add_internal_call ("Interop/JsGlobalization::IndexOf", mono_wasm_index_of); + mono_add_internal_call ("Interop/JsGlobalization::IsNormalized", mono_wasm_is_normalized); + mono_add_internal_call ("Interop/JsGlobalization::NormalizeString", mono_wasm_normalize_string); } diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index ab293f667aea85..23f27df9485603 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -112,6 +112,10 @@ let linked_functions = [ "mono_wasm_starts_with", "mono_wasm_ends_with", "mono_wasm_index_of", + "mono_wasm_is_normalized", + "mono_wasm_normalize_string", + "mono_wasm_to_Unicode", + "mono_wasm_to_ASCII", "icudt68_dat", ]; diff --git a/src/mono/wasm/runtime/exports-linker.ts b/src/mono/wasm/runtime/exports-linker.ts index f4be31cfa0912f..0aa96cc9cdecdd 100644 --- a/src/mono/wasm/runtime/exports-linker.ts +++ b/src/mono/wasm/runtime/exports-linker.ts @@ -27,7 +27,9 @@ import { mono_wasm_invoke_js_blazor, mono_wasm_invoke_js_with_args_ref, mono_wasm_get_object_property_ref, mono_wasm_set_object_property_ref, mono_wasm_get_by_index_ref, mono_wasm_set_by_index_ref, mono_wasm_get_global_object_ref } from "./net6-legacy/method-calls"; -import { mono_wasm_change_case, mono_wasm_change_case_invariant, mono_wasm_compare_string, mono_wasm_ends_with, mono_wasm_index_of, mono_wasm_starts_with } from "./hybrid-globalization"; +import { mono_wasm_change_case, mono_wasm_change_case_invariant } from "./hybrid-globalization/change-case"; +import { mono_wasm_compare_string, mono_wasm_ends_with, mono_wasm_starts_with, mono_wasm_index_of } from "./hybrid-globalization/collations"; +import { mono_wasm_is_normalized, mono_wasm_normalize_string } from "./hybrid-globalization/normalization"; // the methods would be visible to EMCC linker // --- keep in sync with dotnet.cjs.lib.js --- @@ -103,6 +105,8 @@ export function export_linker(): any { mono_wasm_starts_with, mono_wasm_ends_with, mono_wasm_index_of, + mono_wasm_is_normalized, + mono_wasm_normalize_string, // threading exports, if threading is enabled ...mono_wasm_threads_exports, diff --git a/src/mono/wasm/runtime/hybrid-globalization/change-case.ts b/src/mono/wasm/runtime/hybrid-globalization/change-case.ts new file mode 100644 index 00000000000000..fddd80e9823829 --- /dev/null +++ b/src/mono/wasm/runtime/hybrid-globalization/change-case.ts @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { Module } from "../globals"; +import { setU16 } from "../memory"; +import { mono_wasm_new_external_root } from "../roots"; +import { conv_string_root } from "../strings"; +import { MonoObject, MonoObjectRef, MonoString, MonoStringRef } from "../types/internal"; +import { Int32Ptr } from "../types/emscripten"; +import { wrap_error_root, wrap_no_error_root } from "../invoke-js"; + +export function mono_wasm_change_case_invariant(src: number, srcLength: number, dst: number, dstLength: number, toUpper: number, is_exception: Int32Ptr, ex_address: MonoObjectRef) : void{ + const exceptionRoot = mono_wasm_new_external_root(ex_address); + try{ + const input = get_utf16_string(src, srcLength); + let result = toUpper ? input.toUpperCase() : input.toLowerCase(); + // Unicode defines some codepoints which expand into multiple codepoints, + // originally we do not support this expansion + if (result.length > dstLength) + result = input; + + for (let i = 0; i < result.length; i++) + setU16(dst + i*2, result.charCodeAt(i)); + wrap_no_error_root(is_exception, exceptionRoot); + } + catch (ex: any) { + wrap_error_root(is_exception, ex, exceptionRoot); + } + finally { + exceptionRoot.release(); + } +} + +export function mono_wasm_change_case(culture: MonoStringRef, src: number, srcLength: number, dst: number, destLength: number, toUpper: number, is_exception: Int32Ptr, ex_address: MonoObjectRef) : void{ + const cultureRoot = mono_wasm_new_external_root(culture), + exceptionRoot = mono_wasm_new_external_root(ex_address); + try{ + const cultureName = conv_string_root(cultureRoot); + if (!cultureName) + throw new Error("Cannot change case, the culture name is null."); + const input = get_utf16_string(src, srcLength); + let result = toUpper ? input.toLocaleUpperCase(cultureName) : input.toLocaleLowerCase(cultureName); + if (result.length > destLength) + result = input; + + for (let i = 0; i < destLength; i++) + setU16(dst + i*2, result.charCodeAt(i)); + wrap_no_error_root(is_exception, exceptionRoot); + } + catch (ex: any) { + wrap_error_root(is_exception, ex, exceptionRoot); + } + finally { + cultureRoot.release(); + exceptionRoot.release(); + } +} + +function get_utf16_string(ptr: number, length: number): string{ + const view = new Uint16Array(Module.HEAPU16.buffer, ptr, length); + let string = ""; + for (let i = 0; i < length; i++) + string += String.fromCharCode(view[i]); + return string; +} diff --git a/src/mono/wasm/runtime/hybrid-globalization.ts b/src/mono/wasm/runtime/hybrid-globalization/collations.ts similarity index 68% rename from src/mono/wasm/runtime/hybrid-globalization.ts rename to src/mono/wasm/runtime/hybrid-globalization/collations.ts index b94a2d1ed4e47e..0df5b6dd641488 100644 --- a/src/mono/wasm/runtime/hybrid-globalization.ts +++ b/src/mono/wasm/runtime/hybrid-globalization/collations.ts @@ -1,87 +1,48 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { mono_wasm_new_external_root } from "./roots"; -import { MonoString, MonoStringRef } from "./types/internal"; -import { Int32Ptr } from "./types/emscripten"; -import { conv_string_root, js_string_to_mono_string_root, string_decoder } from "./strings"; -import { setU16_unchecked } from "./memory"; +import { mono_wasm_new_external_root } from "../roots"; +import { conv_string_root, string_decoder } from "../strings"; +import { MonoObject, MonoObjectRef, MonoString, MonoStringRef } from "../types/internal"; +import { Int32Ptr } from "../types/emscripten"; +import { wrap_error_root, wrap_no_error_root } from "../invoke-js"; -export function mono_wasm_change_case_invariant(exceptionMessage: Int32Ptr, src: number, srcLength: number, dst: number, dstLength: number, toUpper: number): void { - try { - const input = string_decoder.decode(src, (src + 2 * srcLength)); - let result = toUpper ? input.toUpperCase() : input.toLowerCase(); - // Unicode defines some codepoints which expand into multiple codepoints, - // originally we do not support this expansion - if (result.length > dstLength) - result = input; - - for (let i = 0, j = dst; i < result.length; i++, j += 2) - setU16_unchecked(j, result.charCodeAt(i)); - } - catch (ex: any) { - pass_exception_details(ex, exceptionMessage); - } -} - -export function mono_wasm_change_case(exceptionMessage: Int32Ptr, culture: MonoStringRef, src: number, srcLength: number, dst: number, destLength: number, toUpper: number): void { - const cultureRoot = mono_wasm_new_external_root(culture); - try { - const cultureName = conv_string_root(cultureRoot); - if (!cultureName) - throw new Error("Cannot change case, the culture name is null."); - const input = string_decoder.decode(src, (src + 2 * srcLength)); - let result = toUpper ? input.toLocaleUpperCase(cultureName) : input.toLocaleLowerCase(cultureName); - if (result.length > destLength) - result = input; - - for (let i = 0, j = dst; i < result.length; i++, j += 2) - setU16_unchecked(j, result.charCodeAt(i)); - } - catch (ex: any) { - pass_exception_details(ex, exceptionMessage); - } - finally { - cultureRoot.release(); - } -} +const COMPARISON_ERROR = -2; +const INDEXING_ERROR = -1; -export function mono_wasm_compare_string(exceptionMessage: Int32Ptr, culture: MonoStringRef, str1: number, str1Length: number, str2: number, str2Length: number, options: number): number { - const cultureRoot = mono_wasm_new_external_root(culture); - try { +export function mono_wasm_compare_string(culture: MonoStringRef, str1: number, str1Length: number, str2: number, str2Length: number, options: number, is_exception: Int32Ptr, ex_address: MonoObjectRef) : number{ + const cultureRoot = mono_wasm_new_external_root(culture), + exceptionRoot = mono_wasm_new_external_root(ex_address); + try{ const cultureName = conv_string_root(cultureRoot); - const string1 = string_decoder.decode(str1, (str1 + 2 * str1Length)); - const string2 = string_decoder.decode(str2, (str2 + 2 * str2Length)); + const string1 = string_decoder.decode(str1, (str1 + 2*str1Length)); + const string2 = string_decoder.decode(str2, (str2 + 2*str2Length)); const casePicker = (options & 0x1f); const locale = cultureName ? cultureName : undefined; + wrap_no_error_root(is_exception, exceptionRoot); return compare_strings(string1, string2, locale, casePicker); } catch (ex: any) { - pass_exception_details(ex, exceptionMessage); - return -2; + wrap_error_root(is_exception, ex, exceptionRoot); + return COMPARISON_ERROR; } finally { cultureRoot.release(); + exceptionRoot.release(); } } -function pass_exception_details(ex: any, exceptionMessage: Int32Ptr) { - const exceptionJsString = ex.message + "\n" + ex.stack; - const exceptionRoot = mono_wasm_new_external_root(exceptionMessage); - js_string_to_mono_string_root(exceptionJsString, exceptionRoot); - exceptionRoot.release(); -} - -export function mono_wasm_starts_with(exceptionMessage: Int32Ptr, culture: MonoStringRef, srcPtr: number, srcLength: number, prefixPtr: number, prefixLength: number, options: number): number{ - const cultureRoot = mono_wasm_new_external_root(culture); - try { +export function mono_wasm_starts_with(culture: MonoStringRef, str1: number, str1Length: number, str2: number, str2Length: number, options: number, is_exception: Int32Ptr, ex_address: MonoObjectRef): number{ + const cultureRoot = mono_wasm_new_external_root(culture), + exceptionRoot = mono_wasm_new_external_root(ex_address); + try{ const cultureName = conv_string_root(cultureRoot); - const prefix = decode_to_clean_string(prefixPtr, prefixLength); + const prefix = decode_to_clean_string(str2, str2Length); // no need to look for an empty string if (prefix.length == 0) return 1; // true - const source = decode_to_clean_string(srcPtr, srcLength); + const source = decode_to_clean_string(str1, str1Length); if (source.length < prefix.length) return 0; //false const sourceOfPrefixLength = source.slice(0, prefix.length); @@ -89,26 +50,29 @@ export function mono_wasm_starts_with(exceptionMessage: Int32Ptr, culture: MonoS const casePicker = (options & 0x1f); const locale = cultureName ? cultureName : undefined; const result = compare_strings(sourceOfPrefixLength, prefix, locale, casePicker); + wrap_no_error_root(is_exception, exceptionRoot); return result === 0 ? 1 : 0; // equals ? true : false } catch (ex: any) { - pass_exception_details(ex, exceptionMessage); - return -1; + wrap_error_root(is_exception, ex, exceptionRoot); + return INDEXING_ERROR; } finally { cultureRoot.release(); + exceptionRoot.release(); } } -export function mono_wasm_ends_with(exceptionMessage: Int32Ptr, culture: MonoStringRef, srcPtr: number, srcLength: number, suffixPtr: number, suffixLength: number, options: number): number{ - const cultureRoot = mono_wasm_new_external_root(culture); - try { +export function mono_wasm_ends_with(culture: MonoStringRef, str1: number, str1Length: number, str2: number, str2Length: number, options: number, is_exception: Int32Ptr, ex_address: MonoObjectRef): number{ + const cultureRoot = mono_wasm_new_external_root(culture), + exceptionRoot = mono_wasm_new_external_root(ex_address); + try{ const cultureName = conv_string_root(cultureRoot); - const suffix = decode_to_clean_string(suffixPtr, suffixLength); + const suffix = decode_to_clean_string(str2, str2Length); if (suffix.length == 0) return 1; // true - const source = decode_to_clean_string(srcPtr, srcLength); + const source = decode_to_clean_string(str1, str1Length); const diff = source.length - suffix.length; if (diff < 0) return 0; //false @@ -117,41 +81,38 @@ export function mono_wasm_ends_with(exceptionMessage: Int32Ptr, culture: MonoStr const casePicker = (options & 0x1f); const locale = cultureName ? cultureName : undefined; const result = compare_strings(sourceOfSuffixLength, suffix, locale, casePicker); + wrap_no_error_root(is_exception, exceptionRoot); return result === 0 ? 1 : 0; // equals ? true : false } catch (ex: any) { - pass_exception_details(ex, exceptionMessage); - return -1; + wrap_error_root(is_exception, ex, exceptionRoot); + return INDEXING_ERROR; } finally { cultureRoot.release(); + exceptionRoot.release(); } } -function decode_to_clean_string(strPtr: number, strLen: number) -{ - const str = string_decoder.decode(strPtr, (strPtr + 2*strLen)); - return clean_string(str); -} - -function clean_string(str: string) -{ - const nStr = str.normalize(); - return nStr.replace(/[\u200B-\u200D\uFEFF\0]/g, ""); -} - -export function mono_wasm_index_of(exceptionMessage: Int32Ptr, culture: MonoStringRef, needlePtr: number, needleLength: number, srcPtr: number, srcLength: number, options: number, fromBeginning: number): number{ - const cultureRoot = mono_wasm_new_external_root(culture); +export function mono_wasm_index_of(culture: MonoStringRef, needlePtr: number, needleLength: number, srcPtr: number, srcLength: number, options: number, fromBeginning: number, is_exception: Int32Ptr, ex_address: MonoObjectRef): number{ + const cultureRoot = mono_wasm_new_external_root(culture), + exceptionRoot = mono_wasm_new_external_root(ex_address); try { const needle = string_decoder.decode(needlePtr, (needlePtr + 2*needleLength)); // no need to look for an empty string if (clean_string(needle).length == 0) + { + wrap_no_error_root(is_exception, exceptionRoot); return fromBeginning ? 0 : srcLength; + } const source = string_decoder.decode(srcPtr, (srcPtr + 2*srcLength)); // no need to look in an empty string if (clean_string(source).length == 0) + { + wrap_no_error_root(is_exception, exceptionRoot); return fromBeginning ? 0 : srcLength; + } const cultureName = conv_string_root(cultureRoot); const locale = cultureName ? cultureName : undefined; const casePicker = (options & 0x1f); @@ -210,14 +171,16 @@ export function mono_wasm_index_of(exceptionMessage: Int32Ptr, culture: MonoStri } i = nextIndex; } + wrap_no_error_root(is_exception, exceptionRoot); return result; } catch (ex: any) { - pass_exception_details(ex, exceptionMessage); - return -1; + wrap_error_root(is_exception, ex, exceptionRoot); + return INDEXING_ERROR; } finally { cultureRoot.release(); + exceptionRoot.release(); } function check_match_found(str1: string, str2: string, locale: string | undefined, casePicker: number) : boolean @@ -226,19 +189,20 @@ export function mono_wasm_index_of(exceptionMessage: Int32Ptr, culture: MonoStri } } -export function compare_strings(string1: string, string2: string, locale: string | undefined, casePicker: number): number { - switch (casePicker) { +function compare_strings(string1: string, string2: string, locale: string | undefined, casePicker: number) : number{ + switch (casePicker) + { case 0: // 0: None - default algorithm for the platform OR - // StringSort - since .Net 5 StringSort gives the same result as None, even for hyphen etc. + // StringSort - for ICU it gives the same result as None, see: https://github.com/dotnet/dotnet-api-docs/issues // does not work for "ja" - if (locale && locale.startsWith("ja")) - return -2; + if (locale && locale.split("-")[0] === "ja") + return COMPARISON_ERROR; return string1.localeCompare(string2, locale); // a ≠ b, a ≠ á, a ≠ A case 8: // 8: IgnoreKanaType works only for "ja" - if (locale && !locale.startsWith("ja")) - return -2; + if (locale && locale.split("-")[0] !== "ja") + return COMPARISON_ERROR; return string1.localeCompare(string2, locale); // a ≠ b, a ≠ á, a ≠ A case 1: // 1: IgnoreCase @@ -317,3 +281,15 @@ export function compare_strings(string1: string, string2: string, locale: string throw new Error(`Invalid comparison option. Option=${casePicker}`); } } + +function decode_to_clean_string(strPtr: number, strLen: number) +{ + const str = string_decoder.decode(strPtr, (strPtr + 2*strLen)); + return clean_string(str); +} + +function clean_string(str: string) +{ + const nStr = str.normalize(); + return nStr.replace(/[\u200B-\u200D\uFEFF\0]/g, ""); +} diff --git a/src/mono/wasm/runtime/hybrid-globalization/normalization.ts b/src/mono/wasm/runtime/hybrid-globalization/normalization.ts new file mode 100644 index 00000000000000..a3e92e252817f6 --- /dev/null +++ b/src/mono/wasm/runtime/hybrid-globalization/normalization.ts @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { setU16 } from "../memory"; +import { mono_wasm_new_external_root } from "../roots"; +import { conv_string_root } from "../strings"; +import { MonoObject, MonoObjectRef, MonoString, MonoStringRef } from "../types/internal"; +import { Int32Ptr } from "../types/emscripten"; +import { wrap_error_root, wrap_no_error_root } from "../invoke-js"; + +const NORMALIZATION_FORM_MAP = [undefined, "NFC", "NFD", undefined, undefined, "NFKC", "NFKD"]; +const ERROR = -1; + +export function mono_wasm_is_normalized(normalizationForm: number, inputStr: MonoStringRef, is_exception: Int32Ptr, ex_address: MonoObjectRef) : number{ + const inputRoot = mono_wasm_new_external_root(inputStr), + exceptionRoot = mono_wasm_new_external_root(ex_address); + try{ + const jsString = conv_string_root(inputRoot); + if (!jsString) + throw new Error("Invalid string was received."); + + const normalization = normalization_to_string(normalizationForm); + const result = jsString.normalize(normalization); + wrap_no_error_root(is_exception, exceptionRoot); + return result === jsString ? 1 : 0; + } + catch (ex) { + wrap_error_root(is_exception, ex, exceptionRoot); + return ERROR; + } finally { + inputRoot.release(); + exceptionRoot.release(); + } +} + +export function mono_wasm_normalize_string(normalizationForm: number, inputStr: MonoStringRef, dstPtr: number, dstLength: number, is_exception: Int32Ptr, ex_address: MonoObjectRef) : number{ + const inputRoot = mono_wasm_new_external_root(inputStr), + exceptionRoot = mono_wasm_new_external_root(ex_address); + try { + const jsString = conv_string_root(inputRoot); + if (!jsString) + throw new Error("Invalid string was received."); + + const normalization = normalization_to_string(normalizationForm); + const result = jsString.normalize(normalization); + + // increase the dest buffer + if (result.length > dstLength) + return result.length; + for (let i = 0; i < result.length; i++) + setU16(dstPtr + i*2, result.charCodeAt(i)); + return result.length; + } catch (ex) { + wrap_error_root(is_exception, ex, exceptionRoot); + return ERROR; + } finally { + inputRoot.release(); + exceptionRoot.release(); + } +} + +const normalization_to_string = (normalizationForm: number): string => NORMALIZATION_FORM_MAP[normalizationForm] ?? "NFC"; +