diff --git a/src/libraries/System.Private.CoreLib/src/System/Version.cs b/src/libraries/System.Private.CoreLib/src/System/Version.cs index 5b8f4b0c5fc248..c0af3c196944fc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Version.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Version.cs @@ -1,10 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers.Text; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace System { @@ -16,7 +19,7 @@ namespace System [Serializable] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public sealed class Version : ICloneable, IComparable, IComparable, IEquatable, ISpanFormattable + public sealed class Version : ICloneable, IComparable, IComparable, IEquatable, ISpanFormattable, IUtf8SpanFormattable { // AssemblyName depends on the order staying the same private readonly int _Major; // Do not rename (binary serialization) @@ -177,10 +180,15 @@ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); public bool TryFormat(Span destination, out int charsWritten) => - TryFormat(destination, DefaultFormatFieldCount, out charsWritten); + TryFormatCore(destination, DefaultFormatFieldCount, out charsWritten); - public bool TryFormat(Span destination, int fieldCount, out int charsWritten) + public bool TryFormat(Span destination, int fieldCount, out int charsWritten) => + TryFormatCore(destination, fieldCount, out charsWritten); + + private bool TryFormatCore(Span destination, int fieldCount, out int charsWritten) where TChar : unmanaged, IBinaryInteger { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + switch ((uint)fieldCount) { case > 4: @@ -211,7 +219,7 @@ static void ThrowArgumentException(string failureUpperBound) => return false; } - destination[0] = '.'; + destination[0] = TChar.CreateTruncating('.'); destination = destination.Slice(1); totalCharsWritten++; } @@ -224,7 +232,12 @@ static void ThrowArgumentException(string failureUpperBound) => _ => _Revision }; - if (!((uint)value).TryFormat(destination, out int valueCharsWritten)) + int valueCharsWritten; + bool formatted = typeof(TChar) == typeof(char) ? + ((uint)value).TryFormat(MemoryMarshal.Cast(destination), out valueCharsWritten) : + Utf8Formatter.TryFormat((uint)value, MemoryMarshal.Cast(destination), out valueCharsWritten); // TODO https://github.com/dotnet/runtime/issues/84527: Use UInt32's IUtf8SpanFormattable when available + + if (!formatted) { charsWritten = 0; return false; @@ -240,7 +253,11 @@ static void ThrowArgumentException(string failureUpperBound) => bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => // format and provider are ignored. - TryFormat(destination, DefaultFormatFieldCount, out charsWritten); + TryFormatCore(destination, DefaultFormatFieldCount, out charsWritten); + + bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider) => + // format and provider are ignored. + TryFormatCore(utf8Destination, DefaultFormatFieldCount, out bytesWritten); private int DefaultFormatFieldCount => _Build == -1 ? 2 : diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 6387a1dc769805..ff0925a5ab50fd 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7058,7 +7058,7 @@ protected ValueType() { } public override int GetHashCode() { throw null; } public override string? ToString() { throw null; } } - public sealed partial class Version : System.ICloneable, System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.ISpanFormattable + public sealed partial class Version : System.ICloneable, System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.ISpanFormattable, System.IUtf8SpanFormattable { public Version() { } public Version(int major, int minor) { } @@ -7087,6 +7087,7 @@ public Version(string version) { } public static System.Version Parse(string input) { throw null; } string System.IFormattable.ToString(string? format, System.IFormatProvider? formatProvider) { throw null; } bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } public override string ToString() { throw null; } public string ToString(int fieldCount) { throw null; } public bool TryFormat(System.Span destination, int fieldCount, out int charsWritten) { throw null; } diff --git a/src/libraries/System.Runtime/tests/System/VersionTests.cs b/src/libraries/System.Runtime/tests/System/VersionTests.cs index 29f3cd7a2e37c4..06ee0e0e13cbcc 100644 --- a/src/libraries/System.Runtime/tests/System/VersionTests.cs +++ b/src/libraries/System.Runtime/tests/System/VersionTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Text; using Xunit; namespace System.Tests @@ -405,5 +406,29 @@ public static void TryFormat_Invoke_WritesExpected(Version version, string[] exp AssertExtensions.Throws("fieldCount", () => version.TryFormat(dest, -1, out charsWritten)); // Index < 0 AssertExtensions.Throws("fieldCount", () => version.TryFormat(dest, maxFieldCount + 1, out charsWritten)); // Index > version.fieldCount } + + [Theory] + [MemberData(nameof(ToString_TestData))] + public static void IUtf8SpanFormattableTryFormat_Invoke_WritesExpected(Version version, string[] expectedFieldCounts) + { + string expected = expectedFieldCounts[^1]; + + // Too small + byte[] dest = new byte[expected.Length - 1]; + Assert.False(((IUtf8SpanFormattable)version).TryFormat(dest, out int charsWritten, default, null)); + Assert.Equal(0, charsWritten); + + // Just right + dest = new byte[expected.Length]; + Assert.True(((IUtf8SpanFormattable)version).TryFormat(dest, out charsWritten, default, null)); + Assert.Equal(expected.Length, charsWritten); + Assert.Equal(expected, Encoding.UTF8.GetString(dest.AsSpan(0, charsWritten))); + + // More than needed + dest = new byte[expected.Length + 10]; + Assert.True(((IUtf8SpanFormattable)version).TryFormat(dest, out charsWritten, default, null)); + Assert.Equal(expected.Length, charsWritten); + Assert.Equal(expected, Encoding.UTF8.GetString(dest.AsSpan(0, charsWritten))); + } } }