Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ namespace System.Text
#pragma warning disable SA1001 // Commas should be spaced correctly
, ISpanFormattable
, IUtf8SpanFormattable
, IParsable<Rune>
, ISpanParsable<Rune>
, IUtf8SpanParsable<Rune>
#pragma warning restore SA1001
#endif
Expand Down Expand Up @@ -996,6 +998,54 @@ static Rune IUtf8SpanParsable<Rune>.Parse(ReadOnlySpan<byte> utf8Text, System.IF
return result;
}

/// <inheritdoc cref="IParsable{TSelf}.Parse(string, IFormatProvider?)" />
static Rune IParsable<Rune>.Parse(string s, IFormatProvider? provider)
{
ArgumentNullException.ThrowIfNull(s);

if (DecodeFromUtf16(s, out Rune result, out int charsConsumed) != OperationStatus.Done || charsConsumed != s.Length)
Comment thread
tannergooding marked this conversation as resolved.
{
ThrowHelper.ThrowFormatInvalidString();
}

return result;
}

/// <inheritdoc cref="IParsable{TSelf}.TryParse(string?, IFormatProvider?, out TSelf)" />
static bool IParsable<Rune>.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;
}
Comment thread
MihaZupan marked this conversation as resolved.

/// <inheritdoc cref="ISpanParsable{TSelf}.Parse(ReadOnlySpan{char}, IFormatProvider?)" />
static Rune ISpanParsable<Rune>.Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
{
if (DecodeFromUtf16(s, out Rune result, out int charsConsumed) != OperationStatus.Done || charsConsumed != s.Length)
{
ThrowHelper.ThrowFormatInvalidString();
}

return result;
}

/// <inheritdoc cref="ISpanParsable{TSelf}.TryParse(ReadOnlySpan{char}, IFormatProvider?, out TSelf)" />
static bool ISpanParsable<Rune>.TryParse(ReadOnlySpan<char> 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

Expand Down
6 changes: 5 additions & 1 deletion src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16046,7 +16046,7 @@ public enum NormalizationForm
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
FormKD = 6,
}
public readonly partial struct Rune : System.IComparable, System.IComparable<System.Text.Rune>, System.IEquatable<System.Text.Rune>, System.IFormattable, System.ISpanFormattable, System.IUtf8SpanFormattable, System.IUtf8SpanParsable<System.Text.Rune>
public readonly partial struct Rune : System.IComparable, System.IComparable<System.Text.Rune>, System.IEquatable<System.Text.Rune>, System.IFormattable, System.IParsable<System.Text.Rune>, System.ISpanFormattable, System.ISpanParsable<System.Text.Rune>, System.IUtf8SpanFormattable, System.IUtf8SpanParsable<System.Text.Rune>
Comment thread
MihaZupan marked this conversation as resolved.
{
private readonly int _dummyPrimitive;
public Rune(char ch) { throw null; }
Expand Down Expand Up @@ -16101,7 +16101,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<System.Text.Rune>.Parse(string s, System.IFormatProvider? provider) { throw null; }
static bool System.IParsable<System.Text.Rune>.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, out System.Text.Rune result) { throw null; }
bool System.ISpanFormattable.TryFormat(System.Span<char> destination, out int charsWritten, System.ReadOnlySpan<char> format, System.IFormatProvider? provider) { throw null; }
static System.Text.Rune System.ISpanParsable<System.Text.Rune>.Parse(System.ReadOnlySpan<char> s, System.IFormatProvider? provider) { throw null; }
static bool System.ISpanParsable<System.Text.Rune>.TryParse(System.ReadOnlySpan<char> s, System.IFormatProvider? provider, out System.Text.Rune result) { throw null; }
bool System.IUtf8SpanFormattable.TryFormat(System.Span<byte> utf8Destination, out int bytesWritten, System.ReadOnlySpan<char> format, System.IFormatProvider? provider) { throw null; }
static System.Text.Rune System.IUtf8SpanParsable<System.Text.Rune>.Parse(System.ReadOnlySpan<byte> utf8Text, System.IFormatProvider? provider) { throw null; }
static bool System.IUtf8SpanParsable<System.Text.Rune>.TryParse(System.ReadOnlySpan<byte> utf8Text, System.IFormatProvider? provider, out System.Text.Rune result) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Rune>.Parse(s, null).Value);
Assert.True(ParsableHelper<Rune>.TryParse(s, null, out Rune parsedFromString));
Assert.Equal(expectedRuneValue, parsedFromString.Value);

Assert.Equal(expectedRuneValue, SpanParsableHelper<Rune>.Parse(data, null).Value);
Assert.True(SpanParsableHelper<Rune>.TryParse(data, null, out Rune parsedFromSpan));
Assert.Equal(expectedRuneValue, parsedFromSpan.Value);
}
else
{
Assert.Throws<FormatException>(() => ParsableHelper<Rune>.Parse(s, null));
Assert.False(ParsableHelper<Rune>.TryParse(s, null, out Rune parsedFromString));
Assert.Equal(Rune.ReplacementChar, parsedFromString);

Assert.Throws<FormatException>(() => SpanParsableHelper<Rune>.Parse(data, null));
Assert.False(SpanParsableHelper<Rune>.TryParse(data, null, out Rune parsedFromSpan));
Assert.Equal(Rune.ReplacementChar, parsedFromSpan);
}
}

[Theory]
Expand Down Expand Up @@ -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<Rune>.Parse(data, null).Value);
Assert.True(Utf8SpanParsableHelper<Rune>.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<Rune>.Parse(data, null).Value);
Assert.True(Utf8SpanParsableHelper<Rune>.TryParse(data, null, out Rune parsedFromUtf8));
Assert.Equal(expectedRuneValue, parsedFromUtf8.Value);
}
else
{
Assert.Throws<FormatException>(() => Utf8SpanParsableHelper<Rune>.Parse(data, null));
Assert.False(Utf8SpanParsableHelper<Rune>.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<FormatException>(() => Utf8SpanParsableHelper<Rune>.Parse(data, null));
Assert.False(Utf8SpanParsableHelper<Rune>.TryParse(data, null, out Rune actualRune));
[Fact]
public static void Parse_String_Null()
{
Assert.Throws<ArgumentNullException>(() => ParsableHelper<Rune>.Parse(null, null));
Assert.False(ParsableHelper<Rune>.TryParse(null, null, out Rune actualRune));
Assert.Equal(Rune.ReplacementChar, actualRune);
}

Expand Down
Loading