Add Utf8String ref asms, react to API changes
authorLevi Broderick <levib@microsoft.com>
Wed, 16 Oct 2019 15:40:42 +0000 (08:40 -0700)
committerLevi Broderick <GrabYourPitchforks@users.noreply.github.com>
Wed, 16 Oct 2019 17:56:18 +0000 (10:56 -0700)
Commit migrated from https://github.com/dotnet/corefx/commit/ba76ea2cf8ee916a28a26d657440b94829737d13

21 files changed:
src/libraries/System.Utf8String.Experimental/ref/System.Utf8String.Experimental.cs
src/libraries/System.Utf8String.Experimental/src/System/IO/Utf8StringStream.cs
src/libraries/System.Utf8String.Experimental/src/System/Net/Http/Utf8StringContent.cs
src/libraries/System.Utf8String.Experimental/tests/System.Utf8String.Experimental.Tests.csproj
src/libraries/System.Utf8String.Experimental/tests/System/Utf8ExtensionsTests.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8SpanTests.Conversion.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8SpanTests.Ctor.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8SpanTests.Enumeration.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8SpanTests.Manipulation.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8SpanTests.Searching.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8SpanTests.TestData.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8SpanTests.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Comparison.cs [new file with mode: 0644]
src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Conversion.cs [new file with mode: 0644]
src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Ctor.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Enumeration.cs [new file with mode: 0644]
src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Manipulation.cs [new file with mode: 0644]
src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Searching.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Substring.cs [deleted file]
src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.cs
src/libraries/System.Utf8String.Experimental/tests/System/Utf8TestUtilities.cs

index 75f2850..64826fb 100644 (file)
@@ -51,25 +51,28 @@ namespace System
     public static partial class Utf8Extensions
     {
         public static System.ReadOnlySpan<byte> AsBytes(this System.ReadOnlySpan<System.Char8> text) { throw null; }
-        public static System.ReadOnlySpan<byte> AsBytes(this System.Utf8String text) { throw null; }
-        public static System.ReadOnlySpan<byte> AsBytes(this System.Utf8String text, int start) { throw null; }
-        public static System.ReadOnlySpan<byte> AsBytes(this System.Utf8String text, int start, int length) { throw null; }
-        public static System.ReadOnlyMemory<System.Char8> AsMemory(this System.Utf8String text) { throw null; }
-        public static System.ReadOnlyMemory<System.Char8> AsMemory(this System.Utf8String text, System.Index startIndex) { throw null; }
-        public static System.ReadOnlyMemory<System.Char8> AsMemory(this System.Utf8String text, int start) { throw null; }
-        public static System.ReadOnlyMemory<System.Char8> AsMemory(this System.Utf8String text, int start, int length) { throw null; }
-        public static System.ReadOnlyMemory<System.Char8> AsMemory(this System.Utf8String text, System.Range range) { throw null; }
-        public static System.ReadOnlyMemory<byte> AsMemoryBytes(this System.Utf8String text) { throw null; }
-        public static System.ReadOnlyMemory<byte> AsMemoryBytes(this System.Utf8String text, System.Index startIndex) { throw null; }
-        public static System.ReadOnlyMemory<byte> AsMemoryBytes(this System.Utf8String text, int start) { throw null; }
-        public static System.ReadOnlyMemory<byte> AsMemoryBytes(this System.Utf8String text, int start, int length) { throw null; }
-        public static System.ReadOnlyMemory<byte> AsMemoryBytes(this System.Utf8String text, System.Range range) { throw null; }
+        public static System.ReadOnlySpan<byte> AsBytes(this System.Utf8String? text) { throw null; }
+        public static System.ReadOnlySpan<byte> AsBytes(this System.Utf8String? text, int start) { throw null; }
+        public static System.ReadOnlySpan<byte> AsBytes(this System.Utf8String? text, int start, int length) { throw null; }
+        public static System.ReadOnlyMemory<System.Char8> AsMemory(this System.Utf8String? text) { throw null; }
+        public static System.ReadOnlyMemory<System.Char8> AsMemory(this System.Utf8String? text, System.Index startIndex) { throw null; }
+        public static System.ReadOnlyMemory<System.Char8> AsMemory(this System.Utf8String? text, int start) { throw null; }
+        public static System.ReadOnlyMemory<System.Char8> AsMemory(this System.Utf8String? text, int start, int length) { throw null; }
+        public static System.ReadOnlyMemory<System.Char8> AsMemory(this System.Utf8String? text, System.Range range) { throw null; }
+        public static System.ReadOnlyMemory<byte> AsMemoryBytes(this System.Utf8String? text) { throw null; }
+        public static System.ReadOnlyMemory<byte> AsMemoryBytes(this System.Utf8String? text, System.Index startIndex) { throw null; }
+        public static System.ReadOnlyMemory<byte> AsMemoryBytes(this System.Utf8String? text, int start) { throw null; }
+        public static System.ReadOnlyMemory<byte> AsMemoryBytes(this System.Utf8String? text, int start, int length) { throw null; }
+        public static System.ReadOnlyMemory<byte> AsMemoryBytes(this System.Utf8String? text, System.Range range) { throw null; }
         public static System.Text.Utf8Span AsSpan(this System.Utf8String? text) { throw null; }
         public static System.Text.Utf8Span AsSpan(this System.Utf8String? text, int start) { throw null; }
         public static System.Text.Utf8Span AsSpan(this System.Utf8String? text, int start, int length) { throw null; }
         public static System.Utf8String ToUtf8String(this System.Text.Rune rune) { throw null; }
     }
