diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs b/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs index 84436abadd8653..632535b3bab0c2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs @@ -32,6 +32,8 @@ namespace System.Text #pragma warning disable SA1001 // Commas should be spaced correctly , ISpanFormattable , IUtf8SpanFormattable + , IParsable + , ISpanParsable , IUtf8SpanParsable #pragma warning restore SA1001 #endif @@ -996,6 +998,54 @@ static Rune IUtf8SpanParsable.Parse(ReadOnlySpan utf8Text, System.IF return result; } + /// + static Rune IParsable.Parse(string s, IFormatProvider? provider) + { + ArgumentNullException.ThrowIfNull(s); + + if (DecodeFromUtf16(s, out Rune result, out int charsConsumed) != OperationStatus.Done || charsConsumed != s.Length) + { + ThrowHelper.ThrowFormatInvalidString(); + } + + return result; + } + + /// + static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Rune result) + { + if (DecodeFromUtf16(s, out result, out int charsConsumed) != OperationStatus.Done || charsConsumed != s!.Length) + { + result = ReplacementChar; + return false; + } + + return true; + } + + /// + static Rune ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) + { + if (DecodeFromUtf16(s, out Rune result, out int charsConsumed) != OperationStatus.Done || charsConsumed != s.Length) + { + ThrowHelper.ThrowFormatInvalidString(); + } + + return result; + } + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, out Rune result) + { + if (DecodeFromUtf16(s, out result, out int charsConsumed) != OperationStatus.Done || charsConsumed != s.Length) + { + result = ReplacementChar; + return false; + } + + return true; + } + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); #endif diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 7e7ae5dbe9f49b..b4e1e0dbe0bb05 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -16109,7 +16109,7 @@ public enum NormalizationForm [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] FormKD = 6, } - public readonly partial struct Rune : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.ISpanFormattable, System.IUtf8SpanFormattable, System.IUtf8SpanParsable + public readonly partial struct Rune : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.IUtf8SpanFormattable, System.IUtf8SpanParsable { private readonly int _dummyPrimitive; public Rune(char ch) { throw null; } @@ -16164,7 +16164,11 @@ public enum NormalizationForm public static bool operator <=(System.Text.Rune left, System.Text.Rune right) { throw null; } int System.IComparable.CompareTo(object? obj) { throw null; } string System.IFormattable.ToString(string? format, System.IFormatProvider? formatProvider) { throw null; } + static System.Text.Rune System.IParsable.Parse(string s, System.IFormatProvider? provider) { throw null; } + static bool System.IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, out System.Text.Rune result) { throw null; } bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + static System.Text.Rune System.ISpanParsable.Parse(System.ReadOnlySpan s, System.IFormatProvider? provider) { throw null; } + static bool System.ISpanParsable.TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, out System.Text.Rune result) { throw null; } bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } static System.Text.Rune System.IUtf8SpanParsable.Parse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider) { throw null; } static bool System.IUtf8SpanParsable.TryParse(System.ReadOnlySpan utf8Text, System.IFormatProvider? provider, out System.Text.Rune result) { throw null; } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs index 028c8ac5acc5a5..829662d7cb7cf8 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/RuneTests.cs @@ -199,6 +199,30 @@ public static void DecodeFromUtf16(char[] data, OperationStatus expectedOperatio Assert.Equal(expectedOperationStatus, Rune.DecodeFromUtf16(data, out Rune actualRune, out int actualCharsConsumed)); Assert.Equal(expectedRuneValue, actualRune.Value); Assert.Equal(expectedCharsConsumed, actualCharsConsumed); + + string s = new string(data); + + // Parse / TryParse succeed iff the entire input decodes to a single scalar. + if (expectedOperationStatus == OperationStatus.Done && expectedCharsConsumed == data.Length) + { + Assert.Equal(expectedRuneValue, ParsableHelper.Parse(s, null).Value); + Assert.True(ParsableHelper.TryParse(s, null, out Rune parsedFromString)); + Assert.Equal(expectedRuneValue, parsedFromString.Value); + + Assert.Equal(expectedRuneValue, SpanParsableHelper.Parse(data, null).Value); + Assert.True(SpanParsableHelper.TryParse(data, null, out Rune parsedFromSpan)); + Assert.Equal(expectedRuneValue, parsedFromSpan.Value); + } + else + { + Assert.Throws(() => ParsableHelper.Parse(s, null)); + Assert.False(ParsableHelper.TryParse(s, null, out Rune parsedFromString)); + Assert.Equal(Rune.ReplacementChar, parsedFromString); + + Assert.Throws(() => SpanParsableHelper.Parse(data, null)); + Assert.False(SpanParsableHelper.TryParse(data, null, out Rune parsedFromSpan)); + Assert.Equal(Rune.ReplacementChar, parsedFromSpan); + } } [Theory] @@ -247,47 +271,27 @@ public static void DecodeFromUtf8(byte[] data, OperationStatus expectedOperation Assert.Equal(expectedOperationStatus, Rune.DecodeFromUtf8(data, out Rune actualRune, out int actualBytesConsumed)); Assert.Equal(expectedRuneValue, actualRune.Value); Assert.Equal(expectedBytesConsumed, actualBytesConsumed); - } - [Theory] - [InlineData(new byte[] { 0x30 }, 0x0030)] // ASCII byte - [InlineData(new byte[] { 0xC3, 0x90 }, 0x00D0)] // [ C3 90 ] is U+00D0 LATIN CAPITAL LETTER ETH - [InlineData(new byte[] { 0xE2, 0x88, 0xB4 }, 0x2234)] // [ E2 88 B4 ] is U+2234 THEREFORE - [InlineData(new byte[] { 0xF0, 0x9F, 0x98, 0xB2 }, 0x1F632)] // [ F0 9F 98 B2 ] is U+1F632 ASTONISHED FACE - public static void ParseUtf8(byte[] data, int expectedRuneValue) - { - Assert.Equal(expectedRuneValue, Utf8SpanParsableHelper.Parse(data, null).Value); - Assert.True(Utf8SpanParsableHelper.TryParse(data, null, out Rune actualRune)); - Assert.Equal(expectedRuneValue, actualRune.Value); + // Parse / TryParse succeed iff the entire input decodes to a single scalar. + if (expectedOperationStatus == OperationStatus.Done && expectedBytesConsumed == data.Length) + { + Assert.Equal(expectedRuneValue, Utf8SpanParsableHelper.Parse(data, null).Value); + Assert.True(Utf8SpanParsableHelper.TryParse(data, null, out Rune parsedFromUtf8)); + Assert.Equal(expectedRuneValue, parsedFromUtf8.Value); + } + else + { + Assert.Throws(() => Utf8SpanParsableHelper.Parse(data, null)); + Assert.False(Utf8SpanParsableHelper.TryParse(data, null, out Rune parsedFromUtf8)); + Assert.Equal(Rune.ReplacementChar, parsedFromUtf8); + } } - [Theory] - [InlineData(new byte[0])] // empty buffer - [InlineData(new byte[] { 0x30, 0x40, 0x50 })] // Multiple ASCII bytes - [InlineData(new byte[] { 0x80 })] // standalone continuation byte - [InlineData(new byte[] { 0x80, 0x80, 0x80 })] // standalone continuation byte - [InlineData(new byte[] { 0xC1 })] // C1 is never a valid UTF-8 byte - [InlineData(new byte[] { 0xF5 })] // F5 is never a valid UTF-8 byte - [InlineData(new byte[] { 0xC2 })] // C2 is a valid byte; expecting it to be followed by a continuation byte - [InlineData(new byte[] { 0xED })] // ED is a valid byte; expecting it to be followed by a continuation byte - [InlineData(new byte[] { 0xF4 })] // F4 is a valid byte; expecting it to be followed by a continuation byte - [InlineData(new byte[] { 0xC2, 0xC2 })] // C2 not followed by continuation byte - [InlineData(new byte[] { 0xC1, 0xBF })] // [ C1 BF ] is overlong 2-byte sequence, all overlong sequences have maximal invalid subsequence length 1 - [InlineData(new byte[] { 0xE0, 0x9F })] // [ E0 9F ] is overlong 3-byte sequence, all overlong sequences have maximal invalid subsequence length 1 - [InlineData(new byte[] { 0xE0, 0xA0 })] // [ E0 A0 ] is valid 2-byte start of 3-byte sequence - [InlineData(new byte[] { 0xED, 0x9F })] // [ ED 9F ] is valid 2-byte start of 3-byte sequence - [InlineData(new byte[] { 0xED, 0xBF })] // [ ED BF ] would place us in UTF-16 surrogate range, all surrogate sequences have maximal invalid subsequence length 1 - [InlineData(new byte[] { 0xEE, 0x80 })] // [ EE 80 ] is valid 2-byte start of 3-byte sequence - [InlineData(new byte[] { 0xF0, 0x8F })] // [ F0 8F ] is overlong 4-byte sequence, all overlong sequences have maximal invalid subsequence length 1 - [InlineData(new byte[] { 0xF0, 0x90 })] // [ F0 90 ] is valid 2-byte start of 4-byte sequence - [InlineData(new byte[] { 0xF4, 0x90 })] // [ F4 90 ] would place us beyond U+10FFFF, all such sequences have maximal invalid subsequence length 1 - [InlineData(new byte[] { 0xE2, 0x88, 0xC0 })] // [ E2 88 ] followed by non-continuation byte, maximal invalid subsequence length 2 - [InlineData(new byte[] { 0xF0, 0x9F, 0x98 })] // [ F0 9F 98 ] is valid 3-byte start of 4-byte sequence - [InlineData(new byte[] { 0xF0, 0x9F, 0x98, 0x20 })] // [ F0 9F 98 ] followed by non-continuation byte, maximal invalid subsequence length 3 - public static void ParseUtf8_Invalid(byte[] data) - { - Assert.Throws(() => Utf8SpanParsableHelper.Parse(data, null)); - Assert.False(Utf8SpanParsableHelper.TryParse(data, null, out Rune actualRune)); + [Fact] + public static void Parse_String_Null() + { + Assert.Throws(() => ParsableHelper.Parse(null, null)); + Assert.False(ParsableHelper.TryParse(null, null, out Rune actualRune)); Assert.Equal(Rune.ReplacementChar, actualRune); }