diff --git a/pkg/Microsoft.Private.PackageBaseline/packageIndex.json b/pkg/Microsoft.Private.PackageBaseline/packageIndex.json index 0f193a2b57ef..415e45dd7e84 100644 --- a/pkg/Microsoft.Private.PackageBaseline/packageIndex.json +++ b/pkg/Microsoft.Private.PackageBaseline/packageIndex.json @@ -5307,6 +5307,12 @@ "uap10.0.16299": "4.0.1.0" } }, + "System.Utf8String.Experimental": { + "InboxOn": {}, + "AssemblyVersionInPackageVersion": { + "4.0.0.0": "4.6.0" + } + }, "System.ValueTuple": { "StableVersions": [ "4.3.0", diff --git a/pkg/descriptions.json b/pkg/descriptions.json index 0512d12459de..e3b4369e6e8f 100644 --- a/pkg/descriptions.json +++ b/pkg/descriptions.json @@ -2131,6 +2131,14 @@ "System.Transactions.TransactionScope" ] }, + { + "Name": "System.Utf8String.Experimental", + "Description": "Provides types for representation of UTF-8 string data.", + "CommonTypes": [ + "System.Char8", + "System.Utf8String" + ] + }, { "Name": "System.ValueTuple", "Description": "Provides the System.ValueTuple structs, which implement the underlying types for tuples in C# and Visual Basic.", diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 6653ddf22175..7063f069799d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -31,4 +31,13 @@ + + + + + diff --git a/src/System.Utf8String.Experimental/Directory.Build.props b/src/System.Utf8String.Experimental/Directory.Build.props new file mode 100644 index 000000000000..25e3ba27477f --- /dev/null +++ b/src/System.Utf8String.Experimental/Directory.Build.props @@ -0,0 +1,11 @@ + + + + + 4.0.0.0 + + Open + + true + + diff --git a/src/System.Utf8String.Experimental/System.Utf8String.Experimental.sln b/src/System.Utf8String.Experimental/System.Utf8String.Experimental.sln new file mode 100644 index 000000000000..2d4bdcc1c43a --- /dev/null +++ b/src/System.Utf8String.Experimental/System.Utf8String.Experimental.sln @@ -0,0 +1,50 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27213.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Utf8String.Experimental.Tests", "tests\System.Utf8String.Experimental.Tests.csproj", "{72E9FB32-4692-4692-A10B-9F053F8F1A88}" + ProjectSection(ProjectDependencies) = postProject + {D4266847-6692-481B-9459-6141DB7DA339} = {D4266847-6692-481B-9459-6141DB7DA339} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Utf8String.Experimental", "src\System.Utf8String.Experimental.csproj", "{D4266847-6692-481B-9459-6141DB7DA339}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Utf8String.Experimental", "ref\System.Utf8String.Experimental.csproj", "{7AF57E6B-2CED-45C9-8BCA-5BBA60D018E0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7EC8921F-E96F-445B-AA33-453515641D93}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{8691446A-CA54-49FD-87B9-57A0C6B48095}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FEB087F5-EF72-429D-8A0E-7636B84A1537}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D4266847-6692-481B-9459-6141DB7DA339}.Debug|Any CPU.ActiveCfg = netcoreapp-Debug|Any CPU + {D4266847-6692-481B-9459-6141DB7DA339}.Debug|Any CPU.Build.0 = netcoreapp-Debug|Any CPU + {D4266847-6692-481B-9459-6141DB7DA339}.Release|Any CPU.ActiveCfg = netcoreapp-Release|Any CPU + {D4266847-6692-481B-9459-6141DB7DA339}.Release|Any CPU.Build.0 = netcoreapp-Release|Any CPU + {7AF57E6B-2CED-45C9-8BCA-5BBA60D018E0}.Debug|Any CPU.ActiveCfg = netcoreapp-Debug|Any CPU + {7AF57E6B-2CED-45C9-8BCA-5BBA60D018E0}.Debug|Any CPU.Build.0 = netcoreapp-Debug|Any CPU + {7AF57E6B-2CED-45C9-8BCA-5BBA60D018E0}.Release|Any CPU.ActiveCfg = netcoreapp-Release|Any CPU + {7AF57E6B-2CED-45C9-8BCA-5BBA60D018E0}.Release|Any CPU.Build.0 = netcoreapp-Release|Any CPU + {72E9FB32-4692-4692-A10B-9F053F8F1A88}.Debug|Any CPU.ActiveCfg = netcoreapp-Debug|Any CPU + {72E9FB32-4692-4692-A10B-9F053F8F1A88}.Debug|Any CPU.Build.0 = netcoreapp-Debug|Any CPU + {72E9FB32-4692-4692-A10B-9F053F8F1A88}.Release|Any CPU.ActiveCfg = netcoreapp-Release|Any CPU + {72E9FB32-4692-4692-A10B-9F053F8F1A88}.Release|Any CPU.Build.0 = netcoreapp-Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D4266847-6692-481B-9459-6141DB7DA339} = {7EC8921F-E96F-445B-AA33-453515641D93} + {7AF57E6B-2CED-45C9-8BCA-5BBA60D018E0} = {8691446A-CA54-49FD-87B9-57A0C6B48095} + {72E9FB32-4692-4692-A10B-9F053F8F1A88} = {FEB087F5-EF72-429D-8A0E-7636B84A1537} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7196F6AB-8F22-4E4D-B6D1-3C2CFF86229C} + EndGlobalSection +EndGlobal diff --git a/src/System.Utf8String.Experimental/pkg/System.Utf8String.Experimental.pkgproj b/src/System.Utf8String.Experimental/pkg/System.Utf8String.Experimental.pkgproj new file mode 100644 index 000000000000..016952009282 --- /dev/null +++ b/src/System.Utf8String.Experimental/pkg/System.Utf8String.Experimental.pkgproj @@ -0,0 +1,11 @@ + + + + + + netcoreapp3.0; + + + + + diff --git a/src/System.Utf8String.Experimental/ref/Configurations.props b/src/System.Utf8String.Experimental/ref/Configurations.props new file mode 100644 index 000000000000..d3ac8a63c74a --- /dev/null +++ b/src/System.Utf8String.Experimental/ref/Configurations.props @@ -0,0 +1,8 @@ + + + + + netcoreapp; + + + diff --git a/src/System.Utf8String.Experimental/ref/System.Utf8String.Experimental.csproj b/src/System.Utf8String.Experimental/ref/System.Utf8String.Experimental.csproj new file mode 100644 index 000000000000..6563d2c44bdc --- /dev/null +++ b/src/System.Utf8String.Experimental/ref/System.Utf8String.Experimental.csproj @@ -0,0 +1,16 @@ + + + true + {7AF57E6B-2CED-45C9-8BCA-5BBA60D018E0} + netcoreapp-Debug;netcoreapp-Release + + + + + + + + + + + diff --git a/src/System.Utf8String.Experimental/ref/System.Utf8String.cs b/src/System.Utf8String.Experimental/ref/System.Utf8String.cs new file mode 100644 index 000000000000..7e9bc8f56e95 --- /dev/null +++ b/src/System.Utf8String.Experimental/ref/System.Utf8String.cs @@ -0,0 +1,125 @@ +// 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. +// ------------------------------------------------------------------------------ +// Changes to this file must follow the http://aka.ms/api-review process. +// ------------------------------------------------------------------------------ + +namespace System +{ + public readonly partial struct Char8 : IComparable, IEquatable + { + private readonly int _dummy; + public static bool operator ==(Char8 left, Char8 right) => throw null; + public static bool operator !=(Char8 left, Char8 right) => throw null; + public static bool operator <(Char8 left, Char8 right) => throw null; + public static bool operator <=(Char8 left, Char8 right) => throw null; + public static bool operator >(Char8 left, Char8 right) => throw null; + public static bool operator >=(Char8 left, Char8 right) => throw null; + public static implicit operator byte(Char8 value) => throw null; + [CLSCompliant(false)] + public static explicit operator sbyte(Char8 value) => throw null; + public static explicit operator char(Char8 value) => throw null; + public static implicit operator short(Char8 value) => throw null; + [CLSCompliant(false)] + public static implicit operator ushort(Char8 value) => throw null; + public static implicit operator int(Char8 value) => throw null; + [CLSCompliant(false)] + public static implicit operator uint(Char8 value) => throw null; + public static implicit operator long(Char8 value) => throw null; + [CLSCompliant(false)] + public static implicit operator ulong(Char8 value) => throw null; + public static implicit operator Char8(byte value) => throw null; + [CLSCompliant(false)] + public static explicit operator Char8(sbyte value) => throw null; + public static explicit operator Char8(char value) => throw null; + public static explicit operator Char8(short value) => throw null; + [CLSCompliant(false)] + public static explicit operator Char8(ushort value) => throw null; + public static explicit operator Char8(int value) => throw null; + [CLSCompliant(false)] + public static explicit operator Char8(uint value) => throw null; + public static explicit operator Char8(long value) => throw null; + [CLSCompliant(false)] + public static explicit operator Char8(ulong value) => throw null; + public int CompareTo(Char8 other) => throw null; + public override bool Equals(object obj) => throw null; + public bool Equals(Char8 other) => throw null; + public override int GetHashCode() => throw null; + public override string ToString() => throw null; + } + public static partial class Utf8Extensions + { + public static ReadOnlySpan AsBytes(this ReadOnlySpan text) => throw null; + public static ReadOnlySpan AsBytes(this Utf8String text) => throw null; + public static ReadOnlySpan AsBytes(this Utf8String text, int start) => throw null; + public static ReadOnlySpan AsBytes(this Utf8String text, int start, int length) => throw null; + public static ReadOnlySpan AsSpan(this Utf8String text) => throw null; + public static ReadOnlySpan AsSpan(this Utf8String text, int start) => throw null; + public static ReadOnlySpan AsSpan(this Utf8String text, int start, int length) => throw null; + public static ReadOnlyMemory AsMemory(this Utf8String text) => throw null; + public static ReadOnlyMemory AsMemory(this Utf8String text, int start) => throw null; + public static ReadOnlyMemory AsMemory(this Utf8String text, Index startIndex) => throw null; + public static ReadOnlyMemory AsMemory(this Utf8String text, int start, int length) => throw null; + public static ReadOnlyMemory AsMemory(this Utf8String text, Range range) => throw null; + public static ReadOnlyMemory AsMemoryBytes(this Utf8String text) => throw null; + public static ReadOnlyMemory AsMemoryBytes(this Utf8String text, int start) => throw null; + public static ReadOnlyMemory AsMemoryBytes(this Utf8String text, Index startIndex) => throw null; + public static ReadOnlyMemory AsMemoryBytes(this Utf8String text, int start, int length) => throw null; + public static ReadOnlyMemory AsMemoryBytes(this Utf8String text, Range range) => throw null; + } + public sealed partial class Utf8String : IEquatable + { + public static readonly Utf8String Empty; + public Utf8String(ReadOnlySpan value) { } + public Utf8String(byte[] value, int startIndex, int length) { } + [CLSCompliant(false)] + public unsafe Utf8String(byte* value) { } + public Utf8String(ReadOnlySpan value) { } + public Utf8String(char[] value, int startIndex, int length) { } + [CLSCompliant(false)] + public unsafe Utf8String(char* value) { } + public Utf8String(string value) { } + public static explicit operator ReadOnlySpan(Utf8String value) => throw null; + public static implicit operator ReadOnlySpan(Utf8String value) => throw null; + public static bool operator ==(Utf8String left, Utf8String right) => throw null; + public static bool operator !=(Utf8String left, Utf8String right) => throw null; + public Char8 this[Index index] => throw null; + public Char8 this[int index] => throw null; + public Utf8String this[Range range] => throw null; + public int Length => throw null; + public bool Contains(char value) => throw null; + public bool Contains(System.Text.Rune value) => throw null; + public bool EndsWith(char value) => throw null; + public bool EndsWith(System.Text.Rune value) => throw null; + public override bool Equals(object obj) => throw null; + public bool Equals(Utf8String value) => throw null; + public static bool Equals(Utf8String left, Utf8String right) => throw null; + public override int GetHashCode() => throw null; + [ComponentModel.EditorBrowsable(ComponentModel.EditorBrowsableState.Never)] // for compiler use only + 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(Utf8String value) => throw null; + public bool StartsWith(char value) => throw null; + public bool StartsWith(System.Text.Rune value) => throw null; + public Utf8String Substring(Index startIndex) => throw null; + public Utf8String Substring(int startIndex) => throw null; + public Utf8String Substring(int startIndex, int length) => throw null; + public Utf8String Substring(Range range) => throw null; + public byte[] ToByteArray() => throw null; + public byte[] ToByteArray(int startIndex, int length) => throw null; + public override string ToString() => throw null; + } +} +namespace System.Net.Http +{ + public sealed partial class Utf8StringContent : System.Net.Http.HttpContent + { + public Utf8StringContent(Utf8String content) { } + public Utf8StringContent(Utf8String content, string mediaType) { } + protected override System.Threading.Tasks.Task CreateContentReadStreamAsync() => throw null; + protected override System.Threading.Tasks.Task SerializeToStreamAsync(System.IO.Stream stream, System.Net.TransportContext context) => throw null; + protected override bool TryComputeLength(out long length) => throw null; + } +} diff --git a/src/System.Utf8String.Experimental/src/Configurations.props b/src/System.Utf8String.Experimental/src/Configurations.props new file mode 100644 index 000000000000..e75400d142ff --- /dev/null +++ b/src/System.Utf8String.Experimental/src/Configurations.props @@ -0,0 +1,9 @@ + + + + + netcoreapp-Windows_NT; + netcoreapp-Unix; + + + diff --git a/src/System.Utf8String.Experimental/src/Resources/Strings.resx b/src/System.Utf8String.Experimental/src/Resources/Strings.resx new file mode 100644 index 000000000000..1af7de150c99 --- /dev/null +++ b/src/System.Utf8String.Experimental/src/Resources/Strings.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/System.Utf8String.Experimental/src/System.Utf8String.Experimental.csproj b/src/System.Utf8String.Experimental/src/System.Utf8String.Experimental.csproj new file mode 100644 index 000000000000..964097b580ae --- /dev/null +++ b/src/System.Utf8String.Experimental/src/System.Utf8String.Experimental.csproj @@ -0,0 +1,19 @@ + + + {D4266847-6692-481B-9459-6141DB7DA339} + true + true + netcoreapp-Unix-Debug;netcoreapp-Unix-Release;netcoreapp-Windows_NT-Debug;netcoreapp-Windows_NT-Release; + System + + + + + + + + + + + + diff --git a/src/System.Utf8String.Experimental/src/System/IO/Utf8StringStream.cs b/src/System.Utf8String.Experimental/src/System/IO/Utf8StringStream.cs new file mode 100644 index 000000000000..c2ffa238f458 --- /dev/null +++ b/src/System.Utf8String.Experimental/src/System/IO/Utf8StringStream.cs @@ -0,0 +1,133 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace System.IO +{ + internal sealed class Utf8StringStream : Stream + { + private readonly Utf8String _content; + private int _position; + + public Utf8StringStream(Utf8String content) + { + _content = content ?? Utf8String.Empty; + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanTimeout => true; + + public override bool CanWrite => false; + + public override long Length => _content.Length; + + public override long Position + { + get => _position; + set + { + if ((ulong)value > (uint)_content.Length) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _position = (int)value; + } + } + + public override void Flush() + { + /* no-op */ + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + /* no-op */ + return Task.CompletedTask; + } + + public override int Read(byte[] buffer, int offset, int count) + { + return Read(new Span(buffer, offset, count)); + } + + public override int Read(Span buffer) + { + ReadOnlySpan contentToWrite = _content.AsBytes(_position); + if (buffer.Length < contentToWrite.Length) + { + contentToWrite = contentToWrite.Slice(buffer.Length); + } + + contentToWrite.CopyTo(buffer); + _position += contentToWrite.Length; + + return contentToWrite.Length; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return Task.FromResult(Read(new Span(buffer, offset, count))); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return new ValueTask(Read(buffer.Span)); + } + + public override int ReadByte() + { + int position = _position; + if ((uint)position >= (uint)_content.Length) + { + return -1; + } + + _position++; + return _content[position]; + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + break; + case SeekOrigin.Current: + offset += _position; + break; + case SeekOrigin.End: + offset += _content.Length; + break; + default: + throw new ArgumentOutOfRangeException(nameof(origin)); + } + + if ((ulong)offset > (uint)_content.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + _position = (int)offset; + return offset; + } + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(); + + public override void WriteByte(byte value) => throw new NotSupportedException(); + } +} diff --git a/src/System.Utf8String.Experimental/src/System/Net/Http/Utf8StringContent.cs b/src/System.Utf8String.Experimental/src/System/Net/Http/Utf8StringContent.cs new file mode 100644 index 000000000000..18b36eed9f0e --- /dev/null +++ b/src/System.Utf8String.Experimental/src/System/Net/Http/Utf8StringContent.cs @@ -0,0 +1,55 @@ +// 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.IO; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + public sealed class Utf8StringContent : HttpContent + { + private const string DefaultMediaType = "text/plain"; + + private readonly Utf8String _content; + + public Utf8StringContent(Utf8String content) + : this(content, mediaType: null) + { + } + + public Utf8StringContent(Utf8String content, string mediaType) + { + if (content is null) + { + throw new ArgumentNullException(nameof(content)); + } + + _content = content; + + // Initialize the 'Content-Type' header with information provided by parameters. + + Headers.ContentType = new MediaTypeHeaderValue(mediaType ?? DefaultMediaType) + { + CharSet = "utf-8" // Encoding.UTF8.WebName + }; + } + + protected override Task CreateContentReadStreamAsync() + { + return Task.FromResult(new Utf8StringStream(_content)); + } + + protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + return stream.WriteAsync(_content.AsMemoryBytes()).AsTask(); + } + + protected override bool TryComputeLength(out long length) + { + length = _content.Length; + return true; + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/Configurations.props b/src/System.Utf8String.Experimental/tests/Configurations.props new file mode 100644 index 000000000000..d3ac8a63c74a --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/Configurations.props @@ -0,0 +1,8 @@ + + + + + netcoreapp; + + + diff --git a/src/System.Utf8String.Experimental/tests/System.Utf8String.Experimental.Tests.csproj b/src/System.Utf8String.Experimental/tests/System.Utf8String.Experimental.Tests.csproj new file mode 100644 index 000000000000..5ce4935e8ebf --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System.Utf8String.Experimental.Tests.csproj @@ -0,0 +1,27 @@ + + + true + {72E9FB32-4692-4692-A10B-9F053F8F1A88} + true + netcoreapp-Debug;netcoreapp-Release; + true + System + + + + true + + + + + + + + + + + + + + + diff --git a/src/System.Utf8String.Experimental/tests/System/Char8Tests.cs b/src/System.Utf8String.Experimental/tests/System/Char8Tests.cs new file mode 100644 index 000000000000..7f3a132af7f6 --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System/Char8Tests.cs @@ -0,0 +1,112 @@ +// 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.Globalization; +using Xunit; + +namespace System.Tests +{ + public unsafe partial class Char8Tests + { + [Theory] + [InlineData(10, 20, -1)] + [InlineData(20, 10, 1)] + [InlineData(30, 30, 0)] + public static void CompareTo(Char8 a, Char8 b, int expectedSign) + { + Assert.Equal(expectedSign, Math.Sign(a.CompareTo(b))); + } + + [Theory] + [InlineData(-1)] + [InlineData(0xFF)] + [InlineData(0x80)] + [InlineData(0x00)] + [InlineData(0x1234)] + [InlineData(0x12345678)] + [InlineData(0x1234567812345678)] + public static void CastOperators(long value) + { + // Only the low byte is preserved when casting through Char8. + + Assert.Equal((byte)value, (byte)(Char8)(byte)value); + Assert.Equal((sbyte)value, (sbyte)(Char8)(sbyte)value); + Assert.Equal((char)(value & 0xFF), (char)(Char8)(char)value); + Assert.Equal((short)(value & 0xFF), (short)(Char8)(short)value); + Assert.Equal((ushort)(value & 0xFF), (ushort)(Char8)(ushort)value); + Assert.Equal((int)(value & 0xFF), (int)(Char8)(int)value); + Assert.Equal((uint)(value & 0xFF), (uint)(Char8)(uint)value); + Assert.Equal((long)(value & 0xFF), (long)(Char8)(long)value); + Assert.Equal((ulong)(value & 0xFF), (ulong)(Char8)(ulong)value); + } + + [Fact] + public static void EqualsObject() + { + Assert.False(((Char8)42).Equals((object)null)); + Assert.False(((Char8)42).Equals((object)(int)42)); + Assert.False(((Char8)42).Equals((object)(Char8)43)); + Assert.True(((Char8)42).Equals((object)(Char8)42)); + } + + [Fact] + public static void EqualsChar8() + { + Assert.True(((Char8)42).Equals(42)); // implicit cast to Char8 + Assert.False(((Char8)42).Equals(43)); // implicit cast to Char8 + } + + [Fact] + public static void GetHashCode_ReturnsValue() + { + for (int i = 0; i <= byte.MaxValue; i++) + { + Assert.Equal(i, ((Char8)i).GetHashCode()); + } + } + + [Theory] + [InlineData(10, 20, false)] + [InlineData(20, 10, false)] + [InlineData(30, 30, true)] + public static void OperatorEquals(Char8 a, Char8 b, bool expected) + { + Assert.Equal(expected, (Char8)a == (Char8)b); + Assert.NotEqual(expected, (Char8)a != (Char8)b); + } + + [Theory] + [InlineData(10, 20, true)] + [InlineData(20, 10, false)] + [InlineData(29, 30, true)] + [InlineData(30, 30, false)] + [InlineData(31, 30, false)] + public static void OperatorLessThan(Char8 a, Char8 b, bool expected) + { + Assert.Equal(expected, (Char8)a < (Char8)b); + Assert.NotEqual(expected, (Char8)a >= (Char8)b); + } + + [Theory] + [InlineData(10, 20, false)] + [InlineData(20, 10, true)] + [InlineData(29, 30, false)] + [InlineData(30, 30, false)] + [InlineData(31, 30, true)] + public static void OperatorGreaterThan(Char8 a, Char8 b, bool expected) + { + Assert.Equal(expected, (Char8)a > (Char8)b); + Assert.NotEqual(expected, (Char8)a <= (Char8)b); + } + + [Fact] + public static void ToString_ReturnsHexValue() + { + for (int i = 0; i <= byte.MaxValue; i++) + { + Assert.Equal(i.ToString("X2", CultureInfo.InvariantCulture), ((Char8)i).ToString()); + } + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/System/MemoryTests.cs b/src/System.Utf8String.Experimental/tests/System/MemoryTests.cs new file mode 100644 index 000000000000..2f168bf7c8ae --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System/MemoryTests.cs @@ -0,0 +1,156 @@ +// 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.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Xunit; + +using static System.Tests.Utf8TestUtilities; + +namespace System.Tests +{ + public partial class MemoryTests + { + [Fact] + public static void MemoryMarshal_TryGetArrayOfByte_Utf8String() + { + ReadOnlyMemory rom = u8("Hello").AsMemoryBytes(); + + Assert.False(MemoryMarshal.TryGetArray(rom, out ArraySegment segment)); + Assert.True(default(ArraySegment).Equals(segment)); + } + + [Fact] + public static void MemoryMarshal_TryGetArrayOfChar8_Utf8String() + { + ReadOnlyMemory rom = u8("Hello").AsMemory(); + + Assert.False(MemoryMarshal.TryGetArray(rom, out ArraySegment segment)); + Assert.True(default(ArraySegment).Equals(segment)); + } + + [Fact] + public unsafe static void MemoryOfByte_WithUtf8String_Pin() + { + Utf8String theString = u8("Hello"); + ReadOnlyMemory rom = theString.AsMemoryBytes(); + MemoryHandle memHandle = default; + try + { + memHandle = Unsafe.As, Memory>(ref rom).Pin(); + Assert.True(memHandle.Pointer == Unsafe.AsPointer(ref Unsafe.AsRef(in theString.GetPinnableReference()))); + } + finally + { + memHandle.Dispose(); + } + } + + [Fact] + public static void MemoryOfByte_WithUtf8String_ToString() + { + ReadOnlyMemory rom = u8("Hello").AsMemoryBytes(); + Assert.Equal("System.Memory[5]", Unsafe.As, Memory>(ref rom).ToString()); + } + + [Fact] + public unsafe static void MemoryOfChar8_WithUtf8String_Pin() + { + Utf8String theString = u8("Hello"); + ReadOnlyMemory rom = theString.AsMemory(); + MemoryHandle memHandle = default; + try + { + memHandle = Unsafe.As, Memory>(ref rom).Pin(); + Assert.True(memHandle.Pointer == Unsafe.AsPointer(ref Unsafe.AsRef(in theString.GetPinnableReference()))); + } + finally + { + memHandle.Dispose(); + } + } + + [Fact] + public static void MemoryOfChar8_WithUtf8String_ToString() + { + ReadOnlyMemory rom = u8("Hello").AsMemory(); + Assert.Equal("Hello", Unsafe.As, Memory>(ref rom).ToString()); + } + + [Fact] + public unsafe static void ReadOnlyMemoryOfByte_WithUtf8String_Pin() + { + Utf8String theString = u8("Hello"); + ReadOnlyMemory rom = theString.AsMemoryBytes(); + MemoryHandle memHandle = default; + try + { + memHandle = rom.Pin(); + Assert.True(memHandle.Pointer == Unsafe.AsPointer(ref Unsafe.AsRef(in theString.GetPinnableReference()))); + } + finally + { + memHandle.Dispose(); + } + } + + [Fact] + public static void ReadOnlyMemoryOfByte_WithUtf8String_ToString() + { + Assert.Equal("System.ReadOnlyMemory[5]", u8("Hello").AsMemoryBytes().ToString()); + } + + [Fact] + public unsafe static void ReadOnlyMemoryOfChar8_WithUtf8String_Pin() + { + Utf8String theString = u8("Hello"); + ReadOnlyMemory rom = theString.AsMemory(); + MemoryHandle memHandle = default; + try + { + memHandle = rom.Pin(); + Assert.True(memHandle.Pointer == Unsafe.AsPointer(ref Unsafe.AsRef(in theString.GetPinnableReference()))); + } + finally + { + memHandle.Dispose(); + } + } + + [Fact] + public static void ReadOnlyMemoryOfChar8_WithUtf8String_ToString() + { + Assert.Equal("Hello", u8("Hello").AsMemory().ToString()); + } + + [Fact] + public static void ReadOnlySpanOfByte_ToString() + { + ReadOnlySpan span = stackalloc byte[] { (byte)'H', (byte)'i' }; + Assert.Equal("System.ReadOnlySpan[2]", span.ToString()); + } + + [Fact] + public static void ReadOnlySpanOfChar8_ToString() + { + ReadOnlySpan span = stackalloc Char8[] { (Char8)'H', (Char8)'i' }; + Assert.Equal("Hi", span.ToString()); + } + + [Fact] + public static void SpanOfByte_ToString() + { + Span span = stackalloc byte[] { (byte)'H', (byte)'i' }; + Assert.Equal("System.Span[2]", span.ToString()); + } + + [Fact] + public static void SpanOfChar8_ToString() + { + Span span = stackalloc Char8[] { (Char8)'H', (Char8)'i' }; + Assert.Equal("Hi", span.ToString()); + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/System/Net/Http/Utf8StringContentTests.cs b/src/System.Utf8String.Experimental/tests/System/Net/Http/Utf8StringContentTests.cs new file mode 100644 index 000000000000..87c31c6dce39 --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System/Net/Http/Utf8StringContentTests.cs @@ -0,0 +1,44 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +using static System.Tests.Utf8TestUtilities; + +namespace System.Net.Http.Tests +{ + public partial class Utf8StringContentTests + { + [Fact] + public static void Ctor_NullContent_Throws() + { + Assert.Throws("content", () => new Utf8StringContent(null)); + Assert.Throws("content", () => new Utf8StringContent(null, "application/json")); + } + + [Theory] + [InlineData(null, "text/plain")] + [InlineData("application/json", "application/json")] + public static void Ctor_SetsContentTypeHeader(string mediaTypeForCtor, string expectedMediaType) + { + HttpContent httpContent = new Utf8StringContent(u8("Hello"), mediaTypeForCtor); + + Assert.Equal(expectedMediaType, httpContent.Headers.ContentType.MediaType); + Assert.Equal(Encoding.UTF8.WebName, httpContent.Headers.ContentType.CharSet); + } + + [Fact] + public static async Task Ctor_GetStream() + { + MemoryStream memoryStream = new MemoryStream(); + + await new Utf8StringContent(u8("Hello")).CopyToAsync(memoryStream); + + Assert.Equal(u8("Hello").ToByteArray(), memoryStream.ToArray()); + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/System/ReflectionTests.cs b/src/System.Utf8String.Experimental/tests/System/ReflectionTests.cs new file mode 100644 index 000000000000..4cbd4cf192d2 --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System/ReflectionTests.cs @@ -0,0 +1,36 @@ +// 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.Runtime.Serialization; +using Xunit; + +using static System.Tests.Utf8TestUtilities; + +namespace System.Tests +{ + public partial class ReflectionTests + { + [Fact] + public static void ActivatorCreateInstance_CanCallParameterfulCtor() + { + Utf8String theString = (Utf8String)Activator.CreateInstance(typeof(Utf8String), "Hello"); + Assert.Equal(u8("Hello"), theString); + } + + [Fact] + public static void ActivatorCreateInstance_CannotCallParameterlessCtor() + { + Assert.Throws(() => Activator.CreateInstance(typeof(Utf8String))); + } + + [Fact] + public static void FormatterServices_GetUninitializedObject_Throws() + { + // Like String, shouldn't be able to create an uninitialized Utf8String. + + Assert.Throws(() => FormatterServices.GetSafeUninitializedObject(typeof(Utf8String))); + Assert.Throws(() => FormatterServices.GetUninitializedObject(typeof(Utf8String))); + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/System/Utf8ExtensionsTests.cs b/src/System.Utf8String.Experimental/tests/System/Utf8ExtensionsTests.cs new file mode 100644 index 000000000000..a3c218230a3c --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System/Utf8ExtensionsTests.cs @@ -0,0 +1,209 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Xunit; + +using static System.Tests.Utf8TestUtilities; + +namespace System.Tests +{ + public partial class Utf8ExtensionsTests + { + [Fact] + public unsafe void AsBytes_FromSpan_Default() + { + // First, a default span should become a default span. + + Assert.True(default(ReadOnlySpan) == new ReadOnlySpan().AsBytes()); + + // Next, an empty but non-default span should become an empty but non-default span. + + Assert.True(new ReadOnlySpan((void*)0x12345, 0) == new ReadOnlySpan((void*)0x12345, 0).AsBytes()); + + // 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)theString).AsBytes()); + } + + [Fact] + public void AsBytes_FromUtf8String() + { + Assert.True(default(ReadOnlySpan) == ((Utf8String)null).AsBytes()); + + Utf8String theString = u8("Hello"); + Assert.True(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in theString.GetPinnableReference()), 5) == theString.AsBytes()); + } + + [Fact] + public void AsBytes_FromUtf8String_WithStart() + { + Assert.True(default(ReadOnlySpan) == ((Utf8String)null).AsBytes(0)); + Assert.True(u8("Hello").AsBytes(5).IsEmpty); + + SpanAssert.Equal(new byte[] { (byte)'e', (byte)'l', (byte)'l', (byte)'o' }, u8("Hello").AsBytes(1)); + } + + [Fact] + public void AsBytes_FromUtf8String_WithStart_ArgOutOfRange() + { + Assert.Throws("start", () => ((Utf8String)null).AsBytes(1)); + Assert.Throws("start", () => u8("Hello").AsBytes(-1)); + Assert.Throws("start", () => u8("Hello").AsBytes(6)); + } + + [Fact] + public void AsBytes_FromUtf8String_WithStartAndLength() + { + Assert.True(default(ReadOnlySpan) == ((Utf8String)null).AsBytes(0, 0)); + Assert.True(u8("Hello").AsBytes(5, 0).IsEmpty); + + SpanAssert.Equal(new byte[] { (byte)'e', (byte)'l', (byte)'l' }, u8("Hello").AsBytes(1, 3)); + } + + [Fact] + public void AsBytes_FromUtf8String_WithStartAndLength_ArgOutOfRange() + { + Assert.Throws("start", () => ((Utf8String)null).AsBytes(0, 1)); + Assert.Throws("start", () => ((Utf8String)null).AsBytes(1, 0)); + Assert.Throws("start", () => u8("Hello").AsBytes(5, 1)); + Assert.Throws("start", () => u8("Hello").AsBytes(4, -2)); + } + + [Fact] + public void AsMemory_FromUtf8String() + { + Assert.True(default(ReadOnlyMemory).Equals(((Utf8String)null).AsMemory())); + + Utf8String theString = u8("Hello"); + Assert.True(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in theString.GetPinnableReference())), 5) == theString.AsMemory().Span); + } + + [Fact] + public void AsMemory_FromUtf8String_WithStart() + { + Assert.True(default(ReadOnlyMemory).Equals(((Utf8String)null).AsMemory(0))); + Assert.True(u8("Hello").AsMemory(5).IsEmpty); + + SpanAssert.Equal(new Char8[] { (Char8)'e', (Char8)'l', (Char8)'l', (Char8)'o' }, u8("Hello").AsMemory(1).Span); + } + + [Fact] + public void AsMemory_FromUtf8String_WithStart_ArgOutOfRange() + { + Assert.Throws("start", () => ((Utf8String)null).AsMemory(1)); + Assert.Throws("start", () => u8("Hello").AsMemory(-1)); + Assert.Throws("start", () => u8("Hello").AsMemory(6)); + } + + [Fact] + public void AsMemory_FromUtf8String_WithStartAndLength() + { + Assert.True(default(ReadOnlyMemory).Equals(((Utf8String)null).AsMemory(0, 0))); + Assert.True(u8("Hello").AsMemory(5, 0).IsEmpty); + + SpanAssert.Equal(new Char8[] { (Char8)'e', (Char8)'l', (Char8)'l' }, u8("Hello").AsMemory(1, 3).Span); + } + + [Fact] + public void AsMemory_FromUtf8String_WithStartAndLength_ArgOutOfRange() + { + Assert.Throws("start", () => ((Utf8String)null).AsMemory(0, 1)); + Assert.Throws("start", () => ((Utf8String)null).AsMemory(1, 0)); + Assert.Throws("start", () => u8("Hello").AsMemory(5, 1)); + Assert.Throws("start", () => u8("Hello").AsMemory(4, -2)); + } + + [Fact] + public void AsMemoryBytes_FromUtf8String() + { + Assert.True(default(ReadOnlyMemory).Equals(((Utf8String)null).AsMemoryBytes())); + + Utf8String theString = u8("Hello"); + Assert.True(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in theString.GetPinnableReference()), 5) == theString.AsMemoryBytes().Span); + } + + [Fact] + public void AsMemoryBytes_FromUtf8String_WithStart() + { + Assert.True(default(ReadOnlyMemory).Equals(((Utf8String)null).AsMemoryBytes(0))); + Assert.True(u8("Hello").AsMemoryBytes(5).IsEmpty); + + SpanAssert.Equal(new byte[] { (byte)'e', (byte)'l', (byte)'l', (byte)'o' }, u8("Hello").AsMemoryBytes(1).Span); + } + + [Fact] + public void AsMemoryBytes_FromUtf8String_WithStart_ArgOutOfRange() + { + Assert.Throws("start", () => ((Utf8String)null).AsMemoryBytes(1)); + Assert.Throws("start", () => u8("Hello").AsMemoryBytes(-1)); + Assert.Throws("start", () => u8("Hello").AsMemoryBytes(6)); + } + + [Fact] + public void AsMemoryBytes_FromUtf8String_WithStartAndLength() + { + Assert.True(default(ReadOnlyMemory).Equals(((Utf8String)null).AsMemoryBytes(0, 0))); + Assert.True(u8("Hello").AsMemoryBytes(5, 0).IsEmpty); + + SpanAssert.Equal(new byte[] { (byte)'e', (byte)'l', (byte)'l' }, u8("Hello").AsMemoryBytes(1, 3).Span); + } + + [Fact] + public void AsMemoryBytes_FromUtf8String_WithStartAndLength_ArgOutOfRange() + { + Assert.Throws("start", () => ((Utf8String)null).AsMemoryBytes(0, 1)); + Assert.Throws("start", () => ((Utf8String)null).AsMemoryBytes(1, 0)); + Assert.Throws("start", () => u8("Hello").AsMemoryBytes(5, 1)); + Assert.Throws("start", () => u8("Hello").AsMemoryBytes(4, -2)); + } + + [Fact] + public void AsSpan_FromUtf8String() + { + Assert.True(default(ReadOnlySpan) == ((Utf8String)null).AsSpan()); + + Utf8String theString = u8("Hello"); + Assert.True(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in theString.GetPinnableReference())), 5) == theString.AsSpan()); + } + + [Fact] + public void AsSpan_FromUtf8String_WithStart() + { + Assert.True(default(ReadOnlySpan) == ((Utf8String)null).AsSpan(0)); + Assert.True(u8("Hello").AsSpan(5).IsEmpty); + + SpanAssert.Equal(new Char8[] { (Char8)'e', (Char8)'l', (Char8)'l', (Char8)'o' }, u8("Hello").AsSpan(1)); + } + + [Fact] + public void AsSpan_FromUtf8String_WithStart_ArgOutOfRange() + { + Assert.Throws("start", () => ((Utf8String)null).AsSpan(1)); + Assert.Throws("start", () => u8("Hello").AsSpan(-1)); + Assert.Throws("start", () => u8("Hello").AsSpan(6)); + } + + [Fact] + public void AsSpan_FromUtf8String_WithStartAndLength() + { + Assert.True(default(ReadOnlySpan) == ((Utf8String)null).AsSpan(0, 0)); + Assert.True(u8("Hello").AsSpan(5, 0).IsEmpty); + + SpanAssert.Equal(new Char8[] { (Char8)'e', (Char8)'l', (Char8)'l' }, u8("Hello").AsSpan(1, 3)); + } + + [Fact] + public void AsSpan_FromUtf8String_WithStartAndLength_ArgOutOfRange() + { + Assert.Throws("start", () => ((Utf8String)null).AsSpan(0, 1)); + Assert.Throws("start", () => ((Utf8String)null).AsSpan(1, 0)); + Assert.Throws("start", () => u8("Hello").AsSpan(5, 1)); + Assert.Throws("start", () => u8("Hello").AsSpan(4, -2)); + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.Ctor.cs b/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.Ctor.cs new file mode 100644 index 000000000000..48628033c2c0 --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.Ctor.cs @@ -0,0 +1,209 @@ +// 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.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Xunit; + +using static System.Tests.Utf8TestUtilities; + +namespace System.Tests +{ + public unsafe partial class Utf8StringTests + { + [Fact] + public static void Ctor_ByteArrayOffset_Empty_ReturnsEmpty() + { + byte[] inputData = new byte[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o' }; + Assert.Same(Utf8String.Empty, new Utf8String(inputData, 3, 0)); + } + + [Fact] + public static void Ctor_ByteArrayOffset_ValidData_ReturnsOriginalContents() + { + byte[] inputData = new byte[] { (byte)'x', (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o', (byte)'x' }; + Utf8String expected = u8("Hello"); + + var actual = new Utf8String(inputData, 1, 5); + Assert.Equal(expected, actual); + } + + [Fact] + public static void Ctor_ByteArrayOffset_InvalidData_FixesUpData() + { + 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); + } + + [Fact] + public static void Ctor_BytePointer_NullOrEmpty_ReturnsEmpty() + { + byte[] inputData = new byte[] { 0 }; // standalone null byte + + using (BoundedMemory 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)))); + } + } + + [Fact] + public static void Ctor_BytePointer_ValidData_ReturnsOriginalContents() + { + byte[] inputData = new byte[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o', (byte)'\0' }; + + using (BoundedMemory boundedMemory = BoundedMemory.AllocateFromExistingData(inputData)) + { + Assert.Equal(u8("Hello"), new Utf8String((byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(boundedMemory.Span)))); + } + } + + [Fact] + public static void Ctor_BytePointer_InvalidData_FixesUpData() + { + byte[] inputData = new byte[] { (byte)'H', (byte)'e', (byte)0xFF, (byte)'l', (byte)'o', (byte)'\0' }; + + using (BoundedMemory boundedMemory = BoundedMemory.AllocateFromExistingData(inputData)) + { + Assert.Equal(u8("He\uFFFDlo"), new Utf8String((byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(boundedMemory.Span)))); + } + } + + [Fact] + public static void Ctor_ByteSpan_Empty_ReturnsEmpty() + { + Assert.Same(Utf8String.Empty, new Utf8String(ReadOnlySpan.Empty)); + } + + [Fact] + public static void Ctor_ByteSpan_ValidData_ReturnsOriginalContents() + { + byte[] inputData = new byte[] { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o' }; + Utf8String expected = u8("Hello"); + + var actual = new Utf8String(inputData.AsSpan()); + Assert.Equal(expected, actual); + } + + [Fact] + public static void Ctor_ByteSpan_InvalidData_FixesUpData() + { + 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); + } + + [Fact] + public static void Ctor_CharArrayOffset_Empty_ReturnsEmpty() + { + char[] inputData = "Hello".ToCharArray(); + Assert.Same(Utf8String.Empty, new Utf8String(inputData, 3, 0)); + } + + [Fact] + public static void Ctor_CharArrayOffset_ValidData_ReturnsOriginalContents() + { + char[] inputData = "xHellox".ToCharArray(); + Utf8String expected = u8("Hello"); + + var actual = new Utf8String(inputData, 1, 5); + Assert.Equal(expected, actual); + } + + [Fact] + public static void Ctor_CharArrayOffset_InvalidData_FixesUpData() + { + char[] inputData = new char[] { 'x', 'H', 'e', '\uD800', 'l', 'o', 'x' }; + Utf8String expected = u8("He\uFFFDlo"); + + var actual = new Utf8String(inputData, 1, 5); + Assert.Equal(expected, actual); + } + + [Fact] + public static void Ctor_CharPointer_NullOrEmpty_ReturnsEmpty() + { + char[] inputData = new char[] { '\0' }; // standalone null char + + using (BoundedMemory 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)))); + } + } + + [Fact] + public static void Ctor_CharPointer_ValidData_ReturnsOriginalContents() + { + char[] inputData = new char[] { 'H', 'e', 'l', 'l', 'o', '\0' }; + + using (BoundedMemory boundedMemory = BoundedMemory.AllocateFromExistingData(inputData)) + { + Assert.Equal(u8("Hello"), new Utf8String((char*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(boundedMemory.Span)))); + } + } + + [Fact] + public static void Ctor_CharPointer_InvalidData_FixesUpData() + { + char[] inputData = new char[] { 'H', 'e', '\uD800', 'l', 'o', '\0' }; // standalone surrogate + + using (BoundedMemory boundedMemory = BoundedMemory.AllocateFromExistingData(inputData)) + { + Assert.Equal(u8("He\uFFFDlo"), new Utf8String((char*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(boundedMemory.Span)))); + } + } + + [Fact] + public static void Ctor_CharSpan_Empty_ReturnsEmpty() + { + Assert.Same(Utf8String.Empty, new Utf8String(ReadOnlySpan.Empty)); + } + + [Fact] + public static void Ctor_CharSpan_ValidData_ReturnsOriginalContents() + { + char[] inputData = "Hello".ToCharArray(); + Utf8String expected = u8("Hello"); + + var actual = new Utf8String(inputData.AsSpan()); + Assert.Equal(expected, actual); + } + + [Fact] + public static void Ctor_CharSpan_InvalidData_FixesUpData() + { + char[] inputData = new char[] { 'H', 'e', '\uD800', 'l', 'o' }; + Utf8String expected = u8("He\uFFFDlo"); + + var actual = new Utf8String(inputData.AsSpan()); + Assert.Equal(expected, actual); + } + + [Fact] + public static void Ctor_String_NullOrEmpty_ReturnsEmpty() + { + Assert.Same(Utf8String.Empty, new Utf8String((string)null)); + Assert.Same(Utf8String.Empty, new Utf8String(string.Empty)); + } + + [Fact] + public static void Ctor_String_ValidData_ReturnsOriginalContents() + { + Assert.Equal(u8("Hello"), new Utf8String("Hello")); + } + + [Fact] + public static void Ctor_String_InvalidData_FixesUpData() + { + Assert.Equal(u8("He\uFFFDlo"), new Utf8String("He\uD800lo")); + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.Searching.cs b/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.Searching.cs new file mode 100644 index 000000000000..3359cef7fa1d --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.Searching.cs @@ -0,0 +1,97 @@ +// 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.Text; +using Xunit; + +using static System.Tests.Utf8TestUtilities; + +namespace System.Tests +{ + public unsafe partial class Utf8StringTests + { + [Theory] + [MemberData(nameof(IndexOfTestData))] + public static void Contains_And_IndexOf_CharRune_Ordinal(Utf8String utf8String, Rune searchValue, int expectedIndex) + { + // Contains + + if (searchValue.IsBmp) + { + Assert.Equal(expectedIndex >= 0, utf8String.Contains((char)searchValue.Value)); + } + Assert.Equal(expectedIndex >= 0, utf8String.Contains(searchValue)); + + // IndexOf + + if (searchValue.IsBmp) + { + Assert.Equal(expectedIndex, utf8String.IndexOf((char)searchValue.Value)); + } + Assert.Equal(expectedIndex, utf8String.IndexOf(searchValue)); + } + + [Theory] + [MemberData(nameof(IndexOfTestData))] + public static void StartsWith_And_EndsWith_CharRune_Ordinal(Utf8String utf8String, Rune searchValue, int expectedIndex) + { + // StartsWith + + if (searchValue.IsBmp) + { + Assert.Equal(expectedIndex == 0, utf8String.StartsWith((char)searchValue.Value)); + } + Assert.Equal(expectedIndex == 0, utf8String.StartsWith(searchValue)); + + // EndsWith + + bool endsWithExpectedValue = (expectedIndex >= 0) && (expectedIndex + searchValue.Utf8SequenceLength) == utf8String.Length; + + if (searchValue.IsBmp) + { + Assert.Equal(endsWithExpectedValue, utf8String.EndsWith((char)searchValue.Value)); + } + Assert.Equal(endsWithExpectedValue, utf8String.EndsWith(searchValue)); + } + + [Fact] + public static void Searching_StandaloneSurrogate_Fails() + { + Utf8String utf8String = u8("\ud800\udfff"); + + Assert.False(utf8String.Contains('\ud800')); + Assert.False(utf8String.Contains('\udfff')); + + Assert.Equal(-1, utf8String.IndexOf('\ud800')); + Assert.Equal(-1, utf8String.IndexOf('\udfff')); + + Assert.False(utf8String.StartsWith('\ud800')); + Assert.False(utf8String.StartsWith('\udfff')); + + Assert.False(utf8String.EndsWith('\ud800')); + Assert.False(utf8String.EndsWith('\udfff')); + } + + public static IEnumerable IndexOfTestData + { + 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 }; + } + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.Substring.cs b/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.Substring.cs new file mode 100644 index 000000000000..b024dac6520e --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.Substring.cs @@ -0,0 +1,134 @@ +// 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) + { + Assert.Equal(expected, s.Substring(index)); + + if (index.Value == 0) + { + Assert.Same(index.IsFromEnd ? Utf8String.Empty : s, s.Substring(index)); + } + + if (index.Value == s.Length) + { + Assert.Same(index.IsFromEnd ? s : Utf8String.Empty, s.Substring(index)); + } + }; + + 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.Substring(range)); + Assert.Equal(expected, s[range]); + + if (expected.Length == s.Length) + { + Assert.Same(s, s.Substring(range)); + Assert.Same(s, s[range]); + } + + if (expected.Length == 0) + { + Assert.Same(Utf8String.Empty, s.Substring(range)); + 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("startIndex", () => u8("foo").Substring(-1)); + AssertExtensions.Throws("startIndex", () => u8("foo").Substring(-1, 0)); + + // Start index > string.Length + AssertExtensions.Throws("startIndex", () => u8("foo").Substring(4)); + AssertExtensions.Throws("startIndex", () => u8("foo").Substring(4, 0)); + + // Length < 0 or length > string.Length + AssertExtensions.Throws("length", () => u8("foo").Substring(0, -1)); + AssertExtensions.Throws("length", () => u8("foo").Substring(0, 4)); + + // Start index + length > string.Length + AssertExtensions.Throws("length", () => u8("foo").Substring(3, 2)); + AssertExtensions.Throws("length", () => u8("foo").Substring(2, 2)); + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.cs b/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.cs new file mode 100644 index 000000000000..fb62b311111a --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System/Utf8StringTests.cs @@ -0,0 +1,178 @@ +// 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 Empty_HasLengthZero() + { + Assert.Equal(0, Utf8String.Empty.Length); + SpanAssert.Equal(ReadOnlySpan.Empty, Utf8String.Empty.AsBytes()); + } + + [Fact] + public static void Empty_ReturnsSingleton() + { + Assert.Same(Utf8String.Empty, Utf8String.Empty); + } + + [Theory] + [InlineData(null, null, true)] + [InlineData("", null, false)] + [InlineData(null, "", false)] + [InlineData("hello", null, false)] + [InlineData(null, "hello", false)] + [InlineData("hello", "hello", true)] + [InlineData("hello", "Hello", false)] + [InlineData("hello there", "hello", false)] + public static void Equality_Ordinal(string aString, string bString, bool expected) + { + Utf8String a = u8(aString); + Utf8String b = u8(bString); + + // Operators + + Assert.Equal(expected, a == b); + Assert.NotEqual(expected, a != b); + + // Static methods + + Assert.Equal(expected, Utf8String.Equals(a, b)); + + // Instance methods + + if (a != null) + { + Assert.Equal(expected, a.Equals(b)); + Assert.Equal(expected, a.Equals((object)b)); + } + } + + [Fact] + public static void GetHashCode_ReturnsRandomized() + { + Utf8String a = u8("Hello"); + Utf8String b = new Utf8String(a.AsBytes()); + + Assert.NotSame(a, b); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + + Utf8String c = u8("Goodbye"); + Utf8String d = new Utf8String(c.AsBytes()); + + Assert.NotSame(c, d); + Assert.Equal(c.GetHashCode(), d.GetHashCode()); + + Assert.NotEqual(a.GetHashCode(), c.GetHashCode()); + } + + [Fact] + public static void GetPinnableReference_CalledMultipleTimes_ReturnsSameValue() + { + var utf8 = u8("Hello!"); + + fixed (byte* pA = utf8) + fixed (byte* pB = utf8) + { + Assert.True(pA == pB); + } + } + + [Fact] + public static void GetPinnableReference_Empty() + { + fixed (byte* pStr = Utf8String.Empty) + { + Assert.True(pStr != null); + Assert.Equal((byte)0, *pStr); // should point to null terminator + } + } + + [Fact] + public static void GetPinnableReference_NotEmpty() + { + fixed (byte* pStr = u8("Hello!")) + { + Assert.True(pStr != null); + + Assert.Equal((byte)'H', pStr[0]); + Assert.Equal((byte)'e', pStr[1]); + Assert.Equal((byte)'l', pStr[2]); + Assert.Equal((byte)'l', pStr[3]); + Assert.Equal((byte)'o', pStr[4]); + Assert.Equal((byte)'!', pStr[5]); + Assert.Equal((byte)'\0', pStr[6]); + } + } + + [Theory] + [InlineData("", true)] + [InlineData("not empty", false)] + public static void IsNullOrEmpty(string value, bool expectedIsNullOrEmpty) + { + Assert.Equal(expectedIsNullOrEmpty, Utf8String.IsNullOrEmpty(new Utf8String(value))); + } + + [Fact] + public static void IsNullOrEmpty_Null_ReturnsTrue() + { + Assert.True(Utf8String.IsNullOrEmpty(null)); + } + + [Fact] + public static void ToByteArray_Empty() + { + Assert.Same(Array.Empty(), Utf8String.Empty.ToByteArray()); + Assert.Same(Array.Empty(), u8("Hello!").ToByteArray(0, 0)); + Assert.Same(Array.Empty(), u8("Hello!").ToByteArray(3, 0)); + Assert.Same(Array.Empty(), 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) + { + Utf8String utf8String = u8(value); + Assert.Throws(exceptionParamName, () => utf8String.ToByteArray(startIndex, length)); + } + + [Theory] + [InlineData("")] + [InlineData("Hello!")] + public static void ToString_ReturnsUtf16(string value) + { + Assert.Equal(value, u8(value).ToString()); + } + + [Fact] + public static void ToString_ReturnsUtf16_WithFixups() + { + Utf8String newString = new Utf8String("Hello"); + + fixed (byte* pNewString = newString) + { + pNewString[2] = 0xFF; // corrupt this data + } + + Assert.Equal("He\uFFFDlo", newString.ToString()); + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/System/Utf8TestUtilities.cs b/src/System.Utf8String.Experimental/tests/System/Utf8TestUtilities.cs new file mode 100644 index 000000000000..1df50cb02237 --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/System/Utf8TestUtilities.cs @@ -0,0 +1,77 @@ +// 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.IO; +using System.Reflection; +using System.Text; +using Xunit; + +namespace System.Tests +{ + public static class Utf8TestUtilities + { + private static readonly Lazy> _utf8StringFactory = CreateUtf8StringFactory(); + + private static Lazy> CreateUtf8StringFactory() + { + return new Lazy>(() => + { + MethodInfo fastAllocateMethod = typeof(Utf8String).GetMethod("FastAllocate", BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(int) }, null); + Assert.NotNull(fastAllocateMethod); + return (Func)fastAllocateMethod.CreateDelegate(typeof(Func)); + }); + } + + /// + /// Mimics returning a literal instance. + /// + public unsafe static Utf8String u8(string str) + { + if (str is null) + { + return null; + } + else if (str.Length == 0) + { + return Utf8String.Empty; + } + + // First, transcode UTF-16 to UTF-8. We use direct by-scalar transcoding here + // because we have good reference implementation tests for this and it'll help + // catch any errors we introduce to our bulk transcoding implementations. + + MemoryStream memStream = new MemoryStream(); + + Span utf8Bytes = stackalloc byte[4]; // 4 UTF-8 code units is the largest any scalar value can be encoded as + + int index = 0; + while (index < str.Length) + { + if (Rune.TryGetRuneAt(str, index, out Rune value) && value.TryEncodeToUtf8(utf8Bytes, out int bytesWritten)) + { + memStream.Write(utf8Bytes.Slice(0, bytesWritten)); + index += value.Utf16SequenceLength; + } + else + { + throw new ArgumentException($"String '{str}' is not a well-formed UTF-16 string."); + } + } + + Assert.True(memStream.TryGetBuffer(out ArraySegment 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(pNewUtf8String, newUtf8String.Length)); + } + + return newUtf8String; + } + } +} diff --git a/src/System.Utf8String.Experimental/tests/Xunit/SpanAssert.cs b/src/System.Utf8String.Experimental/tests/Xunit/SpanAssert.cs new file mode 100644 index 000000000000..59f21c0e6b45 --- /dev/null +++ b/src/System.Utf8String.Experimental/tests/Xunit/SpanAssert.cs @@ -0,0 +1,36 @@ +// 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; + +namespace Xunit +{ + public static class SpanAssert + { + public static void Equal(ReadOnlySpan a, ReadOnlySpan b, IEqualityComparer comparer = null) where T : IEquatable + { + if (comparer is null) + { + Assert.Equal(a.ToArray(), b.ToArray()); + } + else + { + Assert.Equal(a.ToArray(), b.ToArray(), comparer); + } + } + + public static void Equal(Span a, Span b, IEqualityComparer comparer = null) where T : IEquatable + { + if (comparer is null) + { + Assert.Equal(a.ToArray(), b.ToArray()); + } + else + { + Assert.Equal(a.ToArray(), b.ToArray(), comparer); + } + } + } +}