-    public sealed partial class Utf8String : System.IEquatable<System.Utf8String>
+    public sealed partial class Utf8String : System.IComparable<System.Utf8String?>,
+#nullable disable
+        System.IEquatable<System.Utf8String>
+#nullable restore
     {
         public static readonly System.Utf8String Empty;
         [System.CLSCompliantAttribute(false)]
@@ -81,38 +84,185 @@ namespace System
         public Utf8String(System.ReadOnlySpan<byte> value) { }
         public Utf8String(System.ReadOnlySpan<char> value) { }
         public Utf8String(string value) { }
-        public System.Char8 this[int index] { get { throw null; } }
+        public ByteEnumerable Bytes { get { throw null; } }
+        public CharEnumerable Chars { get { throw null; } }
         public int Length { get { throw null; } }
+        public RuneEnumerable Runes { get { throw null; } }
         public static bool AreEquivalent(System.Utf8String? utf8Text, string? utf16Text) { throw null; }
         public static bool AreEquivalent(System.Text.Utf8Span utf8Text, System.ReadOnlySpan<char> utf16Text) { throw null; }
         public static bool AreEquivalent(System.ReadOnlySpan<byte> utf8Text, System.ReadOnlySpan<char> utf16Text) { throw null; }
+        public int CompareTo(System.Utf8String? other) { throw null; }
+        public int CompareTo(System.Utf8String? other, System.StringComparison comparison) { throw null; }
         public bool Contains(char value) { throw null; }
+        public bool Contains(char value, System.StringComparison comparison) { throw null; }
         public bool Contains(System.Text.Rune value) { throw null; }
+        public bool Contains(System.Text.Rune value, System.StringComparison comparison) { throw null; }
+        public bool Contains(System.Utf8String value) { throw null; }
+        public bool Contains(System.Utf8String value, System.StringComparison comparison) { throw null; }
+        public static System.Utf8String Create<TState>(int length, TState state, System.Buffers.SpanAction<byte, TState> action) { throw null; }
+        public static System.Utf8String CreateFromRelaxed(System.ReadOnlySpan<byte> buffer) { throw null; }
+        public static System.Utf8String CreateFromRelaxed(System.ReadOnlySpan<char> buffer) { throw null; }
+        public static System.Utf8String CreateRelaxed<TState>(int length, TState state, System.Buffers.SpanAction<byte, TState> action) { throw null; }
         public bool EndsWith(char value) { throw null; }
+        public bool EndsWith(char value, System.StringComparison comparison) { throw null; }
         public bool EndsWith(System.Text.Rune value) { throw null; }
+        public bool EndsWith(System.Text.Rune value, System.StringComparison comparison) { throw null; }
+        public bool EndsWith(System.Utf8String value) { throw null; }
+        public bool EndsWith(System.Utf8String value, System.StringComparison comparison) { throw null; }
         public override bool Equals(object? obj) { throw null; }
-        public bool Equals(System.Utf8String value) { throw null; }
-        public static bool Equals(System.Utf8String left, System.Utf8String right) { throw null; }
+        public static bool Equals(System.Utf8String? a, System.Utf8String? b, System.StringComparison comparison) { throw null; }
+        public static bool Equals(System.Utf8String? left, System.Utf8String? right) { throw null; }
+        public bool Equals(System.Utf8String? value) { throw null; }
+        public bool Equals(System.Utf8String? value, System.StringComparison comparison) { throw null; }
         public override int GetHashCode() { throw null; }
+        public int GetHashCode(System.StringComparison comparison) { throw null; }
         [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
         public ref readonly byte GetPinnableReference() { throw null; }
-        public int IndexOf(char value) { throw null; }
-        public int IndexOf(System.Text.Rune value) { throw null; }
-        public static bool IsNullOrEmpty(System.Utf8String value) { throw null; }
-        public static bool operator ==(System.Utf8String left, System.Utf8String right) { throw null; }
-        public static explicit operator System.ReadOnlySpan<byte>(System.Utf8String value) { throw null; }
-        public static implicit operator System.ReadOnlySpan<System.Char8>(System.Utf8String value) { throw null; }
-        public static implicit operator System.Text.Utf8Span(System.Utf8String value) { throw null; }
-        public static bool operator !=(System.Utf8String left, System.Utf8String right) { throw null; }
-        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
-        public System.Utf8String Slice(int startIndex, int length) { throw null; }
+        public static implicit operator System.Text.Utf8Span(System.Utf8String? value) { throw null; }
+        public bool IsAscii() { throw null; }
+        public bool IsNormalized(System.Text.NormalizationForm normalizationForm = System.Text.NormalizationForm.FormC) { throw null; }
+        public static bool IsNullOrEmpty([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(false)] System.Utf8String? value) { throw null; }
+        public static bool IsNullOrWhiteSpace([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(false)] System.Utf8String? value) { throw null; }
+        public System.Utf8String Normalize(System.Text.NormalizationForm normalizationForm = System.Text.NormalizationForm.FormC) { throw null; }
+        public static bool operator !=(System.Utf8String? left, System.Utf8String? right) { throw null; }
+        public static bool operator ==(System.Utf8String? left, System.Utf8String? right) { throw null; }
+        public SplitResult Split(char separator, System.Utf8StringSplitOptions options = System.Utf8StringSplitOptions.None) { throw null; }
+        public SplitResult Split(System.Text.Rune separator, System.Utf8StringSplitOptions options = System.Utf8StringSplitOptions.None) { throw null; }
+        public SplitResult Split(System.Utf8String separator, System.Utf8StringSplitOptions options = System.Utf8StringSplitOptions.None) { throw null; }
+        public SplitOnResult SplitOn(char separator) { throw null; }
+        public SplitOnResult SplitOn(char separator, System.StringComparison comparisonType) { throw null; }
+        public SplitOnResult SplitOn(System.Text.Rune separator) { throw null; }
+        public SplitOnResult SplitOn(System.Text.Rune separator, System.StringComparison comparisonType) { throw null; }
+        public SplitOnResult SplitOn(System.Utf8String separator) { throw null; }
+        public SplitOnResult SplitOn(System.Utf8String separator, System.StringComparison comparisonType) { throw null; }
+        public SplitOnResult SplitOnLast(char separator) { throw null; }
+        public SplitOnResult SplitOnLast(char separator, System.StringComparison comparisonType) { throw null; }
+        public SplitOnResult SplitOnLast(System.Text.Rune separator) { throw null; }
+        public SplitOnResult SplitOnLast(System.Text.Rune separator, System.StringComparison comparisonType) { throw null; }
+        public SplitOnResult SplitOnLast(System.Utf8String separator) { throw null; }
+        public SplitOnResult SplitOnLast(System.Utf8String separator, System.StringComparison comparisonType) { throw null; }
         public bool StartsWith(char value) { throw null; }
+        public bool StartsWith(char value, System.StringComparison comparison) { throw null; }
         public bool StartsWith(System.Text.Rune value) { throw null; }
-        public System.Utf8String Substring(int startIndex) { throw null; }
-        public System.Utf8String Substring(int startIndex, int length) { throw null; }
+        public bool StartsWith(System.Text.Rune value, System.StringComparison comparison) { throw null; }
+        public bool StartsWith(System.Utf8String value) { throw null; }
+        public bool StartsWith(System.Utf8String value, System.StringComparison comparison) { throw null; }
+        public System.Utf8String this[System.Range range] { get { throw null; } }
         public byte[] ToByteArray() { throw null; }
-        public byte[] ToByteArray(int startIndex, int length) { throw null; }
+        public char[] ToCharArray() { throw null; }
+        public System.Utf8String ToLower(System.Globalization.CultureInfo culture) { throw null; }
+        public System.Utf8String ToLowerInvariant() { throw null; }
         public override string ToString() { throw null; }
+        public System.Utf8String ToUpper(System.Globalization.CultureInfo culture) { throw null; }
+        public System.Utf8String ToUpperInvariant() { throw null; }
+        public System.Utf8String Trim() { throw null; }
+        public System.Utf8String TrimEnd() { throw null; }
+        public System.Utf8String TrimStart() { throw null; }
+        public static bool TryCreateFrom(System.ReadOnlySpan<byte> buffer, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Utf8String? value) { throw null; }
+        public static bool TryCreateFrom(System.ReadOnlySpan<char> buffer, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Utf8String? value) { throw null; }
+        public bool TryFind(char value, out System.Range range) { throw null; }
+        public bool TryFind(char value, System.StringComparison comparisonType, out System.Range range) { throw null; }
+        public bool TryFind(System.Text.Rune value, out System.Range range) { throw null; }
+        public bool TryFind(System.Text.Rune value, System.StringComparison comparisonType, out System.Range range) { throw null; }
+        public bool TryFind(System.Utf8String value, out System.Range range) { throw null; }
+        public bool TryFind(System.Utf8String value, System.StringComparison comparisonType, out System.Range range) { throw null; }
+        public bool TryFindLast(char value, out System.Range range) { throw null; }
+        public bool TryFindLast(char value, System.StringComparison comparisonType, out System.Range range) { throw null; }
+        public bool TryFindLast(System.Text.Rune value, out System.Range range) { throw null; }
+        public bool TryFindLast(System.Text.Rune value, System.StringComparison comparisonType, out System.Range range) { throw null; }
+        public bool TryFindLast(System.Utf8String value, out System.Range range) { throw null; }
+        public bool TryFindLast(System.Utf8String value, System.StringComparison comparisonType, out System.Range range) { throw null; }
+        public static System.Utf8String UnsafeCreateWithoutValidation(System.ReadOnlySpan<byte> utf8Contents) { throw null; }
+        public static System.Utf8String UnsafeCreateWithoutValidation<TState>(int length, TState state, System.Buffers.SpanAction<byte, TState> action) { throw null; }
+        public readonly partial struct ByteEnumerable : System.Collections.Generic.IEnumerable<byte>
+        {
+            private readonly object _dummy;
+            public Enumerator GetEnumerator() { throw null; }
+            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+            System.Collections.Generic.IEnumerator<byte> System.Collections.Generic.IEnumerable<byte>.GetEnumerator() { throw null; }
+            public struct Enumerator : System.Collections.Generic.IEnumerator<byte>
+            {
+                private readonly object _dummy;
+                private readonly int _dummyPrimitive;
+                public byte Current { get { throw null; } }
+                public bool MoveNext() { throw null; }
+                void System.IDisposable.Dispose() { }
+                object System.Collections.IEnumerator.Current { get { throw null; } }
+                void System.Collections.IEnumerator.Reset() { }
+            }
+        }
+        public readonly partial struct CharEnumerable : System.Collections.Generic.IEnumerable<char>
+        {
+            private readonly object _dummy;
+            public Enumerator GetEnumerator() { throw null; }
+            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+            System.Collections.Generic.IEnumerator<char> System.Collections.Generic.IEnumerable<char>.GetEnumerator() { throw null; }
+            public struct Enumerator : System.Collections.Generic.IEnumerator<char>
+            {
+                private readonly object _dummy;
+                private readonly int _dummyPrimitive;
+                public char Current { get { throw null; } }
+                public bool MoveNext() { throw null; }
+                void System.IDisposable.Dispose() { }
+                object System.Collections.IEnumerator.Current { get { throw null; } }
+                void System.Collections.IEnumerator.Reset() { }
+            }
+        }
+        public readonly partial struct RuneEnumerable : System.Collections.Generic.IEnumerable<System.Text.Rune>
+        {
+            private readonly object _dummy;
+            public Enumerator GetEnumerator() { throw null; }
+            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+            System.Collections.Generic.IEnumerator<System.Text.Rune> System.Collections.Generic.IEnumerable<System.Text.Rune>.GetEnumerator() { throw null; }
+            public struct Enumerator : System.Collections.Generic.IEnumerator<System.Text.Rune>
+            {
+                private readonly object _dummy;
+                private readonly int _dummyPrimitive;
+                public System.Text.Rune Current { get { throw null; } }
+                public bool MoveNext() { throw null; }
+                void System.IDisposable.Dispose() { }
+                object System.Collections.IEnumerator.Current { get { throw null; } }
+                void System.Collections.IEnumerator.Reset() { }
+            }
+        }
+        public readonly struct SplitResult : System.Collections.Generic.IEnumerable<Utf8String?>
+        {
+            private readonly object _dummy;
+            [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
+            public void Deconstruct(out System.Utf8String? item1, out System.Utf8String? item2) { throw null; }
+            [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
+            public void Deconstruct(out System.Utf8String? item1, out System.Utf8String? item2, out System.Utf8String? item3) { throw null; }
+            [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
+            public void Deconstruct(out System.Utf8String? item1, out System.Utf8String? item2, out System.Utf8String? item3, out System.Utf8String? item4) { throw null; }
+            [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
+            public void Deconstruct(out System.Utf8String? item1, out System.Utf8String? item2, out System.Utf8String? item3, out System.Utf8String? item4, out System.Utf8String? item5) { throw null; }
+            [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
+            public void Deconstruct(out System.Utf8String? item1, out System.Utf8String? item2, out System.Utf8String? item3, out System.Utf8String? item4, out System.Utf8String? item5, out System.Utf8String? item6) { throw null; }
+            [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
+            public void Deconstruct(out System.Utf8String? item1, out System.Utf8String? item2, out System.Utf8String? item3, out System.Utf8String? item4, out System.Utf8String? item5, out System.Utf8String? item6, out System.Utf8String? item7) { throw null; }
+            [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
+            public void Deconstruct(out System.Utf8String? item1, out System.Utf8String? item2, out System.Utf8String? item3, out System.Utf8String? item4, out System.Utf8String? item5, out System.Utf8String? item6, out System.Utf8String? item7, out System.Utf8String? item8) { throw null; }
+            public Enumerator GetEnumerator() { throw null; }
+            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+            System.Collections.Generic.IEnumerator<System.Utf8String?> System.Collections.Generic.IEnumerable<System.Utf8String?>.GetEnumerator() { throw null; }
+            public struct Enumerator : System.Collections.Generic.IEnumerator<System.Utf8String?>
+            {
+                private readonly object _dummy;
+                public System.Utf8String? Current { get { throw null; } }
+                public bool MoveNext() { throw null; }
+                void System.IDisposable.Dispose() { }
+                object? System.Collections.IEnumerator.Current { get { throw null; } }
+                void System.Collections.IEnumerator.Reset() { throw null; }
+            }
+        }
+        public readonly struct SplitOnResult
+        {
+            private readonly object _dummy;
+            public System.Utf8String? After { get { throw null; } }
+            public System.Utf8String Before { get { throw null; } }
+            [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
+            public void Deconstruct(out System.Utf8String before, out System.Utf8String? after) { throw null; }
+        }
     }
     [System.FlagsAttribute]
     public enum Utf8StringSplitOptions
@@ -144,6 +294,7 @@ namespace System.Text
         public CharEnumerable Chars { get { throw null; } }
         public static System.Text.Utf8Span Empty { get { throw null; } }
         public bool IsEmpty { get { throw null; } }
+        public int Length { get { throw null; } }
         public RuneEnumerable Runes { get { throw null; } }
         public int CompareTo(System.Text.Utf8Span other) { throw null; }
         public int CompareTo(System.Text.Utf8Span other, System.StringComparison comparison) { throw null; }
@@ -202,6 +353,7 @@ namespace System.Text
         public System.Text.Utf8Span Trim() { throw null; }
         public System.Text.Utf8Span TrimEnd() { throw null; }
         public System.Text.Utf8Span TrimStart() { throw null; }
+        public byte[] ToByteArray() { throw null; }
         public char[] ToCharArray() { throw null; }
         public int ToChars(System.Span<char> destination) { throw null; }
         public System.Utf8String ToLower(System.Globalization.CultureInfo culture) { throw null; }
index 5a339db..39bf7eb 100644 (file)
@@ -25,14 +25,14 @@ namespace System.IO
 
         public override bool CanWrite => false;
 
-        public override long Length => _content.AsBytes().Length;
+        public override long Length => _content.Length;
 
         public override long Position
         {
             get => _position;
             set
             {
-                if ((ulong)value > (uint)_content.AsBytes().Length)
+                if ((ulong)value > (uint)_content.Length)
                 {
                     throw new ArgumentOutOfRangeException(nameof(value));
                 }
@@ -84,7 +84,7 @@ namespace System.IO
         public override int ReadByte()
         {
             int position = _position;
-            if ((uint)position >= (uint)_content.AsBytes().Length)
+            if ((uint)position >= (uint)_content.Length)
             {
                 return -1;
             }
@@ -103,13 +103,13 @@ namespace System.IO
                     offset += _position;
                     break;
                 case SeekOrigin.End:
-                    offset += _content.AsBytes().Length;
+                    offset += _content.Length;
                     break;
                 default:
                     throw new ArgumentOutOfRangeException(nameof(origin));
             }
 
-            if ((ulong)offset > (uint)_content.AsBytes().Length)
+            if ((ulong)offset > (uint)_content.Length)
             {
                 throw new ArgumentOutOfRangeException(nameof(offset));
             }
index 2c350f0..18b36ee 100644 (file)
@@ -48,7 +48,7 @@ namespace System.Net.Http
 
         protected override bool TryComputeLength(out long length)
         {
-            length = _content.AsBytes().Length;
+            length = _content.Length;
             return true;
         }
     }
index deb16c3..e21b2a3 100644 (file)
     <Compile Include="System\Utf8SpanTests.Searching.TestData.cs" />
     <Compile Include="System\Utf8SpanTests.TestData.cs" />
     <Compile Include="System\Utf8StringTests.cs" />
+    <Compile Include="System\Utf8StringTests.Comparison.cs" />
+    <Compile Include="System\Utf8StringTests.Conversion.cs" />
     <Compile Include="System\Utf8StringTests.Ctor.cs" />
+    <Compile Include="System\Utf8StringTests.Enumeration.cs" />
+    <Compile Include="System\Utf8StringTests.Manipulation.cs" />
     <Compile Include="System\Utf8StringTests.Searching.cs" />
-    <Compile Include="System\Utf8StringTests.Substring.cs" />
     <Compile Include="Xunit\SpanAssert.cs" />
     <Compile Include="System\Utf8TestUtilities.cs" />
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
index 604b02e..13d5f4c 100644 (file)
@@ -27,7 +27,7 @@ namespace System.Tests
             // Finally, a span wrapping data should become a span wrapping that same data.
 
             Utf8String theString = u8("Hello");
-            Assert.True(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in theString.GetPinnableReference()), 5) == ((ReadOnlySpan<Char8>)theString).AsBytes());
+            Assert.True(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in theString.GetPinnableReference()), 5) == (theString.AsMemory().Span).AsBytes());
         }
 
         [Fact]
@@ -170,7 +170,7 @@ namespace System.Tests
             Utf8String theString = u8("Hello");
 
             Assert.True(Unsafe.AreSame(ref Unsafe.AsRef(in theString.GetPinnableReference()), ref Unsafe.AsRef(in theString.AsSpan().GetPinnableReference())));
-            Assert.Equal(5, theString.AsSpan().Bytes.Length);
+            Assert.Equal(5, theString.AsSpan().Length);
         }
 
         [Fact]
index 01721a4..bd18fdd 100644 (file)
@@ -33,16 +33,16 @@ namespace System.Text.Tests
 
             // Normalize to byte arrays which are too small, expect -1 (failure)
 
-            Assert.Equal(-1, utf8Source.Normalize(new byte[utf8Normalized.GetByteLength() - 1], normalizationForm));
+            Assert.Equal(-1, utf8Source.Normalize(new byte[utf8Normalized.Length - 1], normalizationForm));
 
             // Normalize to byte arrays which are the correct length, expect success,
             // then compare buffer contents for ordinal equality.
 
-            foreach (int bufferLength in new int[] { utf8Normalized.GetByteLength() /* just right */, utf8Normalized.GetByteLength() + 1 /* with extra room */})
+            foreach (int bufferLength in new int[] { utf8Normalized.Length /* just right */, utf8Normalized.Length + 1 /* with extra room */})
             {
                 byte[] dest = new byte[bufferLength];
-                Assert.Equal(utf8Normalized.GetByteLength(), utf8Source.Normalize(dest, normalizationForm));
-                Utf8Span normalizedSpan = Utf8Span.UnsafeCreateWithoutValidation(dest[..utf8Normalized.GetByteLength()]);
+                Assert.Equal(utf8Normalized.Length, utf8Source.Normalize(dest, normalizationForm));
+                Utf8Span normalizedSpan = Utf8Span.UnsafeCreateWithoutValidation(dest[..utf8Normalized.Length]);
                 Assert.True(utf8Normalized.AsSpan() == normalizedSpan); // ordinal equality
                 Assert.True(normalizedSpan.IsNormalized(normalizationForm));
             }
@@ -75,9 +75,9 @@ namespace System.Text.Tests
 
                 // Next, try the non-allocating APIs with too small a buffer
 
-                if (expectedUtf8.GetByteLength() > 0)
+                if (expectedUtf8.Length > 0)
                 {
-                    byte[] bufferTooSmall = new byte[expectedUtf8.GetByteLength() - 1];
+                    byte[] bufferTooSmall = new byte[expectedUtf8.Length - 1];
 
                     if (culture is null)
                     {
@@ -91,17 +91,17 @@ namespace System.Text.Tests
 
                 // Then the non-allocating APIs with a properly sized buffer
 
-                foreach (int bufferSize in new[] { expectedUtf8.GetByteLength(), expectedUtf8.GetByteLength() + 1 })
+                foreach (int bufferSize in new[] { expectedUtf8.Length, expectedUtf8.Length + 1 })
                 {
-                    byte[] buffer = new byte[expectedUtf8.GetByteLength()];
+                    byte[] buffer = new byte[expectedUtf8.Length];
 
                     if (culture is null)
                     {
-                        Assert.Equal(expectedUtf8.GetByteLength(), inputSpan.ToLowerInvariant(buffer));
+                        Assert.Equal(expectedUtf8.Length, inputSpan.ToLowerInvariant(buffer));
                     }
                     else
                     {
-                        Assert.Equal(expectedUtf8.GetByteLength(), inputSpan.ToLower(buffer, culture));
+                        Assert.Equal(expectedUtf8.Length, inputSpan.ToLower(buffer, culture));
                     }
 
                     Assert.True(expectedUtf8.AsBytes().SequenceEqual(buffer));
@@ -141,9 +141,9 @@ namespace System.Text.Tests
 
                 // Next, try the non-allocating APIs with too small a buffer
 
-                if (expectedUtf8.GetByteLength() > 0)
+                if (expectedUtf8.Length > 0)
                 {
-                    byte[] bufferTooSmall = new byte[expectedUtf8.GetByteLength() - 1];
+                    byte[] bufferTooSmall = new byte[expectedUtf8.Length - 1];
 
                     if (culture is null)
                     {
@@ -157,17 +157,17 @@ namespace System.Text.Tests
 
                 // Then the non-allocating APIs with a properly sized buffer
 
-                foreach (int bufferSize in new[] { expectedUtf8.GetByteLength(), expectedUtf8.GetByteLength() + 1 })
+                foreach (int bufferSize in new[] { expectedUtf8.Length, expectedUtf8.Length + 1 })
                 {
-                    byte[] buffer = new byte[expectedUtf8.GetByteLength()];
+                    byte[] buffer = new byte[expectedUtf8.Length];
 
                     if (culture is null)
                     {
-                        Assert.Equal(expectedUtf8.GetByteLength(), inputSpan.ToUpperInvariant(buffer));
+                        Assert.Equal(expectedUtf8.Length, inputSpan.ToUpperInvariant(buffer));
                     }
                     else
                     {
-                        Assert.Equal(expectedUtf8.GetByteLength(), inputSpan.ToUpper(buffer, culture));
+                        Assert.Equal(expectedUtf8.Length, inputSpan.ToUpper(buffer, culture));
                     }
 
                     Assert.True(expectedUtf8.AsBytes().SequenceEqual(buffer));
index 725a775..5233c31 100644 (file)
@@ -31,7 +31,7 @@ namespace System.Text.Tests
             Assert.True(span.IsEmpty);
             Assert.Equal(IntPtr.Zero, (IntPtr)(void*)Unsafe.AsPointer(ref Unsafe.AsRef(in span.GetPinnableReference())));
             Assert.True(Unsafe.AreSame(ref Unsafe.AsRef(in str.GetPinnableReference()), ref MemoryMarshal.GetReference(span.Bytes)));
-            Assert.Equal(0, span.Bytes.Length);
+            Assert.Equal(0, span.Length);
         }
 
         [Fact]
@@ -50,7 +50,7 @@ namespace System.Text.Tests
             Assert.False(span.IsEmpty);
             Assert.True(Unsafe.AreSame(ref Unsafe.AsRef(in str.GetPinnableReference()), ref Unsafe.AsRef(in span.GetPinnableReference())));
             Assert.True(Unsafe.AreSame(ref Unsafe.AsRef(in str.GetPinnableReference()), ref MemoryMarshal.GetReference(span.Bytes)));
-            Assert.Equal(6, span.Bytes.Length);
+            Assert.Equal(6, span.Length);
         }
 
         [Fact]
@@ -70,7 +70,7 @@ namespace System.Text.Tests
             Assert.True(span.IsEmpty);
             Assert.Equal(IntPtr.Zero, (IntPtr)(void*)Unsafe.AsPointer(ref Unsafe.AsRef(in span.GetPinnableReference())));
             Assert.Equal(IntPtr.Zero, (IntPtr)(void*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(span.Bytes)));
-            Assert.Equal(0, span.Bytes.Length);
+            Assert.Equal(0, span.Length);
         }
 
         [Fact]
@@ -89,7 +89,7 @@ namespace System.Text.Tests
             Assert.False(span.IsEmpty);
             Assert.True(Unsafe.AreSame(ref Unsafe.AsRef(in original.GetPinnableReference()), ref Unsafe.AsRef(in span.GetPinnableReference())));
             Assert.True(Unsafe.AreSame(ref Unsafe.AsRef(in original.GetPinnableReference()), ref MemoryMarshal.GetReference(span.Bytes)));
-            Assert.Equal(5, span.Bytes.Length);
+            Assert.Equal(5, span.Length);
         }
 
         [Fact]
@@ -109,7 +109,7 @@ namespace System.Text.Tests
             Assert.True(span.IsEmpty);
             Assert.Equal(IntPtr.Zero, (IntPtr)(void*)Unsafe.AsPointer(ref Unsafe.AsRef(in span.GetPinnableReference())));
             Assert.True(Unsafe.AreSame(ref MemoryMarshal.GetReference(original), ref MemoryMarshal.GetReference(span.Bytes)));
-            Assert.Equal(0, span.Bytes.Length);
+            Assert.Equal(0, span.Length);
         }
 
         [Fact]
@@ -129,7 +129,7 @@ namespace System.Text.Tests
             Assert.True(span.IsEmpty);
             Assert.Equal(IntPtr.Zero, (IntPtr)(void*)Unsafe.AsPointer(ref Unsafe.AsRef(in span.GetPinnableReference())));
             Assert.Equal(IntPtr.Zero, (IntPtr)(void*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(span.Bytes)));
-            Assert.Equal(0, span.Bytes.Length);
+            Assert.Equal(0, span.Length);
         }
     }
 }
index 64b85a9..a4875e6 100644 (file)
@@ -47,12 +47,6 @@ namespace System.Text.Tests
         }
 
         [Fact]
-        public static void RunesProperty_FromEmpty()
-        {
-            Assert.False(Utf8Span.Empty.Runes.GetEnumerator().MoveNext());
-        }
-
-        [Fact]
         public static void RunesProperty_FromData()
         {
             using BoundedUtf8Span boundedSpan = new BoundedUtf8Span("\U00000012\U00000123\U00001234\U00101234\U00000012\U00000123\U00001234\U00101234");
@@ -78,5 +72,11 @@ namespace System.Text.Tests
             Assert.Equal(new Rune(0x101234), runesEnumerator.Current);
             Assert.False(runesEnumerator.MoveNext());
         }
+
+        [Fact]
+        public static void RunesProperty_FromEmpty()
+        {
+            Assert.False(Utf8Span.Empty.Runes.GetEnumerator().MoveNext());
+        }
     }
 }
index cc42dbb..7135b13 100644 (file)
@@ -165,7 +165,7 @@ namespace System.Text.Tests
         {
             using BoundedUtf8Span boundedSpan = new BoundedUtf8Span(source.AsBytes());
             Utf8Span span = boundedSpan.Span;
-            int totalSpanLengthInBytes = span.Bytes.Length;
+            int totalSpanLengthInBytes = span.Length;
             source = null; // to avoid inadvertently using this for the remainder of the method
 
             // First, run the split with default options and make sure the ranges are equivalent
index 7df9259..a46da8a 100644 (file)
@@ -12,6 +12,10 @@ using ustring = System.Utf8String;
 
 namespace System.Text.Tests
 {
+    /*
+     * Please keep these tests in sync with those in Utf8StringTests.Searching.cs.
+     */
+
     public unsafe partial class Utf8SpanTests
     {
         [Theory]
@@ -29,7 +33,7 @@ namespace System.Text.Tests
 
             if (wasFound)
             {
-                AssertRangesEqual(searchSpan.Bytes.Length, expectedForwardMatch.Value, actualForwardMatch);
+                AssertRangesEqual(searchSpan.Length, expectedForwardMatch.Value, actualForwardMatch);
             }
 
             // Also check Contains / StartsWith / SplitOn
@@ -56,7 +60,7 @@ namespace System.Text.Tests
 
             if (wasFound)
             {
-                AssertRangesEqual(searchSpan.Bytes.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+                AssertRangesEqual(searchSpan.Length, expectedBackwardMatch.Value, actualBackwardMatch);
             }
 
             // Also check EndsWith / SplitOnLast
@@ -99,7 +103,7 @@ namespace System.Text.Tests
 
                 if (wasFound)
                 {
-                    AssertRangesEqual(searchSpan.Bytes.Length, expectedForwardMatch.Value, actualForwardMatch);
+                    AssertRangesEqual(searchSpan.Length, expectedForwardMatch.Value, actualForwardMatch);
                 }
 
                 // Also check Contains / StartsWith / SplitOn
@@ -126,7 +130,7 @@ namespace System.Text.Tests
 
                 if (wasFound)
                 {
-                    AssertRangesEqual(searchSpan.Bytes.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+                    AssertRangesEqual(searchSpan.Length, expectedBackwardMatch.Value, actualBackwardMatch);
                 }
 
                 // Also check EndsWith / SplitOnLast
@@ -162,7 +166,7 @@ namespace System.Text.Tests
 
             if (wasFound)
             {
-                AssertRangesEqual(searchSpan.Bytes.Length, expectedForwardMatch.Value, actualForwardMatch);
+                AssertRangesEqual(searchSpan.Length, expectedForwardMatch.Value, actualForwardMatch);
             }
 
             // Also check Contains / StartsWith / SplitOn
@@ -189,7 +193,7 @@ namespace System.Text.Tests
 
             if (wasFound)
             {
-                AssertRangesEqual(searchSpan.Bytes.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+                AssertRangesEqual(searchSpan.Length, expectedBackwardMatch.Value, actualBackwardMatch);
             }
 
             // Also check EndsWith / SplitOnLast
@@ -232,7 +236,7 @@ namespace System.Text.Tests
 
                 if (wasFound)
                 {
-                    AssertRangesEqual(searchSpan.Bytes.Length, expectedForwardMatch.Value, actualForwardMatch);
+                    AssertRangesEqual(searchSpan.Length, expectedForwardMatch.Value, actualForwardMatch);
                 }
 
                 // Also check Contains / StartsWith / SplitOn
@@ -259,7 +263,7 @@ namespace System.Text.Tests
 
                 if (wasFound)
                 {
-                    AssertRangesEqual(searchSpan.Bytes.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+                    AssertRangesEqual(searchSpan.Length, expectedBackwardMatch.Value, actualBackwardMatch);
                 }
 
                 // Also check EndsWith / SplitOnLast
@@ -295,7 +299,7 @@ namespace System.Text.Tests
 
             if (wasFound)
             {
-                AssertRangesEqual(searchSpan.Bytes.Length, expectedForwardMatch.Value, actualForwardMatch);
+                AssertRangesEqual(searchSpan.Length, expectedForwardMatch.Value, actualForwardMatch);
             }
 
             // Also check Contains / StartsWith / SplitOn
@@ -322,7 +326,7 @@ namespace System.Text.Tests
 
             if (wasFound)
             {
-                AssertRangesEqual(searchSpan.Bytes.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+                AssertRangesEqual(searchSpan.Length, expectedBackwardMatch.Value, actualBackwardMatch);
             }
 
             // Also check EndsWith / SplitOnLast
@@ -365,7 +369,7 @@ namespace System.Text.Tests
 
                 if (wasFound)
                 {
-                    AssertRangesEqual(searchSpan.Bytes.Length, expectedForwardMatch.Value, actualForwardMatch);
+                    AssertRangesEqual(searchSpan.Length, expectedForwardMatch.Value, actualForwardMatch);
                 }
 
                 // Also check Contains / StartsWith / SplitOn
@@ -392,7 +396,7 @@ namespace System.Text.Tests
 
                 if (wasFound)
                 {
-                    AssertRangesEqual(searchSpan.Bytes.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+                    AssertRangesEqual(searchSpan.Length, expectedBackwardMatch.Value, actualBackwardMatch);
                 }
 
                 // Also check EndsWith / SplitOnLast
index 290eb2d..e5ee9b3 100644 (file)
@@ -97,11 +97,14 @@ namespace System.Text.Tests
             }
             else if (searchTerm is ustring ustr)
             {
-                var asString = ustr.ToString();
-                if (asString.Length == 1)
+                var enumerator = ustr.Chars.GetEnumerator();
+                if (enumerator.MoveNext())
                 {
-                    parsed = asString[0];
-                    return true;
+                    parsed = enumerator.Current;
+                    if (!enumerator.MoveNext())
+                    {
+                        return true;
+                    }
                 }
             }
 
@@ -131,7 +134,7 @@ namespace System.Text.Tests
             else if (searchTerm is ustring ustr)
             {
                 if (Rune.DecodeFromUtf8(ustr.AsBytes(), out parsed, out int bytesConsumed) == OperationStatus.Done
-                    && bytesConsumed == ustr.GetByteLength())
+                    && bytesConsumed == ustr.Length)
                 {
                     return true;
                 }
@@ -158,10 +161,8 @@ namespace System.Text.Tests
             }
             else if (searchTerm is string str)
             {
-                ustring asUtf8 = new ustring(str);
-                if (asUtf8.ToString() == str) // make sure this round-trips properly
+                if (ustring.TryCreateFrom(str, out parsed))
                 {
-                    parsed = asUtf8;
                     return true;
                 }
             }
index 25579c3..91d19de 100644 (file)
@@ -53,7 +53,7 @@ namespace System.Text.Tests
             Assert.True(span.IsEmpty);
             Assert.Equal(IntPtr.Zero, (IntPtr)(void*)Unsafe.AsPointer(ref Unsafe.AsRef(in span.GetPinnableReference())));
             Assert.Equal(IntPtr.Zero, (IntPtr)(void*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(span.Bytes)));
-            Assert.Equal(0, span.Bytes.Length);
+            Assert.Equal(0, span.Length);
         }
 
         [Fact]
@@ -221,9 +221,9 @@ namespace System.Text.Tests
 
             // Now ensure the slice was correctly produced by comparing the references directly.
 
-            (int offset, int length) = range.GetOffsetAndLength(originalSpan.Bytes.Length);
+            (int offset, int length) = range.GetOffsetAndLength(originalSpan.Length);
             Assert.True(Unsafe.AreSame(ref startOfSlicedSpan, ref Unsafe.Add(ref startOfOriginalSpan, offset)));
-            Assert.Equal(length, slicedSpan.Bytes.Length);
+            Assert.Equal(length, slicedSpan.Length);
         }
 
         [Theory]
diff --git a/src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Comparison.cs b/src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Comparison.cs
new file mode 100644 (file)
index 0000000..45fc859
--- /dev/null
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Xunit;
+
+using static System.Tests.Utf8TestUtilities;
+
+namespace System.Tests
+{
+    public unsafe partial class Utf8StringTests
+    {
+        [Fact]
+        public static void AreEquivalent_Utf8StringAndString_NullHandling()
+        {
+            Assert.True(Utf8String.AreEquivalent((Utf8String)null, (string)null));
+            Assert.False(Utf8String.AreEquivalent(Utf8String.Empty, (string)null));
+            Assert.False(Utf8String.AreEquivalent((Utf8String)null, string.Empty));
+            Assert.True(Utf8String.AreEquivalent(Utf8String.Empty, string.Empty));
+        }
+
+        [Theory]
+        [InlineData("Hello", "hello", false)]
+        [InlineData("hello", "hello", true)]
+        [InlineData("hello", "helloo", false)]
+        [InlineData("hellooo", "helloo", false)]
+        [InlineData("encyclopaedia", "encyclopaedia", true)]
+        [InlineData("encyclopaedia", "encyclop\u00e6dia", false)]
+        [InlineData("encyclop\u00e6dia", "encyclop\u00e6dia", true)]
+        public static void AreEquivalent_Tests(string utf8Input, string utf16Input, bool expected)
+        {
+            Utf8String asUtf8 = u8(utf8Input);
+
+            // Call all three overloads
+
+            Assert.Equal(expected, Utf8String.AreEquivalent(asUtf8, utf16Input));
+            Assert.Equal(expected, Utf8String.AreEquivalent(asUtf8.AsSpan(), utf16Input.AsSpan()));
+            Assert.Equal(expected, Utf8String.AreEquivalent(asUtf8.AsBytes(), utf16Input.AsSpan()));
+        }
+
+        [Theory]
+        [InlineData(new byte[] { 0xED, 0xA0, 0x80 }, new char[] { '\uD800' })] // don't support "wobbly" UTF-8
+        [InlineData(new byte[] { 0xED, 0xA0, 0x80, 0xED, 0xBF, 0xBF }, new char[] { '\uD800', '\uDFFF' })] // don't support "wobbly" UTF-8
+        [InlineData(new byte[] { 0xED }, new char[] { '\uD800' })] // don't support partials
+        public static void AreEquivalent_IllFormedData_AlwaysReturnsFalse(byte[] asUtf8, char[] asUtf16)
+        {
+            Assert.False(Utf8String.AreEquivalent(asUtf8, asUtf16));
+        }
+    }
+}
diff --git a/src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Conversion.cs b/src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Conversion.cs
new file mode 100644 (file)
index 0000000..aa5eea2
--- /dev/null
@@ -0,0 +1,91 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using System.Text.Tests;
+using Xunit;
+
+using static System.Tests.Utf8TestUtilities;
+
+namespace System.Tests
+{
+    public unsafe partial class Utf8StringTests
+    {
+        public static IEnumerable<object[]> NormalizationData() => Utf8SpanTests.NormalizationData();
+
+        [Theory]
+        [MemberData(nameof(NormalizationData))]
+        public static void Normalize(string utf16Source, string utf16Expected, NormalizationForm normalizationForm)
+        {
+            Utf8String utf8Source = u8(utf16Source);
+
+            // Quick IsNormalized tests
+
+            Assert.Equal(utf16Source == utf16Expected, utf8Source.IsNormalized(normalizationForm));
+
+            // Normalize and return new Utf8String instances
+
+            Utf8String utf8Normalized = utf8Source.Normalize(normalizationForm);
+            Assert.True(Utf8String.AreEquivalent(utf8Normalized, utf16Expected));
+        }
+
+        public static IEnumerable<object[]> CaseConversionData() => Utf8SpanTests.CaseConversionData();
+
+        [Theory]
+        [MemberData(nameof(CaseConversionData))]
+        public static void ToLower(string testData)
+        {
+            static void RunTest(string testData, string expected, CultureInfo culture)
+            {
+                if (culture is null)
+                {
+                    Assert.Equal(u8(expected), u8(testData).ToLowerInvariant());
+                }
+                else
+                {
+                    Assert.Equal(u8(expected), u8(testData).ToLower(culture));
+                }
+            }
+
+            if (testData is null)
+            {
+                return; // no point in testing null "this" objects; we'll just null-ref
+            }
+
+            RunTest(testData, testData.ToLowerInvariant(), null);
+            RunTest(testData, testData.ToLower(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
+            RunTest(testData, testData.ToLower(CultureInfo.GetCultureInfo("en-US")), CultureInfo.GetCultureInfo("en-US"));
+            RunTest(testData, testData.ToLower(CultureInfo.GetCultureInfo("tr-TR")), CultureInfo.GetCultureInfo("tr-TR"));
+        }
+
+        [Theory]
+        [MemberData(nameof(CaseConversionData))]
+        public static void ToUpper(string testData)
+        {
+            static void RunTest(string testData, string expected, CultureInfo culture)
+            {
+                if (culture is null)
+                {
+                    Assert.Equal(u8(expected), u8(testData).ToUpperInvariant());
+                }
+                else
+                {
+                    Assert.Equal(u8(expected), u8(testData).ToUpper(culture));
+                }
+            }
+
+            if (testData is null)
+            {
+                return; // no point in testing null "this" objects; we'll just null-ref
+            }
+
+            RunTest(testData, testData.ToUpperInvariant(), null);
+            RunTest(testData, testData.ToUpper(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture);
+            RunTest(testData, testData.ToUpper(CultureInfo.GetCultureInfo("en-US")), CultureInfo.GetCultureInfo("en-US"));
+            RunTest(testData, testData.ToUpper(CultureInfo.GetCultureInfo("tr-TR")), CultureInfo.GetCultureInfo("tr-TR"));
+        }
+    }
+}
index 4862803..4b5e278 100644 (file)
@@ -31,23 +31,42 @@ namespace System.Tests
         }
 
         [Fact]
-        public static void Ctor_ByteArrayOffset_InvalidData_FixesUpData()
+        public static void Ctor_ByteArrayOffset_InvalidData_Throws()
         {
             byte[] inputData = new byte[] { (byte)'x', (byte)'H', (byte)'e', (byte)0xFF, (byte)'l', (byte)'o', (byte)'x' };
-            Utf8String expected = u8("He\uFFFDlo");
 
-            var actual = new Utf8String(inputData, 1, 5);
-            Assert.Equal(expected, actual);
+            Assert.Throws<ArgumentException>(() => new Utf8String(inputData, 0, inputData.Length));
+        }
+
+        [Fact]
+        public static void Ctor_ByteArrayOffset_NullValue_Throws()
+        {
+            var exception = Assert.Throws<ArgumentNullException>(() => new Utf8String((byte[])null, 0, 0));
+            Assert.Equal("value", exception.ParamName);
         }
 
         [Fact]
-        public static void Ctor_BytePointer_NullOrEmpty_ReturnsEmpty()
+        public static void Ctor_ByteArrayOffset_InvalidStartIndexOrLength_Throws()
+        {
+            byte[] inputData = new byte[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o' };
+
+            Assert.Throws<ArgumentOutOfRangeException>(() => new Utf8String(inputData, 1, 5));
+        }
+
+        [Fact]
+        public static void Ctor_BytePointer_Null_Throws()
+        {
+            var exception = Assert.Throws<ArgumentNullException>(() => new Utf8String((byte*)null));
+            Assert.Equal("value", exception.ParamName);
+        }
+
+        [Fact]
+        public static void Ctor_BytePointer_Empty_ReturnsEmpty()
         {
             byte[] inputData = new byte[] { 0 }; // standalone null byte
 
             using (BoundedMemory<byte> boundedMemory = BoundedMemory.AllocateFromExistingData(inputData))
             {
-                Assert.Same(Utf8String.Empty, new Utf8String((byte*)null));
                 Assert.Same(Utf8String.Empty, new Utf8String((byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(boundedMemory.Span))));
             }
         }
@@ -64,13 +83,13 @@ namespace System.Tests
         }
 
         [Fact]
-        public static void Ctor_BytePointer_InvalidData_FixesUpData()
+        public static void Ctor_BytePointer_InvalidData_Throws()
         {
             byte[] inputData = new byte[] { (byte)'H', (byte)'e', (byte)0xFF, (byte)'l', (byte)'o', (byte)'\0' };
 
             using (BoundedMemory<byte> boundedMemory = BoundedMemory.AllocateFromExistingData(inputData))
             {
-                Assert.Equal(u8("He\uFFFDlo"), new Utf8String((byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(boundedMemory.Span))));
+                Assert.Throws<ArgumentException>(() => new Utf8String((byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(boundedMemory.Span))));
             }
         }
 
@@ -91,50 +110,67 @@ namespace System.Tests
         }
 
         [Fact]
-        public static void Ctor_ByteSpan_InvalidData_FixesUpData()
+        public static void Ctor_ByteSpan_InvalidData_Throws()
         {
             byte[] inputData = new byte[] { (byte)'H', (byte)'e', (byte)0xFF, (byte)'l', (byte)'o' };
-            Utf8String expected = u8("He\uFFFDlo");
 
-            var actual = new Utf8String(inputData.AsSpan());
-            Assert.Equal(expected, actual);
+            Assert.Throws<ArgumentException>(() => new Utf8String(inputData.AsSpan()));
         }
 
         [Fact]
         public static void Ctor_CharArrayOffset_Empty_ReturnsEmpty()
         {
-            char[] inputData = "Hello".ToCharArray();
+            char[] inputData = "H\U00012345ello".ToCharArray(); // ok to have an empty slice in the middle of a multi-byte subsequence
             Assert.Same(Utf8String.Empty, new Utf8String(inputData, 3, 0));
         }
 
         [Fact]
-        public static void Ctor_CharArrayOffset_ValidData_ReturnsOriginalContents()
+        public static void Ctor_CharArrayOffset_ValidData_ReturnsAsUtf8()
         {
-            char[] inputData = "xHellox".ToCharArray();
-            Utf8String expected = u8("Hello");
+            char[] inputData = "H\U00012345\u07ffello".ToCharArray();
+            Utf8String expected = u8("\u07ffello");
 
-            var actual = new Utf8String(inputData, 1, 5);
+            var actual = new Utf8String(inputData, 3, 5);
             Assert.Equal(expected, actual);
         }
 
         [Fact]
-        public static void Ctor_CharArrayOffset_InvalidData_FixesUpData()
+        public static void Ctor_CharArrayOffset_InvalidData_Throws()
         {
-            char[] inputData = new char[] { 'x', 'H', 'e', '\uD800', 'l', 'o', 'x' };
-            Utf8String expected = u8("He\uFFFDlo");
+            char[] inputData = "H\ud800ello".ToCharArray();
 
-            var actual = new Utf8String(inputData, 1, 5);
-            Assert.Equal(expected, actual);
+            Assert.Throws<ArgumentException>(() => new Utf8String(inputData, 0, inputData.Length));
+        }
+
+        [Fact]
+        public static void Ctor_CharArrayOffset_NullValue_Throws()
+        {
+            var exception = Assert.Throws<ArgumentNullException>(() => new Utf8String((char[])null, 0, 0));
+            Assert.Equal("value", exception.ParamName);
+        }
+
+        [Fact]
+        public static void Ctor_CharArrayOffset_InvalidStartIndexOrLength_Throws()
+        {
+            char[] inputData = "Hello".ToCharArray();
+
+            Assert.Throws<ArgumentOutOfRangeException>(() => new Utf8String(inputData, 1, 5));
+        }
+
+        [Fact]
+        public static void Ctor_CharPointer_Null_Throws()
+        {
+            var exception = Assert.Throws<ArgumentNullException>(() => new Utf8String((char*)null));
+            Assert.Equal("value", exception.ParamName);
         }
 
         [Fact]
-        public static void Ctor_CharPointer_NullOrEmpty_ReturnsEmpty()
+        public static void Ctor_CharPointer_Empty_ReturnsEmpty()
         {
             char[] inputData = new char[] { '\0' }; // standalone null char
 
             using (BoundedMemory<char> boundedMemory = BoundedMemory.AllocateFromExistingData(inputData))
             {
-                Assert.Same(Utf8String.Empty, new Utf8String((char*)null));
                 Assert.Same(Utf8String.Empty, new Utf8String((char*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(boundedMemory.Span))));
             }
         }
@@ -142,7 +178,7 @@ namespace System.Tests
         [Fact]
         public static void Ctor_CharPointer_ValidData_ReturnsOriginalContents()
         {
-            char[] inputData = new char[] { 'H', 'e', 'l', 'l', 'o', '\0' };
+            char[] inputData = "Hello\0".ToCharArray(); // need to manually null-terminate
 
             using (BoundedMemory<char> boundedMemory = BoundedMemory.AllocateFromExistingData(inputData))
             {
@@ -151,13 +187,13 @@ namespace System.Tests
         }
 
         [Fact]
-        public static void Ctor_CharPointer_InvalidData_FixesUpData()
+        public static void Ctor_CharPointer_InvalidData_Throws()
         {
-            char[] inputData = new char[] { 'H', 'e', '\uD800', 'l', 'o', '\0' }; // standalone surrogate
+            char[] inputData = "He\ud800llo\0".ToCharArray(); // need to manually null-terminate
 
             using (BoundedMemory<char> boundedMemory = BoundedMemory.AllocateFromExistingData(inputData))
             {
-                Assert.Equal(u8("He\uFFFDlo"), new Utf8String((char*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(boundedMemory.Span))));
+                Assert.Throws<ArgumentException>(() => new Utf8String((char*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(boundedMemory.Span))));
             }
         }
 
@@ -178,19 +214,23 @@ namespace System.Tests
         }
 
         [Fact]
-        public static void Ctor_CharSpan_InvalidData_FixesUpData()
+        public static void Ctor_CharSpan_InvalidData_Throws()
         {
-            char[] inputData = new char[] { 'H', 'e', '\uD800', 'l', 'o' };
-            Utf8String expected = u8("He\uFFFDlo");
+            char[] inputData = "He\ud800llo".ToCharArray();
 
-            var actual = new Utf8String(inputData.AsSpan());
-            Assert.Equal(expected, actual);
+            Assert.Throws<ArgumentException>(() => new Utf8String(inputData.AsSpan()));
         }
 
         [Fact]
-        public static void Ctor_String_NullOrEmpty_ReturnsEmpty()
+        public static void Ctor_String_Null_Throws()
+        {
+            var exception = Assert.Throws<ArgumentNullException>(() => new Utf8String((string)null));
+            Assert.Equal("value", exception.ParamName);
+        }
+
+        [Fact]
+        public static void Ctor_String_Empty_ReturnsEmpty()
         {
-            Assert.Same(Utf8String.Empty, new Utf8String((string)null));
             Assert.Same(Utf8String.Empty, new Utf8String(string.Empty));
         }
 
@@ -201,9 +241,176 @@ namespace System.Tests
         }
 
         [Fact]
-        public static void Ctor_String_InvalidData_FixesUpData()
+        public static void Ctor_String_InvalidData_Throws()
+        {
+            Assert.Throws<ArgumentException>(() => new Utf8String("He\uD800lo"));
+        }
+
+        [Fact]
+        public static void Ctor_NonValidating_FromByteSpan()
+        {
+            byte[] inputData = new byte[] { (byte)'x', (byte)'y', (byte)'z' };
+            Utf8String actual = Utf8String.UnsafeCreateWithoutValidation(inputData);
+
+            Assert.Equal(u8("xyz"), actual);
+        }
+
+        [Fact]
+        public static void Ctor_NonValidating_FromDelegate()
+        {
+            object expectedState = new object();
+            SpanAction<byte, object> spanAction = (span, actualState) =>
+            {
+                Assert.Same(expectedState, actualState);
+                Assert.NotEqual(0, span.Length); // shouldn't have been called for a zero-length span
+
+                for (int i = 0; i < span.Length; i++)
+                {
+                    Assert.Equal(0, span[i]); // should've been zero-inited
+                    span[i] = (byte)('a' + (i % 26)); // writes "abc...xyzabc...xyz..."
+                }
+            };
+
+            ArgumentException exception = Assert.Throws<ArgumentOutOfRangeException>(() => Utf8String.UnsafeCreateWithoutValidation(-1, expectedState, spanAction));
+            Assert.Equal("length", exception.ParamName);
+
+            exception = Assert.Throws<ArgumentNullException>(() => Utf8String.UnsafeCreateWithoutValidation(10, expectedState, action: null));
+            Assert.Equal("action", exception.ParamName);
+
+            Assert.Same(Utf8String.Empty, Utf8String.UnsafeCreateWithoutValidation(0, expectedState, spanAction));
+
+            Assert.Equal(u8("abcde"), Utf8String.UnsafeCreateWithoutValidation(5, expectedState, spanAction));
+        }
+
+        [Fact]
+        public static void Ctor_Validating_FromDelegate()
+        {
+            object expectedState = new object();
+            SpanAction<byte, object> spanAction = (span, actualState) =>
+            {
+                Assert.Same(expectedState, actualState);
+                Assert.NotEqual(0, span.Length); // shouldn't have been called for a zero-length span
+
+                for (int i = 0; i < span.Length; i++)
+                {
+                    Assert.Equal(0, span[i]); // should've been zero-inited
+                    span[i] = (byte)('a' + (i % 26)); // writes "abc...xyzabc...xyz..."
+                }
+            };
+
+            ArgumentException exception = Assert.Throws<ArgumentOutOfRangeException>(() => Utf8String.Create(-1, expectedState, spanAction));
+            Assert.Equal("length", exception.ParamName);
+
+            exception = Assert.Throws<ArgumentNullException>(() => Utf8String.Create(10, expectedState, action: null));
+            Assert.Equal("action", exception.ParamName);
+
+            Assert.Same(Utf8String.Empty, Utf8String.Create(0, expectedState, spanAction));
+
+            Assert.Equal(u8("abcde"), Utf8String.Create(5, expectedState, spanAction));
+        }
+
+        [Fact]
+        public static void Ctor_Validating_FromDelegate_ThrowsIfDelegateProvidesInvalidData()
+        {
+            SpanAction<byte, object> spanAction = (span, actualState) =>
+            {
+                span[0] = 0xFF; // never a valid UTF-8 byte
+            };
+
+            Assert.Throws<ArgumentException>(() => Utf8String.Create(10, new object(), spanAction));
+        }
+
+        [Fact]
+        public static void Ctor_CreateFromRelaxed_Utf16()
+        {
+            Assert.Same(Utf8String.Empty, Utf8String.CreateFromRelaxed(ReadOnlySpan<char>.Empty));
+            Assert.Equal(u8("xy\uFFFDz"), Utf8String.CreateFromRelaxed("xy\ud800z"));
+        }
+
+        [Fact]
+        public static void Ctor_CreateFromRelaxed_Utf8()
+        {
+            Assert.Same(Utf8String.Empty, Utf8String.CreateFromRelaxed(ReadOnlySpan<byte>.Empty));
+            Assert.Equal(u8("xy\uFFFDz"), Utf8String.CreateFromRelaxed(new byte[] { (byte)'x', (byte)'y', 0xF4, 0x80, 0x80, (byte)'z' }));
+        }
+
+        [Fact]
+        public static void Ctor_CreateRelaxed_FromDelegate()
+        {
+            object expectedState = new object();
+            SpanAction<byte, object> spanAction = (span, actualState) =>
+            {
+                Assert.Same(expectedState, actualState);
+                Assert.NotEqual(0, span.Length); // shouldn't have been called for a zero-length span
+
+                for (int i = 0; i < span.Length; i++)
+                {
+                    Assert.Equal(0, span[i]); // should've been zero-inited
+                    span[i] = 0xFF; // never a valid UTF-8 byte
+                }
+            };
+
+            ArgumentException exception = Assert.Throws<ArgumentOutOfRangeException>(() => Utf8String.CreateRelaxed(-1, expectedState, spanAction));
+            Assert.Equal("length", exception.ParamName);
+
+            exception = Assert.Throws<ArgumentNullException>(() => Utf8String.CreateRelaxed(10, expectedState, action: null));
+            Assert.Equal("action", exception.ParamName);
+
+            Assert.Same(Utf8String.Empty, Utf8String.CreateRelaxed(0, expectedState, spanAction));
+
+            Assert.Equal(u8("\uFFFD\uFFFD"), Utf8String.CreateRelaxed(2, expectedState, spanAction));
+        }
+
+        [Fact]
+        public static void Ctor_TryCreateFrom_Utf8()
         {
-            Assert.Equal(u8("He\uFFFDlo"), new Utf8String("He\uD800lo"));
+            Utf8String value;
+
+            // Empty string
+
+            Assert.True(Utf8String.TryCreateFrom(ReadOnlySpan<byte>.Empty, out value));
+            Assert.Same(Utf8String.Empty, value);
+
+            // Well-formed ASCII contents
+
+            Assert.True(Utf8String.TryCreateFrom(new byte[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o' }, out value));
+            Assert.Equal(u8("Hello"), value);
+
+            // Well-formed non-ASCII contents
+
+            Assert.True(Utf8String.TryCreateFrom(new byte[] { 0xF0, 0x9F, 0x91, 0xBD }, out value)); // U+1F47D EXTRATERRESTRIAL ALIEN
+            Assert.Equal(u8("\U0001F47D"), value);
+
+            // Ill-formed contents
+
+            Assert.False(Utf8String.TryCreateFrom(new byte[] { 0xF0, 0x9F, 0x91, (byte)'x' }, out value));
+            Assert.Null(value);
+        }
+
+        [Fact]
+        public static void Ctor_TryCreateFrom_Utf16()
+        {
+            Utf8String value;
+
+            // Empty string
+
+            Assert.True(Utf8String.TryCreateFrom(ReadOnlySpan<char>.Empty, out value));
+            Assert.Same(Utf8String.Empty, value);
+
+            // Well-formed ASCII contents
+
+            Assert.True(Utf8String.TryCreateFrom("Hello", out value));
+            Assert.Equal(u8("Hello"), value);
+
+            // Well-formed non-ASCII contents
+
+            Assert.True(Utf8String.TryCreateFrom("\U0001F47D", out value)); // U+1F47D EXTRATERRESTRIAL ALIEN
+            Assert.Equal(u8("\U0001F47D"), value);
+
+            // Ill-formed contents
+
+            Assert.False(Utf8String.TryCreateFrom("\uD800x", out value));
+            Assert.Null(value);
         }
     }
 }
diff --git a/src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Enumeration.cs b/src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Enumeration.cs
new file mode 100644 (file)
index 0000000..6333127
--- /dev/null
@@ -0,0 +1,86 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text;
+using Xunit;
+
+using static System.Tests.Utf8TestUtilities;
+
+namespace System.Tests
+{
+    public unsafe partial class Utf8StringTests
+    {
+        [Fact]
+        public static void BytesProperty_FromData()
+        {
+            Utf8String ustr = u8("\U00000012\U00000123\U00001234\U00101234\U00000012\U00000123\U00001234\U00101234");
+
+            Assert.Equal(new byte[]
+            {
+                0x12,
+                0xC4, 0xA3,
+                0xE1, 0x88, 0xB4,
+                0xF4, 0x81, 0x88, 0xB4,
+                0x12,
+                0xC4, 0xA3,
+                0xE1, 0x88, 0xB4,
+                0xF4, 0x81, 0x88, 0xB4,
+            }, ustr.Bytes);
+        }
+
+        [Fact]
+        public static void BytesProperty_FromEmpty()
+        {
+            Assert.False(Utf8String.Empty.Bytes.GetEnumerator().MoveNext());
+        }
+
+        [Fact]
+        public static void CharsProperty_FromData()
+        {
+            Utf8String ustr = u8("\U00000012\U00000123\U00001234\U00101234\U00000012\U00000123\U00001234\U00101234");
+
+            Assert.Equal(new char[]
+            {
+                '\u0012',
+                '\u0123',
+                '\u1234',
+                '\uDBC4', '\uDE34',
+                '\u0012',
+                '\u0123',
+                '\u1234',
+                '\uDBC4', '\uDE34',
+            }, ustr.Chars);
+        }
+
+        [Fact]
+        public static void CharsProperty_FromEmpty()
+        {
+            Assert.False(Utf8String.Empty.Chars.GetEnumerator().MoveNext());
+        }
+
+        [Fact]
+        public static void RunesProperty_FromData()
+        {
+            Utf8String ustr = u8("\U00000012\U00000123\U00001234\U00101234\U00000012\U00000123\U00001234\U00101234");
+
+            Assert.Equal(new Rune[]
+            {
+                new Rune(0x0012),
+                new Rune(0x0123),
+                new Rune(0x1234),
+                new Rune(0x101234),
+                new Rune(0x0012),
+                new Rune(0x0123),
+                new Rune(0x1234),
+                new Rune(0x101234),
+            }, ustr.Runes);
+        }
+
+        [Fact]
+        public static void RunesProperty_FromEmpty()
+        {
+            Assert.False(Utf8String.Empty.Runes.GetEnumerator().MoveNext());
+        }
+    }
+}
diff --git a/src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Manipulation.cs b/src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Manipulation.cs
new file mode 100644 (file)
index 0000000..e80e611
--- /dev/null
@@ -0,0 +1,211 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Tests;
+using Xunit;
+
+using static System.Tests.Utf8TestUtilities;
+
+namespace System.Tests
+{
+    public unsafe partial class Utf8StringTests
+    {
+        private delegate Utf8String.SplitResult Utf8StringSplitDelegate(Utf8String ustr, Utf8StringSplitOptions splitOptions);
+
+        [Fact]
+        public static void Split_Utf8StringSeparator_WithNullOrEmptySeparator_Throws()
+        {
+            var ex = Assert.Throws<ArgumentException>(() => { u8("Hello").Split((Utf8String)null); });
+            Assert.Equal("separator", ex.ParamName);
+
+            // Shouldn't be able to split on an empty Utf8String.
+            // Such an enumerator would iterate forever, so we forbid it.
+
+            ex = Assert.Throws<ArgumentException>(() => { u8("Hello").Split(Utf8String.Empty); });
+            Assert.Equal("separator", ex.ParamName);
+        }
+
+        [Fact]
+        public static void Split_InvalidChar_Throws()
+        {
+            // Shouldn't be able to split on a standalone surrogate char
+            // Other search methods (TryFind) return false when given a standalone surrogate char as input,
+            // but the Split methods returns a complex data structure instead of a simple bool. So to keep
+            // the logic of that data structure relatively simple we'll forbid the bad char at the call site.
+
+            var ex = Assert.Throws<ArgumentOutOfRangeException>(() => { u8("Hello").Split('\ud800'); });
+            Assert.Equal("separator", ex.ParamName);
+        }
+
+        public static IEnumerable<object[]> SplitData_CharSeparator => Utf8SpanTests.SplitData_CharSeparator();
+
+        [Theory]
+        [MemberData(nameof(SplitData_CharSeparator))]
+        public static void Split_Char(Utf8String source, char separator, Range[] expectedRanges)
+        {
+            SplitTest_Common(source ?? Utf8String.Empty, (ustr, splitOptions) => ustr.Split(separator, splitOptions), expectedRanges);
+        }
+
+        [Fact]
+        public static void Split_Deconstruct()
+        {
+            Utf8String ustr = u8("a,b,c,d,e");
+
+            {
+                (Utf8String a, Utf8String b) = ustr.Split('x'); // not found
+                Assert.Same(ustr, a); // Expected referential equality of input
+                Assert.Null(b);
+            }
+
+            {
+                (Utf8String a, Utf8String b) = ustr.Split(',');
+                Assert.Equal(u8("a"), a);
+                Assert.Equal(u8("b,c,d,e"), b);
+            }
+
+            {
+                (Utf8String a, Utf8String b, Utf8String c, Utf8String d, Utf8String e) = ustr.Split(',');
+                Assert.Equal(u8("a"), a);
+                Assert.Equal(u8("b"), b);
+                Assert.Equal(u8("c"), c);
+                Assert.Equal(u8("d"), d);
+                Assert.Equal(u8("e"), e);
+            }
+
+            {
+                (Utf8String a, Utf8String b, Utf8String c, Utf8String d, Utf8String e, Utf8String f, Utf8String g, Utf8String h) = ustr.Split(',');
+                Assert.Equal(u8("a"), a);
+                Assert.Equal(u8("b"), b);
+                Assert.Equal(u8("c"), c);
+                Assert.Equal(u8("d"), d);
+                Assert.Equal(u8("e"), e);
+                Assert.Null(f);
+                Assert.Null(g);
+                Assert.Null(h);
+            }
+        }
+
+        [Fact]
+        public static void Split_Deconstruct_WithOptions()
+        {
+            Utf8String ustr = u8("a, , b, c,, d, e");
+
+            // Note referential equality checks below (since we want to know exact slices
+            // into the original buffer), not deep (textual) equality checks.
+
+            {
+                (Utf8String a, Utf8String b) = ustr.Split(',', Utf8StringSplitOptions.RemoveEmptyEntries);
+                Assert.Equal(u8("a"), a);
+                Assert.Equal(u8(" , b, c,, d, e"), b);
+            }
+
+            {
+                (Utf8String a, Utf8String x, Utf8String b, Utf8String c, Utf8String d, Utf8String e) = ustr.Split(',', Utf8StringSplitOptions.RemoveEmptyEntries);
+                Assert.Equal(u8("a"), a); // "a"
+                Assert.Equal(u8(" "), x); // " "
+                Assert.Equal(u8(" b"), b); // " b"
+                Assert.Equal(u8(" c"), c); // " c"
+                Assert.Equal(u8(" d"), d); // " d"
+                Assert.Equal(u8(" e"), e); // " e"
+            }
+
+            {
+                (Utf8String a, Utf8String b, Utf8String c, Utf8String d, Utf8String e, Utf8String f, Utf8String g, Utf8String h) = ustr.Split(',', Utf8StringSplitOptions.RemoveEmptyEntries | Utf8StringSplitOptions.TrimEntries);
+                Assert.Equal(u8("a"), a);
+                Assert.Equal(u8("b"), b);
+                Assert.Equal(u8("c"), c);
+                Assert.Equal(u8("d"), d);
+                Assert.Equal(u8("e"), e);
+                Assert.Null(f);
+                Assert.Null(g);
+                Assert.Null(h);
+            }
+        }
+
+        public static IEnumerable<object[]> SplitData_RuneSeparator() => Utf8SpanTests.SplitData_RuneSeparator();
+
+        [Theory]
+        [MemberData(nameof(SplitData_RuneSeparator))]
+        public static void Split_Rune(Utf8String source, Rune separator, Range[] expectedRanges)
+        {
+            SplitTest_Common(source ?? Utf8String.Empty, (ustr, splitOptions) => ustr.Split(separator, splitOptions), expectedRanges);
+        }
+
+        public static IEnumerable<object[]> SplitData_Utf8StringSeparator() => Utf8SpanTests.SplitData_Utf8SpanSeparator();
+
+        [Theory]
+        [MemberData(nameof(SplitData_Utf8StringSeparator))]
+        public static void Split_Utf8String(Utf8String source, Utf8String separator, Range[] expectedRanges)
+        {
+            SplitTest_Common(source ?? Utf8String.Empty, (ustr, splitOptions) => ustr.Split(separator, splitOptions), expectedRanges);
+        }
+
+        private static void SplitTest_Common(Utf8String source, Utf8StringSplitDelegate splitAction, Range[] expectedRanges)
+        {
+            // First, run the split with default options and make sure the results are equivalent
+
+            Assert.Equal(
+                expected: expectedRanges.Select(range => source[range]),
+                actual: splitAction(source, Utf8StringSplitOptions.None));
+
+            // Next, run the split with empty entries removed
+
+            Assert.Equal(
+                expected: expectedRanges.Select(range => source[range]).Where(ustr => ustr.Length != 0),
+                actual: splitAction(source, Utf8StringSplitOptions.RemoveEmptyEntries));
+
+            // Next, run the split with results trimmed (but allowing empty results)
+
+            Assert.Equal(
+                expected: expectedRanges.Select(range => source[range].Trim()),
+                actual: splitAction(source, Utf8StringSplitOptions.TrimEntries));
+
+            // Finally, run the split both trimmed and with empty entries removed
+
+            Assert.Equal(
+                expected: expectedRanges.Select(range => source[range].Trim()).Where(ustr => ustr.Length != 0),
+                actual: splitAction(source, Utf8StringSplitOptions.TrimEntries | Utf8StringSplitOptions.RemoveEmptyEntries));
+        }
+
+        public static IEnumerable<object[]> Trim_TestData() => Utf8SpanTests.Trim_TestData();
+
+        [Theory]
+        [MemberData(nameof(Trim_TestData))]
+        public static void Trim(string input)
+        {
+            if (input is null)
+            {
+                return; // don't want to null ref
+            }
+
+            Utf8String utf8Input = u8(input);
+
+            void RunTest(Func<Utf8String, Utf8String> utf8TrimAction, Func<string, string> utf16TrimAction)
+            {
+                Utf8String utf8Trimmed = utf8TrimAction(utf8Input);
+                string utf16Trimmed = utf16TrimAction(input);
+
+                if (utf16Trimmed.Length == input.Length)
+                {
+                    Assert.Same(utf8Input, utf8Trimmed); // Trimming should no-op, return original input
+                }
+                else if (utf16Trimmed.Length == 0)
+                {
+                    Assert.Same(Utf8String.Empty, utf8Trimmed); // Trimming an all-whitespace input, return Empty
+                }
+                else
+                {
+                    Assert.True(Utf8String.AreEquivalent(utf8Trimmed, utf16Trimmed));
+                }
+            }
+
+            RunTest(ustr => ustr.Trim(), str => str.Trim());
+            RunTest(ustr => ustr.TrimStart(), str => str.TrimStart());
+            RunTest(ustr => ustr.TrimEnd(), str => str.TrimEnd());
+        }
+    }
+}
index 3359cef..573ffd3 100644 (file)
 // See the LICENSE file in the project root for more information.
 
 using System.Collections.Generic;
+using System.Globalization;
 using System.Text;
+using System.Text.Tests;
 using Xunit;
 
 using static System.Tests.Utf8TestUtilities;
 
+using ustring = System.Utf8String;
+
 namespace System.Tests
 {
+    /*
+     * Please keep these tests in sync with those in Utf8SpanTests.Searching.cs.
+     */
+
     public unsafe partial class Utf8StringTests
     {
+        public static IEnumerable<object[]> TryFindData_Char_Ordinal() => Utf8SpanTests.TryFindData_Char_Ordinal();
+
         [Theory]
-        [MemberData(nameof(IndexOfTestData))]
-        public static void Contains_And_IndexOf_CharRune_Ordinal(Utf8String utf8String, Rune searchValue, int expectedIndex)
+        [MemberData(nameof(TryFindData_Char_Ordinal))]
+        public static void TryFind_Char_Ordinal(ustring source, char searchTerm, Range? expectedForwardMatch, Range? expectedBackwardMatch)
         {
-            // Contains
+            if (source is null)
+            {
+                return; // don't null ref
+            }
+
+            // First, search forward
+
+            bool wasFound = source.TryFind(searchTerm, out Range actualForwardMatch);
+            Assert.Equal(expectedForwardMatch.HasValue, wasFound);
+
+            if (wasFound)
+            {
+                AssertRangesEqual(source.Length, expectedForwardMatch.Value, actualForwardMatch);
+            }
+
+            // Also check Contains / StartsWith / SplitOn
+
+            Assert.Equal(wasFound, source.Contains(searchTerm));
+            Assert.Equal(wasFound && source[..actualForwardMatch.Start].Length == 0, source.StartsWith(searchTerm));
+
+            (var before, var after) = source.SplitOn(searchTerm);
+            if (wasFound)
+            {
+                Assert.Equal(source[..actualForwardMatch.Start], before);
+                Assert.Equal(source[actualForwardMatch.End..], after);
+            }
+            else
+            {
+                Assert.Same(source, before); // check for reference equality
+                Assert.Null(after);
+            }
 
-            if (searchValue.IsBmp)
+            // Now search backward
+
+            wasFound = source.TryFindLast(searchTerm, out Range actualBackwardMatch);
+            Assert.Equal(expectedBackwardMatch.HasValue, wasFound);
+
+            if (wasFound)
             {
-                Assert.Equal(expectedIndex >= 0, utf8String.Contains((char)searchValue.Value));
+                AssertRangesEqual(source.Length, expectedBackwardMatch.Value, actualBackwardMatch);
             }
-            Assert.Equal(expectedIndex >= 0, utf8String.Contains(searchValue));
 
-            // IndexOf
+            // Also check EndsWith / SplitOnLast
+
+            Assert.Equal(wasFound && source[actualBackwardMatch.End..].Length == 0, source.EndsWith(searchTerm));
 
-            if (searchValue.IsBmp)
+            (before, after) = source.SplitOnLast(searchTerm);
+            if (wasFound)
+            {
+                Assert.Equal(source[..actualBackwardMatch.Start], before);
+                Assert.Equal(source[actualBackwardMatch.End..], after);
+            }
+            else
             {
-                Assert.Equal(expectedIndex, utf8String.IndexOf((char)searchValue.Value));
+                Assert.Same(source, before); // check for reference equality
+                Assert.Null(after);
             }
-            Assert.Equal(expectedIndex, utf8String.IndexOf(searchValue));
         }
 
+        public static IEnumerable<object[]> TryFindData_Char_WithComparison() => Utf8SpanTests.TryFindData_Char_WithComparison();
+
         [Theory]
-        [MemberData(nameof(IndexOfTestData))]
-        public static void StartsWith_And_EndsWith_CharRune_Ordinal(Utf8String utf8String, Rune searchValue, int expectedIndex)
+        [PlatformSpecific(TestPlatforms.Windows)]
+        [MemberData(nameof(TryFindData_Char_WithComparison))]
+        public static void TryFind_Char_WithComparison(ustring source, char searchTerm, StringComparison comparison, CultureInfo currentCulture, Range? expectedForwardMatch, Range? expectedBackwardMatch)
         {
-            // StartsWith
+            if (source is null)
+            {
+                return; // don't null ref
+            }
 
-            if (searchValue.IsBmp)
+            RunOnDedicatedThread(() =>
             {
-                Assert.Equal(expectedIndex == 0, utf8String.StartsWith((char)searchValue.Value));
+                if (currentCulture != null)
+                {
+                    CultureInfo.CurrentCulture = currentCulture;
+                }
+
+                // First, search forward
+
+                bool wasFound = source.TryFind(searchTerm, comparison, out Range actualForwardMatch);
+                Assert.Equal(expectedForwardMatch.HasValue, wasFound);
+
+                if (wasFound)
+                {
+                    AssertRangesEqual(source.Length, expectedForwardMatch.Value, actualForwardMatch);
+                }
+
+                // Also check Contains / StartsWith / SplitOn
+
+                Assert.Equal(wasFound, source.Contains(searchTerm, comparison));
+                Assert.Equal(wasFound && source[..actualForwardMatch.Start].Length == 0, source.StartsWith(searchTerm, comparison));
+
+                (var before, var after) = source.SplitOn(searchTerm, comparison);
+                if (wasFound)
+                {
+                    Assert.Equal(source[..actualForwardMatch.Start], before);
+                    Assert.Equal(source[actualForwardMatch.End..], after);
+                }
+                else
+                {
+                    Assert.Same(source, before); // check for reference equality
+                    Assert.Null(after);
+                }
+
+                // Now search backward
+
+                wasFound = source.TryFindLast(searchTerm, comparison, out Range actualBackwardMatch);
+                Assert.Equal(expectedBackwardMatch.HasValue, wasFound);
+
+                if (wasFound)
+                {
+                    AssertRangesEqual(source.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+                }
+
+                // Also check EndsWith / SplitOnLast
+
+                Assert.Equal(wasFound && source[actualBackwardMatch.End..].Length == 0, source.EndsWith(searchTerm, comparison));
+
+                (before, after) = source.SplitOnLast(searchTerm, comparison);
+                if (wasFound)
+                {
+                    Assert.Equal(source[..actualBackwardMatch.Start], before);
+                    Assert.Equal(source[actualBackwardMatch.End..], after);
+                }
+                else
+                {
+                    Assert.Same(source, before); // check for reference equality
+                    Assert.Null(after);
+                }
+            });
+        }
+
+        public static IEnumerable<object[]> TryFindData_Rune_Ordinal() => Utf8SpanTests.TryFindData_Rune_Ordinal();
+
+        [Theory]
+        [MemberData(nameof(TryFindData_Rune_Ordinal))]
+        public static void TryFind_Rune_Ordinal(ustring source, Rune searchTerm, Range? expectedForwardMatch, Range? expectedBackwardMatch)
+        {
+            if (source is null)
+            {
+                return; // don't null ref
             }
-            Assert.Equal(expectedIndex == 0, utf8String.StartsWith(searchValue));
 
-            // EndsWith
+            // First, search forward
 
-            bool endsWithExpectedValue = (expectedIndex >= 0) && (expectedIndex + searchValue.Utf8SequenceLength) == utf8String.Length;
+            bool wasFound = source.TryFind(searchTerm, out Range actualForwardMatch);
+            Assert.Equal(expectedForwardMatch.HasValue, wasFound);
 
-            if (searchValue.IsBmp)
+            if (wasFound)
             {
-                Assert.Equal(endsWithExpectedValue, utf8String.EndsWith((char)searchValue.Value));
+                AssertRangesEqual(source.Length, expectedForwardMatch.Value, actualForwardMatch);
+            }
+
+            // Also check Contains / StartsWith / SplitOn
+
+            Assert.Equal(wasFound, source.Contains(searchTerm));
+            Assert.Equal(wasFound && source[..actualForwardMatch.Start].Length == 0, source.StartsWith(searchTerm));
+
+            (var before, var after) = source.SplitOn(searchTerm);
+            if (wasFound)
+            {
+                Assert.Equal(source[..actualForwardMatch.Start], before);
+                Assert.Equal(source[actualForwardMatch.End..], after);
+            }
+            else
+            {
+                Assert.Same(source, before); // check for reference equality
+                Assert.Null(after);
+            }
+
+            // Now search backward
+
+            wasFound = source.TryFindLast(searchTerm, out Range actualBackwardMatch);
+            Assert.Equal(expectedBackwardMatch.HasValue, wasFound);
+
+            if (wasFound)
+            {
+                AssertRangesEqual(source.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+            }
+
+            // Also check EndsWith / SplitOnLast
+
+            Assert.Equal(wasFound && source[actualBackwardMatch.End..].Length == 0, source.EndsWith(searchTerm));
+
+            (before, after) = source.SplitOnLast(searchTerm);
+            if (wasFound)
+            {
+                Assert.Equal(source[..actualBackwardMatch.Start], before);
+                Assert.Equal(source[actualBackwardMatch.End..], after);
+            }
+            else
+            {
+                Assert.Same(source, before); // check for reference equality
+                Assert.Null(after);
             }
-            Assert.Equal(endsWithExpectedValue, utf8String.EndsWith(searchValue));
         }
 
-        [Fact]
-        public static void Searching_StandaloneSurrogate_Fails()
+        public static IEnumerable<object[]> TryFindData_Rune_WithComparison() => Utf8SpanTests.TryFindData_Rune_WithComparison();
+
+        [Theory]
+        [PlatformSpecific(TestPlatforms.Windows)]
+        [MemberData(nameof(TryFindData_Rune_WithComparison))]
+        public static void TryFind_Rune_WithComparison(ustring source, Rune searchTerm, StringComparison comparison, CultureInfo currentCulture, Range? expectedForwardMatch, Range? expectedBackwardMatch)
+        {
+            if (source is null)
+            {
+                return; // don't null ref
+            }
+
+            RunOnDedicatedThread(() =>
+            {
+                if (currentCulture != null)
+                {
+                    CultureInfo.CurrentCulture = currentCulture;
+                }
+
+                // First, search forward
+
+                bool wasFound = source.TryFind(searchTerm, comparison, out Range actualForwardMatch);
+                Assert.Equal(expectedForwardMatch.HasValue, wasFound);
+
+                if (wasFound)
+                {
+                    AssertRangesEqual(source.Length, expectedForwardMatch.Value, actualForwardMatch);
+                }
+
+                // Also check Contains / StartsWith / SplitOn
+
+                Assert.Equal(wasFound, source.Contains(searchTerm, comparison));
+                Assert.Equal(wasFound && source[..actualForwardMatch.Start].Length == 0, source.StartsWith(searchTerm, comparison));
+
+                (var before, var after) = source.SplitOn(searchTerm, comparison);
+                if (wasFound)
+                {
+                    Assert.Equal(source[..actualForwardMatch.Start], before);
+                    Assert.Equal(source[actualForwardMatch.End..], after);
+                }
+                else
+                {
+                    Assert.Same(source, before); // check for reference equality
+                    Assert.Null(after);
+                }
+
+                // Now search backward
+
+                wasFound = source.TryFindLast(searchTerm, comparison, out Range actualBackwardMatch);
+                Assert.Equal(expectedBackwardMatch.HasValue, wasFound);
+
+                if (wasFound)
+                {
+                    AssertRangesEqual(source.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+                }
+
+                // Also check EndsWith / SplitOnLast
+
+                Assert.Equal(wasFound && source[actualBackwardMatch.End..].Length == 0, source.EndsWith(searchTerm, comparison));
+
+                (before, after) = source.SplitOnLast(searchTerm, comparison);
+                if (wasFound)
+                {
+                    Assert.Equal(source[..actualBackwardMatch.Start], before);
+                    Assert.Equal(source[actualBackwardMatch.End..], after);
+                }
+                else
+                {
+                    Assert.Same(source, before); // check for reference equality
+                    Assert.Null(after);
+                }
+            });
+        }
+
+        public static IEnumerable<object[]> TryFindData_Utf8String_Ordinal() => Utf8SpanTests.TryFindData_Utf8Span_Ordinal();
+
+        [Theory]
+        [MemberData(nameof(TryFindData_Utf8String_Ordinal))]
+        public static void TryFind_Utf8String_Ordinal(ustring source, ustring searchTerm, Range? expectedForwardMatch, Range? expectedBackwardMatch)
         {
-            Utf8String utf8String = u8("\ud800\udfff");
+            if (source is null)
+            {
+                return; // don't null ref
+            }
 
-            Assert.False(utf8String.Contains('\ud800'));
-            Assert.False(utf8String.Contains('\udfff'));
+            // First, search forward
 
-            Assert.Equal(-1, utf8String.IndexOf('\ud800'));
-            Assert.Equal(-1, utf8String.IndexOf('\udfff'));
+            bool wasFound = source.TryFind(searchTerm, out Range actualForwardMatch);
+            Assert.Equal(expectedForwardMatch.HasValue, wasFound);
 
-            Assert.False(utf8String.StartsWith('\ud800'));
-            Assert.False(utf8String.StartsWith('\udfff'));
+            if (wasFound)
+            {
+                AssertRangesEqual(source.Length, expectedForwardMatch.Value, actualForwardMatch);
+            }
+
+            // Also check Contains / StartsWith / SplitOn
 
-            Assert.False(utf8String.EndsWith('\ud800'));
-            Assert.False(utf8String.EndsWith('\udfff'));
+            Assert.Equal(wasFound, source.Contains(searchTerm));
+            Assert.Equal(wasFound && source[..actualForwardMatch.Start].Length == 0, source.StartsWith(searchTerm));
+
+            (var before, var after) = source.SplitOn(searchTerm);
+            if (wasFound)
+            {
+                Assert.Equal(source[..actualForwardMatch.Start], before);
+                Assert.Equal(source[actualForwardMatch.End..], after);
+            }
+            else
+            {
+                Assert.Same(source, before); // check for reference equality
+                Assert.Null(after);
+            }
+
+            // Now search backward
+
+            wasFound = source.TryFindLast(searchTerm, out Range actualBackwardMatch);
+            Assert.Equal(expectedBackwardMatch.HasValue, wasFound);
+
+            if (wasFound)
+            {
+                AssertRangesEqual(source.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+            }
+
+            // Also check EndsWith / SplitOnLast
+
+            Assert.Equal(wasFound && source[actualBackwardMatch.End..].Length == 0, source.EndsWith(searchTerm));
+
+            (before, after) = source.SplitOnLast(searchTerm);
+            if (wasFound)
+            {
+                Assert.Equal(source[..actualBackwardMatch.Start], before);
+                Assert.Equal(source[actualBackwardMatch.End..], after);
+            }
+            else
+            {
+                Assert.Same(source, before); // check for reference equality
+                Assert.Null(after);
+            }
         }
 
-        public static IEnumerable<object[]> IndexOfTestData
+        public static IEnumerable<object[]> TryFindData_Utf8String_WithComparison() => Utf8SpanTests.TryFindData_Utf8Span_WithComparison();
+
+        [Theory]
+        [PlatformSpecific(TestPlatforms.Windows)]
+        [MemberData(nameof(TryFindData_Utf8String_WithComparison))]
+        public static void TryFind_Utf8String_WithComparison(ustring source, ustring searchTerm, StringComparison comparison, CultureInfo currentCulture, Range? expectedForwardMatch, Range? expectedBackwardMatch)
         {
-            get
-            {
-                yield return new object[] { Utf8String.Empty, default(Rune), -1 };
-                yield return new object[] { u8("Hello"), (Rune)'H', 0 };
-                yield return new object[] { u8("Hello"), (Rune)'h', -1 };
-                yield return new object[] { u8("Hello"), (Rune)'O', -1 };
-                yield return new object[] { u8("Hello"), (Rune)'o', 4 };
-                yield return new object[] { u8("Hello"), (Rune)'L', -1 };
-                yield return new object[] { u8("Hello"), (Rune)'l', 2 };
-                yield return new object[] { u8("\U00012345\U0010ABCD"), (Rune)0x00012345, 0 };
-                yield return new object[] { u8("\U00012345\U0010ABCD"), (Rune)0x0010ABCD, 4 };
-                yield return new object[] { u8("abc\ufffdef"), (Rune)'c', 2 };
-                yield return new object[] { u8("abc\ufffdef"), (Rune)'\ufffd', 3 };
-                yield return new object[] { u8("abc\ufffdef"), (Rune)'d', -1 };
-                yield return new object[] { u8("abc\ufffdef"), (Rune)'e', 6 };
+            if (source is null)
+            {
+                return; // don't null ref
             }
+
+            RunOnDedicatedThread(() =>
+            {
+                if (currentCulture != null)
+                {
+                    CultureInfo.CurrentCulture = currentCulture;
+                }
+
+                // First, search forward
+
+                bool wasFound = source.TryFind(searchTerm, comparison, out Range actualForwardMatch);
+                Assert.Equal(expectedForwardMatch.HasValue, wasFound);
+
+                if (wasFound)
+                {
+                    AssertRangesEqual(source.Length, expectedForwardMatch.Value, actualForwardMatch);
+                }
+
+                // Also check Contains / StartsWith / SplitOn
+
+                Assert.Equal(wasFound, source.Contains(searchTerm, comparison));
+                Assert.Equal(wasFound && source[..actualForwardMatch.Start].Length == 0, source.StartsWith(searchTerm, comparison));
+
+                (var before, var after) = source.SplitOn(searchTerm, comparison);
+                if (wasFound)
+                {
+                    Assert.Equal(source[..actualForwardMatch.Start], before);
+                    Assert.Equal(source[actualForwardMatch.End..], after);
+                }
+                else
+                {
+                    Assert.Same(source, before); // check for reference equality
+                    Assert.Null(after);
+                }
+
+                // Now search backward
+
+                wasFound = source.TryFindLast(searchTerm, comparison, out Range actualBackwardMatch);
+                Assert.Equal(expectedBackwardMatch.HasValue, wasFound);
+
+                if (wasFound)
+                {
+                    AssertRangesEqual(source.Length, expectedBackwardMatch.Value, actualBackwardMatch);
+                }
+
+                // Also check EndsWith / SplitOnLast
+
+                Assert.Equal(wasFound && source[actualBackwardMatch.End..].Length == 0, source.EndsWith(searchTerm, comparison));
+
+                (before, after) = source.SplitOnLast(searchTerm, comparison);
+                if (wasFound)
+                {
+                    Assert.Equal(source[..actualBackwardMatch.Start], before);
+                    Assert.Equal(source[actualBackwardMatch.End..], after);
+                }
+                else
+                {
+                    Assert.Same(source, before); // check for reference equality
+                    Assert.Null(after);
+                }
+            });
+        }
+
+        [Fact]
+        public static void TryFind_WithNullUtf8String_Throws()
+        {
+            static void RunTest(Action action)
+            {
+                var exception = Assert.Throws<ArgumentNullException>(action);
+                Assert.Equal("value", exception.ParamName);
+            }
+
+            ustring str = u8("Hello!");
+            const ustring value = null;
+            const StringComparison comparison = StringComparison.OrdinalIgnoreCase;
+
+            // Run this test for a bunch of methods, not simply TryFind
+
+            RunTest(() => str.Contains(value));
+            RunTest(() => str.Contains(value, comparison));
+            RunTest(() => str.EndsWith(value));
+            RunTest(() => str.EndsWith(value, comparison));
+            RunTest(() => str.SplitOn(value));
+            RunTest(() => str.SplitOn(value, comparison));
+            RunTest(() => str.SplitOnLast(value));
+            RunTest(() => str.SplitOnLast(value, comparison));
+            RunTest(() => str.StartsWith(value));
+            RunTest(() => str.StartsWith(value, comparison));
+            RunTest(() => str.TryFind(value, out _));
+            RunTest(() => str.TryFind(value, comparison, out _));
+            RunTest(() => str.TryFindLast(value, out _));
+            RunTest(() => str.TryFindLast(value, comparison, out _));
         }
     }
 }
diff --git a/src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Substring.cs b/src/libraries/System.Utf8String.Experimental/tests/System/Utf8StringTests.Substring.cs
deleted file mode 100644 (file)
index f17047c..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Reflection;
-using Xunit;
-
-using static System.Tests.Utf8TestUtilities;
-
-namespace System.Tests
-{
-    public unsafe partial class Utf8StringTests
-    {
-        [Theory]
-        [InlineData("Hello", 0, false, "Hello")]
-        [InlineData("Hello", 0, true, "")]
-        [InlineData("Hello", 2, false, "llo")]
-        [InlineData("Hello", 2, true, "lo")]
-        [InlineData("Hello", 5, false, "")]
-        [InlineData("Hello", 5, true, "Hello")]
-        [InlineData("", 0, true, "")]
-        [InlineData("", 0, false, "")]
-        public static void Substring_Index(string sAsString, int indexValue, bool fromEnd, string expectedAsString)
-        {
-            Index index = new Index(indexValue, fromEnd);
-
-            void Substring_IndexCore(Utf8String s, Utf8String expected)
-            {
-                Range r = new Range(index, ^0);
-                Assert.Equal(expected, s[r]);
-
-                if (index.Value == 0)
-                {
-                    Assert.Same(index.IsFromEnd ? Utf8String.Empty : s, s[r]);
-                }
-
-                if (index.Value == s.Length)
-                {
-                    Assert.Same(index.IsFromEnd ? s : Utf8String.Empty, s[r]);
-                }
-            };
-
-            Substring_IndexCore(new Utf8String(sAsString), new Utf8String(expectedAsString));
-        }
-
-        [Theory]
-        [InlineData("Hello", 0, 5, "Hello")]
-        [InlineData("Hello", 0, 3, "Hel")]
-        [InlineData("Hello", 2, 3, "llo")]
-        [InlineData("Hello", 5, 0, "")]
-        [InlineData("", 0, 0, "")]
-        public static void Substring_Int(string sAsString, int startIndex, int length, string expectedAsString)
-        {
-            void Substring_IntCore(Utf8String s, Utf8String expected)
-            {
-                if (startIndex + length == s.Length)
-                {
-                    Assert.Equal(expected, s.Substring(startIndex));
-                    Assert.Equal(expected, new Utf8String(s.AsBytes(startIndex)));
-
-                    if (length == 0)
-                    {
-                        Assert.Same(Utf8String.Empty, s.Substring(startIndex));
-                    }
-                }
-                Assert.Equal(expected, s.Substring(startIndex, length));
-
-                Assert.Equal(expected, new Utf8String(s.AsBytes(startIndex, length)));
-
-                if (length == s.Length)
-                {
-                    Assert.Same(s, s.Substring(startIndex));
-                    Assert.Same(s, s.Substring(startIndex, length));
-                }
-                else if (length == 0)
-                {
-                    Assert.Same(Utf8String.Empty, s.Substring(startIndex, length));
-                }
-            };
-
-            Substring_IntCore(new Utf8String(sAsString), new Utf8String(expectedAsString));
-        }
-
-        [Fact]
-        public static void Substring_Range()
-        {
-            void Substring_RangeCore(Utf8String s, Range range, Utf8String expected)
-            {
-                Assert.Equal(expected, s[range]);
-
-                if (expected.Length == s.Length)
-                {
-                    Assert.Same(s, s[range]);
-                }
-
-                if (expected.Length == 0)
-                {
-                    Assert.Same(Utf8String.Empty, s[range]);
-                }
-            };
-
-            Substring_RangeCore(u8("Hello"), .., u8("Hello"));
-            Substring_RangeCore(u8("Hello"), 0..3, u8("Hel"));
-            Substring_RangeCore(u8("Hello"), ..^4, u8("H"));
-            Substring_RangeCore(u8("Hello"), 1.., u8("ello"));
-            Substring_RangeCore(u8("Hello"), ..^5, Utf8String.Empty);
-        }
-
-        [Fact]
-        public static void Substring_Invalid()
-        {
-            // Start index < 0
-            AssertExtensions.Throws<ArgumentOutOfRangeException>("startIndex", () => u8("foo").Substring(-1));
-            AssertExtensions.Throws<ArgumentOutOfRangeException>("startIndex", () => u8("foo").Substring(-1, 0));
-
-            // Start index > string.Length
-            AssertExtensions.Throws<ArgumentOutOfRangeException>("startIndex", () => u8("foo").Substring(4));
-            AssertExtensions.Throws<ArgumentOutOfRangeException>("startIndex", () => u8("foo").Substring(4, 0));
-
-            // Length < 0 or length > string.Length
-            AssertExtensions.Throws<ArgumentOutOfRangeException>("length", () => u8("foo").Substring(0, -1));
-            AssertExtensions.Throws<ArgumentOutOfRangeException>("length", () => u8("foo").Substring(0, 4));
-
-            // Start index + length > string.Length
-            AssertExtensions.Throws<ArgumentOutOfRangeException>("length", () => u8("foo").Substring(3, 2));
-            AssertExtensions.Throws<ArgumentOutOfRangeException>("length", () => u8("foo").Substring(2, 2));
-        }
-    }
-}
index fb62b31..17da58a 100644 (file)
@@ -1,7 +1,9 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
+using System.Collections.Generic;
+using System.Globalization;
 using Xunit;
 
 using static System.Tests.Utf8TestUtilities;
@@ -45,32 +47,125 @@ namespace System.Tests
             // Static methods
 
             Assert.Equal(expected, Utf8String.Equals(a, b));
+            Assert.Equal(expected, Utf8String.Equals(a, b, StringComparison.Ordinal));
 
             // Instance methods
 
             if (a != null)
             {
-                Assert.Equal(expected, a.Equals(b));
                 Assert.Equal(expected, a.Equals((object)b));
+                Assert.Equal(expected, a.Equals(b));
+                Assert.Equal(expected, a.Equals(b, StringComparison.Ordinal));
+            }
+        }
+
+        [Fact]
+        public static void GetHashCode_Ordinal()
+        {
+            // Generate 17 all-null strings and make sure they have unique hash codes.
+            // Assuming Marvin32 is a good PRF and has a full 32-bit output domain, we should
+            // expect this test to fail only once every ~30 million runs due to the birthday paradox.
+            // That should be good enough for inclusion as a unit test.
+
+            HashSet<int> seenHashCodes = new HashSet<int>();
+
+            for (int i = 0; i <= 16; i++)
+            {
+                Utf8String ustr = new Utf8String(new byte[i]);
+
+                Assert.True(seenHashCodes.Add(ustr.GetHashCode()), "This hash code was previously seen.");
             }
         }
 
         [Fact]
-        public static void GetHashCode_ReturnsRandomized()
+        [PlatformSpecific(TestPlatforms.Windows)]
+        public static void GetHashCode_WithComparison()
         {
-            Utf8String a = u8("Hello");
-            Utf8String b = new Utf8String(a.AsBytes());
+            // Since hash code generation is randomized, it's possible (though unlikely) that
+            // we might see unanticipated collisions. It's ok if this unit test fails once in
+            // every few million runs, but if the unit test becomes truly flaky then that would
+            // be indicative of a larger problem with hash code generation.
+            //
+            // These tests also make sure that the hash code is computed over the buffer rather
+            // than over the reference.
+
+            // Ordinal
+
+            {
+                Utf8String ustr = u8("ababaaAA");
+
+                Assert.Equal(ustr[0..2].GetHashCode(StringComparison.Ordinal), ustr[2..4].GetHashCode(StringComparison.Ordinal));
+                Assert.NotEqual(ustr[4..6].GetHashCode(StringComparison.Ordinal), ustr[6..8].GetHashCode(StringComparison.Ordinal));
+                Assert.Equal(Utf8String.Empty.GetHashCode(StringComparison.Ordinal), ustr[^0..].GetHashCode(StringComparison.Ordinal));
+            }
 
-            Assert.NotSame(a, b);
-            Assert.Equal(a.GetHashCode(), b.GetHashCode());
+            // OrdinalIgnoreCase
 
-            Utf8String c = u8("Goodbye");
-            Utf8String d = new Utf8String(c.AsBytes());
+            {
+                Utf8String ustr = u8("ababaaAA");
+
+                Assert.Equal(ustr[0..2].GetHashCode(StringComparison.OrdinalIgnoreCase), ustr[2..4].GetHashCode(StringComparison.OrdinalIgnoreCase));
+                Assert.Equal(ustr[4..6].GetHashCode(StringComparison.OrdinalIgnoreCase), ustr[6..8].GetHashCode(StringComparison.OrdinalIgnoreCase));
+                Assert.NotEqual(ustr[0..2].GetHashCode(StringComparison.OrdinalIgnoreCase), ustr[6..8].GetHashCode(StringComparison.OrdinalIgnoreCase));
+                Assert.Equal(Utf8String.Empty.GetHashCode(StringComparison.OrdinalIgnoreCase), ustr[^0..].GetHashCode(StringComparison.OrdinalIgnoreCase));
+            }
+
+            // InvariantCulture
+
+            {
+                Utf8String ustr = u8("ae\u00e6AE\u00c6"); // U+00E6 = 'æ' LATIN SMALL LETTER AE, U+00E6 = 'Æ' LATIN CAPITAL LETTER AE
+
+                Assert.Equal(ustr[0..2].GetHashCode(StringComparison.InvariantCulture), ustr[2..4].GetHashCode(StringComparison.InvariantCulture));
+                Assert.NotEqual(ustr[0..2].GetHashCode(StringComparison.InvariantCulture), ustr[4..6].GetHashCode(StringComparison.InvariantCulture));
+                Assert.Equal(Utf8String.Empty.GetHashCode(StringComparison.InvariantCulture), ustr[^0..].GetHashCode(StringComparison.InvariantCulture));
+            }
 
-            Assert.NotSame(c, d);
-            Assert.Equal(c.GetHashCode(), d.GetHashCode());
+            // InvariantCultureIgnoreCase
 
-            Assert.NotEqual(a.GetHashCode(), c.GetHashCode());
+            {
+                Utf8String ustr = u8("ae\u00e6AE\u00c6EA");
+
+                Assert.Equal(ustr[0..2].GetHashCode(StringComparison.InvariantCultureIgnoreCase), ustr[2..4].GetHashCode(StringComparison.InvariantCultureIgnoreCase));
+                Assert.Equal(ustr[0..2].GetHashCode(StringComparison.InvariantCultureIgnoreCase), ustr[6..8].GetHashCode(StringComparison.InvariantCultureIgnoreCase));
+                Assert.NotEqual(ustr[0..2].GetHashCode(StringComparison.InvariantCultureIgnoreCase), ustr[8..10].GetHashCode(StringComparison.InvariantCultureIgnoreCase));
+                Assert.Equal(Utf8String.Empty.GetHashCode(StringComparison.InvariantCultureIgnoreCase), ustr[^0..].GetHashCode(StringComparison.InvariantCultureIgnoreCase));
+            }
+
+            // Invariant culture should not match Turkish I case conversion
+
+            {
+                Utf8String ustr = u8("i\u0130"); // U+0130 = 'İ' LATIN CAPITAL LETTER I WITH DOT ABOVE
+
+                Assert.NotEqual(ustr[0..1].GetHashCode(StringComparison.InvariantCultureIgnoreCase), ustr[1..3].GetHashCode(StringComparison.InvariantCultureIgnoreCase));
+            }
+
+            // CurrentCulture (we'll use tr-TR)
+
+            RunOnDedicatedThread(() =>
+            {
+                Utf8String ustr = u8("i\u0131\u0130Ii\u0131\u0130I"); // U+0131 = 'ı' LATIN SMALL LETTER DOTLESS I
+
+                CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR");
+
+                Assert.Equal(ustr[0..6].GetHashCode(StringComparison.CurrentCulture), ustr[6..12].GetHashCode(StringComparison.CurrentCulture));
+                Assert.NotEqual(ustr[0..1].GetHashCode(StringComparison.CurrentCulture), ustr[1..3].GetHashCode(StringComparison.CurrentCulture));
+                Assert.Equal(Utf8String.Empty.GetHashCode(StringComparison.CurrentCulture), ustr[^0..].GetHashCode(StringComparison.CurrentCulture));
+            });
+
+            // CurrentCultureIgnoreCase (we'll use tr-TR)
+
+            RunOnDedicatedThread(() =>
+            {
+                Utf8String ustr = u8("i\u0131\u0130Ii\u0131\u0130I");
+
+                CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR");
+
+                Assert.Equal(ustr[0..6].GetHashCode(StringComparison.CurrentCultureIgnoreCase), ustr[6..12].GetHashCode(StringComparison.CurrentCultureIgnoreCase));
+                Assert.NotEqual(ustr[0..1].GetHashCode(StringComparison.CurrentCultureIgnoreCase), ustr[1..3].GetHashCode(StringComparison.CurrentCultureIgnoreCase)); // 'i' shouldn't match 'ı'
+                Assert.Equal(ustr[0..1].GetHashCode(StringComparison.CurrentCultureIgnoreCase), ustr[3..5].GetHashCode(StringComparison.CurrentCultureIgnoreCase)); // 'i' should match 'İ'
+                Assert.NotEqual(ustr[0..1].GetHashCode(StringComparison.CurrentCultureIgnoreCase), ustr[5..6].GetHashCode(StringComparison.CurrentCultureIgnoreCase)); // 'i' shouldn't match 'I'
+                Assert.Equal(Utf8String.Empty.GetHashCode(StringComparison.CurrentCultureIgnoreCase), ustr[^0..].GetHashCode(StringComparison.CurrentCultureIgnoreCase));
+            });
         }
 
         [Fact]
@@ -113,45 +208,50 @@ namespace System.Tests
         }
 
         [Theory]
+        [InlineData(null, true)]
         [InlineData("", true)]
+        [InlineData("\r\n", false)]
         [InlineData("not empty", false)]
         public static void IsNullOrEmpty(string value, bool expectedIsNullOrEmpty)
         {
-            Assert.Equal(expectedIsNullOrEmpty, Utf8String.IsNullOrEmpty(new Utf8String(value)));
+            Assert.Equal(expectedIsNullOrEmpty, Utf8String.IsNullOrEmpty(u8(value)));
         }
 
-        [Fact]
-        public static void IsNullOrEmpty_Null_ReturnsTrue()
+        [Theory]
+        [InlineData(null, true)]
+        [InlineData("", true)]
+        [InlineData(" \u2028\u2029\t\v", true)]
+        [InlineData(" x\r\n", false)]
+        [InlineData("\r\nhello\r\n", false)]
+        [InlineData("\r\n\0\r\n", false)]
+        [InlineData("\r\n\r\n", true)]
+        public static void IsNullOrWhiteSpace(string input, bool expected)
         {
-            Assert.True(Utf8String.IsNullOrEmpty(null));
+            Assert.Equal(expected, Utf8String.IsNullOrWhiteSpace(u8(input)));
         }
 
         [Fact]
         public static void ToByteArray_Empty()
         {
             Assert.Same(Array.Empty<byte>(), Utf8String.Empty.ToByteArray());
-            Assert.Same(Array.Empty<byte>(), u8("Hello!").ToByteArray(0, 0));
-            Assert.Same(Array.Empty<byte>(), u8("Hello!").ToByteArray(3, 0));
-            Assert.Same(Array.Empty<byte>(), u8("Hello!").ToByteArray(6, 0));
         }
 
         [Fact]
         public static void ToByteArray_NotEmpty()
         {
             Assert.Equal(new byte[] { (byte)'H', (byte)'i' }, u8("Hi").ToByteArray());
-            Assert.Equal(new byte[] { (byte)'l', (byte)'l', (byte)'o' }, u8("Hello!").ToByteArray(2, 3));
         }
 
-        [Theory]
-        [InlineData("", 1, 0, "startIndex")]
-        [InlineData("", 0, 1, "length")]
-        [InlineData("Hello", 5, 2, "length")]
-        [InlineData("Hello", 5, -1, "length")]
-        [InlineData("Hello", -1, 4, "startIndex")]
-        public static void ToByteArray_Invalid(string value, int startIndex, int length, string exceptionParamName)
+        [Fact]
+        public static void ToCharArray_NotEmpty()
         {
-            Utf8String utf8String = u8(value);
-            Assert.Throws<ArgumentOutOfRangeException>(exceptionParamName, () => utf8String.ToByteArray(startIndex, length));
+            Assert.Equal("Hi".ToCharArray(), u8("Hi").ToCharArray());
+        }
+
+        [Fact]
+        public static void ToCharArray_Empty()
+        {
+            Assert.Same(Array.Empty<char>(), Utf8String.Empty.ToCharArray());
         }
 
         [Theory]
@@ -162,17 +262,41 @@ namespace System.Tests
             Assert.Equal(value, u8(value).ToString());
         }
 
+        [Theory]
+        [InlineData("Hello", "6..")]
+        [InlineData("Hello", "3..7")]
+        [InlineData("Hello", "2..1")]
+        [InlineData("Hello", "^10..")]
+        public static void Indexer_Range_ArgOutOfRange_Throws(string strAsUtf16, string rangeAsString)
+        {
+            Utf8String utf8String = u8(strAsUtf16);
+            Range range = ParseRangeExpr(rangeAsString);
+
+            Assert.Throws<ArgumentOutOfRangeException>(() => utf8String[range]);
+        }
+
         [Fact]
-        public static void ToString_ReturnsUtf16_WithFixups()
+        public static void Indexer_Range_Success()
         {
-            Utf8String newString = new Utf8String("Hello");
+            Utf8String utf8String = u8("Hello\u0800world.");
 
-            fixed (byte* pNewString = newString)
-            {
-                pNewString[2] = 0xFF; // corrupt this data
-            }
+            Assert.Equal(u8("Hello"), utf8String[..5]);
+            Assert.Equal(u8("world."), utf8String[^6..]);
+            Assert.Equal(u8("o\u0800w"), utf8String[4..9]);
+
+            Assert.Same(utf8String, utf8String[..]); // don't allocate new instance if slicing to entire string
+            Assert.Same(Utf8String.Empty, utf8String[1..1]); // don't allocare new zero-length string instane
+            Assert.Same(Utf8String.Empty, utf8String[6..6]); // ok to have a zero-length slice within a multi-byte sequence
+        }
+
+        [Fact]
+        public static void Indexer_Range_TriesToSplitMultiByteSequence_Throws()
+        {
+            Utf8String utf8String = u8("Hello\u0800world.");
 
-            Assert.Equal("He\uFFFDlo", newString.ToString());
+            Assert.Throws<InvalidOperationException>(() => utf8String[..6]);
+            Assert.Throws<InvalidOperationException>(() => utf8String[6..]);
+            Assert.Throws<InvalidOperationException>(() => utf8String[7..8]);
         }
     }
 }
index 96fe60f..591dd8f 100644 (file)
@@ -28,11 +28,6 @@ namespace System.Tests
             });
         }
 
-        public static int GetByteLength(this Utf8String value)
-        {
-            return value.AsBytes().Length;
-        }
-
         public unsafe static bool IsNull(this Utf8Span span)
         {
             return Unsafe.AreSame(ref Unsafe.AsRef<byte>(null), ref MemoryMarshal.GetReference(span.Bytes));
@@ -135,7 +130,7 @@ namespace System.Tests
         /// <summary>
         /// Mimics returning a literal <see cref="Utf8String"/> instance.
         /// </summary>
-        public static unsafe Utf8String u8(string str)
+        public static Utf8String u8(string str)
         {
             if (str is null)
             {
@@ -171,16 +166,8 @@ namespace System.Tests
             Assert.True(memStream.TryGetBuffer(out ArraySegment<byte> buffer));
 
             // Now allocate a UTF-8 string instance and set this as the contents.
-            // We do it this way rather than go through a public ctor because we don't
-            // want the "control" part of our unit tests to depend on the code under test.
-
-            Utf8String newUtf8String = _utf8StringFactory.Value(buffer.Count);
-            fixed (byte* pNewUtf8String = newUtf8String)
-            {
-                buffer.AsSpan().CopyTo(new Span<byte>(pNewUtf8String, newUtf8String.GetByteLength()));
-            }
 
-            return newUtf8String;
+            return Utf8String.UnsafeCreateWithoutValidation(buffer);
         }
 
         public unsafe static Range GetRangeOfSubspan<T>(ReadOnlySpan<T> outerSpan, ReadOnlySpan<T> innerSpan)