From f516d02550e5f47d12b7d551b8b3dfcf33d8743a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 10 Apr 2023 16:15:35 -0400 Subject: [PATCH] Implement IUtf8SpanFormattable on IPAddress and IPNetwork (#84487) * Implement IUtf8SpanFormattable on IPAddress and IPNetwork Implements IUtf8SpanFormattable explicitly on both IPAddress and IPNetwork. For IPNetwork, we just use Utf8.TryWrite just as the existing ISpanFormattable uses MemoryExtensions.TryWrite. For IPAddress, the existing formatting code is made to work generically for either byte or char. In the process, I removed the unsafe pointer-based code from the formatting logic while also making it faster. * Fix parameter names --- .../System/Net/IPv6AddressHelper.Common.cs | 2 +- .../ref/System.Net.Primitives.cs | 6 +- .../src/System/Net/IPAddress.cs | 108 ++++-- .../src/System/Net/IPAddressParser.cs | 330 ++++++++---------- .../src/System/Net/IPNetwork.cs | 7 +- .../tests/FunctionalTests/IPAddressParsing.cs | 80 ++++- .../tests/FunctionalTests/IPNetworkTest.cs | 66 +++- 7 files changed, 350 insertions(+), 249 deletions(-) diff --git a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs index 2753240ccc7b3..71c655a52efd6 100644 --- a/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs +++ b/src/libraries/Common/src/System/Net/IPv6AddressHelper.Common.cs @@ -35,7 +35,7 @@ internal static (int longestSequenceStart, int longestSequenceLength) FindCompre return longestSequenceLength > 1 ? (longestSequenceStart, longestSequenceStart + longestSequenceLength) : - (-1, -1); + (-1, 0); } // Returns true if the IPv6 address should be formatted with an embedded IPv4 address: diff --git a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs index 5e329e0b9aa5e..fcfbae6ee681f 100644 --- a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs +++ b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs @@ -218,7 +218,7 @@ public partial interface ICredentialsByHost { System.Net.NetworkCredential? GetCredential(string host, int port, string authenticationType); } - public partial class IPAddress : ISpanFormattable, ISpanParsable + public partial class IPAddress : ISpanFormattable, ISpanParsable, IUtf8SpanFormattable { public static readonly System.Net.IPAddress Any; public static readonly System.Net.IPAddress Broadcast; @@ -262,6 +262,7 @@ public IPAddress(System.ReadOnlySpan address, long scopeid) { } string IFormattable.ToString(string? format, IFormatProvider? formatProvider) { throw null; } public bool TryFormat(System.Span destination, out int charsWritten) { throw null; } bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) { throw null; } + bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } public static bool TryParse(System.ReadOnlySpan ipSpan, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPAddress? address) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? ipString, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPAddress? address) { throw null; } static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, out IPAddress result) { throw null; } @@ -287,7 +288,7 @@ public IPEndPoint(System.Net.IPAddress address, int port) { } public static bool TryParse(System.ReadOnlySpan s, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPEndPoint? result) { throw null; } public static bool TryParse(string s, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPEndPoint? result) { throw null; } } - public readonly partial struct IPNetwork : System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable + public readonly partial struct IPNetwork : System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.IUtf8SpanFormattable { private readonly object _dummy; private readonly int _dummyPrimitive; @@ -306,6 +307,7 @@ public IPEndPoint(System.Net.IPAddress address, int port) { } static System.Net.IPNetwork System.IParsable.Parse([System.Diagnostics.CodeAnalysis.NotNullAttribute] string s, System.IFormatProvider? provider) { throw null; } static bool System.IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, out System.Net.IPNetwork result) { 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; } static System.Net.IPNetwork System.ISpanParsable.Parse(System.ReadOnlySpan s, System.IFormatProvider? provider) { throw null; } static bool System.ISpanParsable.TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, out System.Net.IPNetwork result) { throw null; } public override string ToString() { throw null; } diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs index 81b0851f45a96..cf2a603a24b38 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.Sockets; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; @@ -18,7 +19,7 @@ namespace System.Net /// Provides an Internet Protocol (IP) address. /// /// - public class IPAddress : ISpanFormattable, ISpanParsable + public class IPAddress : ISpanFormattable, ISpanParsable, IUtf8SpanFormattable { public static readonly IPAddress Any = new ReadOnlyIPAddress(new byte[] { 0, 0, 0, 0 }); public static readonly IPAddress Loopback = new ReadOnlyIPAddress(new byte[] { 127, 0, 0, 1 }); @@ -375,7 +376,7 @@ public long ScopeId // Not valid for IPv4 addresses if (IsIPv4) { - throw new SocketException(SocketError.OperationNotSupported); + ThrowSocketOperationNotSupported(); } return PrivateScopeId; @@ -385,7 +386,7 @@ public long ScopeId // Not valid for IPv4 addresses if (IsIPv4) { - throw new SocketException(SocketError.OperationNotSupported); + ThrowSocketOperationNotSupported(); } // Consider: Since scope is only valid for link-local and site-local @@ -403,27 +404,74 @@ public long ScopeId /// or standard IPv6 representation. /// /// - public override string ToString() => - _toString ??= IsIPv4 ? - IPAddressParser.IPv4AddressToString(PrivateAddress) : - IPAddressParser.IPv6AddressToString(_numbers, PrivateScopeId); + public override string ToString() + { + string? toString = _toString; + if (toString is null) + { + Span span = stackalloc char[IPAddressParser.MaxIPv6StringLength]; + int length = IsIPv4 ? + IPAddressParser.FormatIPv4Address(_addressOrScopeId, span) : + IPAddressParser.FormatIPv6Address(_numbers, _addressOrScopeId, span); + _toString = toString = new string(span.Slice(0, length)); + } + + return toString; + } /// string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => // format and provider are explicitly ignored ToString(); - public bool TryFormat(Span destination, out int charsWritten) - { - return IsIPv4 ? - IPAddressParser.IPv4AddressToString(PrivateAddress, destination, out charsWritten) : - IPAddressParser.IPv6AddressToString(_numbers, PrivateScopeId, destination, out charsWritten); - } + public bool TryFormat(Span destination, out int charsWritten) => + TryFormatCore(destination, out charsWritten); /// bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => // format and provider are explicitly ignored - TryFormat(destination, out charsWritten); + TryFormatCore(destination, out charsWritten); + + /// + bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider) => + // format and provider are explicitly ignored + TryFormatCore(utf8Destination, out bytesWritten); + + private bool TryFormatCore(Span destination, out int charsWritten) where TChar : unmanaged, IBinaryInteger + { + if (IsIPv4) + { + if (destination.Length >= IPAddressParser.MaxIPv4StringLength) + { + charsWritten = IPAddressParser.FormatIPv4Address(_addressOrScopeId, destination); + return true; + } + } + else + { + if (destination.Length >= IPAddressParser.MaxIPv6StringLength) + { + charsWritten = IPAddressParser.FormatIPv6Address(_numbers, _addressOrScopeId, destination); + return true; + } + } + + Span tmpDestination = stackalloc TChar[IPAddressParser.MaxIPv6StringLength]; + Debug.Assert(tmpDestination.Length >= IPAddressParser.MaxIPv4StringLength); + + int written = IsIPv4 ? + IPAddressParser.FormatIPv4Address(PrivateAddress, tmpDestination) : + IPAddressParser.FormatIPv6Address(_numbers, PrivateScopeId, tmpDestination); + + if (tmpDestination.Slice(0, written).TryCopyTo(destination)) + { + charsWritten = written; + return true; + } + + charsWritten = 0; + return false; + } public static long HostToNetworkOrder(long host) { @@ -551,37 +599,28 @@ public long Address { get { - // - // IPv6 Changes: Can't do this for IPv6, so throw an exception. - // - // if (AddressFamily == AddressFamily.InterNetworkV6) { - throw new SocketException(SocketError.OperationNotSupported); - } - else - { - return PrivateAddress; + ThrowSocketOperationNotSupported(); } + + return PrivateAddress; } set { - // - // IPv6 Changes: Can't do this for IPv6 addresses if (AddressFamily == AddressFamily.InterNetworkV6) { - throw new SocketException(SocketError.OperationNotSupported); + ThrowSocketOperationNotSupported(); } - else + + if (PrivateAddress != value) { - if (PrivateAddress != value) + if (this is ReadOnlyIPAddress) { - if (this is ReadOnlyIPAddress) - { - throw new SocketException(SocketError.OperationNotSupported); - } - PrivateAddress = unchecked((uint)value); + ThrowSocketOperationNotSupported(); } + + PrivateAddress = unchecked((uint)value); } } } @@ -677,6 +716,9 @@ public IPAddress MapToIPv4() [DoesNotReturn] private static byte[] ThrowAddressNullException() => throw new ArgumentNullException("address"); + [DoesNotReturn] + private static void ThrowSocketOperationNotSupported() => throw new SocketException(SocketError.OperationNotSupported); + private sealed class ReadOnlyIPAddress : IPAddress { public ReadOnlyIPAddress(ReadOnlySpan newAddress) : base(newAddress) diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs index 408e17e9716f8..964cd4308366a 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddressParser.cs @@ -2,20 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Net.Sockets; -using System.Runtime.InteropServices; -using System.Text; using System.Globalization; using System.Net.NetworkInformation; -using System.Buffers.Binary; +using System.Net.Sockets; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace System.Net { internal static class IPAddressParser { - private const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number + internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number + internal const int MaxIPv6StringLength = 65; internal static IPAddress? Parse(ReadOnlySpan ipSpan, bool tryParse) { @@ -25,12 +24,12 @@ internal static class IPAddressParser // we don't support/parse a port specification at the end of an IPv4 address. Span numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts]; numbers.Clear(); - if (Ipv6StringToAddress(ipSpan, numbers, IPAddressParserStatics.IPv6AddressShorts, out uint scope)) + if (TryParseIPv6(ipSpan, numbers, IPAddressParserStatics.IPv6AddressShorts, out uint scope)) { return new IPAddress(numbers, scope); } } - else if (Ipv4StringToAddress(ipSpan, out long address)) + else if (TryParseIpv4(ipSpan, out long address)) { return new IPAddress(address); } @@ -43,132 +42,7 @@ internal static class IPAddressParser throw new FormatException(SR.dns_bad_ip_address, new SocketException(SocketError.InvalidArgument)); } - internal static unsafe string IPv4AddressToString(uint address) - { - char* addressString = stackalloc char[MaxIPv4StringLength]; - int charsWritten = IPv4AddressToStringHelper(address, addressString); - return new string(addressString, 0, charsWritten); - } - - internal static unsafe void IPv4AddressToString(uint address, StringBuilder destination) - { - char* addressString = stackalloc char[MaxIPv4StringLength]; - int charsWritten = IPv4AddressToStringHelper(address, addressString); - destination.Append(addressString, charsWritten); - } - - internal static unsafe bool IPv4AddressToString(uint address, Span formatted, out int charsWritten) - { - if (formatted.Length < MaxIPv4StringLength) - { - charsWritten = 0; - return false; - } - - fixed (char* formattedPtr = &MemoryMarshal.GetReference(formatted)) - { - charsWritten = IPv4AddressToStringHelper(address, formattedPtr); - } - - return true; - } - - private static unsafe int IPv4AddressToStringHelper(uint address, char* addressString) - { - int offset = 0; - address = (uint)IPAddress.NetworkToHostOrder(unchecked((int)address)); - - FormatIPv4AddressNumber((int)((address >> 24) & 0xFF), addressString, ref offset); - addressString[offset++] = '.'; - FormatIPv4AddressNumber((int)((address >> 16) & 0xFF), addressString, ref offset); - addressString[offset++] = '.'; - FormatIPv4AddressNumber((int)((address >> 8) & 0xFF), addressString, ref offset); - addressString[offset++] = '.'; - FormatIPv4AddressNumber((int)(address & 0xFF), addressString, ref offset); - - return offset; - } - - internal static string IPv6AddressToString(ushort[] address, uint scopeId) - { - Debug.Assert(address != null); - Debug.Assert(address.Length == IPAddressParserStatics.IPv6AddressShorts); - - StringBuilder buffer = IPv6AddressToStringHelper(address, scopeId); - - return StringBuilderCache.GetStringAndRelease(buffer); - } - - internal static bool IPv6AddressToString(ushort[] address, uint scopeId, Span destination, out int charsWritten) - { - Debug.Assert(address != null); - Debug.Assert(address.Length == IPAddressParserStatics.IPv6AddressShorts); - - StringBuilder buffer = IPv6AddressToStringHelper(address, scopeId); - - if (destination.Length < buffer.Length) - { - StringBuilderCache.Release(buffer); - charsWritten = 0; - return false; - } - - buffer.CopyTo(0, destination, buffer.Length); - charsWritten = buffer.Length; - - StringBuilderCache.Release(buffer); - - return true; - } - - internal static StringBuilder IPv6AddressToStringHelper(ushort[] address, uint scopeId) - { - const int INET6_ADDRSTRLEN = 65; - StringBuilder buffer = StringBuilderCache.Acquire(INET6_ADDRSTRLEN); - - if (IPv6AddressHelper.ShouldHaveIpv4Embedded(address)) - { - // We need to treat the last 2 ushorts as a 4-byte IPv4 address, - // so output the first 6 ushorts normally, followed by the IPv4 address. - AppendSections(address, 0, 6, buffer); - if (buffer[buffer.Length - 1] != ':') - { - buffer.Append(':'); - } - IPv4AddressToString(ExtractIPv4Address(address), buffer); - } - else - { - // No IPv4 address. Output all 8 sections as part of the IPv6 address - // with normal formatting rules. - AppendSections(address, 0, 8, buffer); - } - - // If there's a scope ID, append it. - if (scopeId != 0) - { - buffer.Append('%').Append(scopeId); - } - - return buffer; - } - - private static unsafe void FormatIPv4AddressNumber(int number, char* addressString, ref int offset) - { - // Math.DivRem has no overload for byte, assert here for safety - Debug.Assert(number < 256); - - offset += number > 99 ? 3 : number > 9 ? 2 : 1; - - int i = offset; - do - { - number = Math.DivRem(number, 10, out int rem); - addressString[--i] = (char)('0' + rem); - } while (number != 0); - } - - public static unsafe bool Ipv4StringToAddress(ReadOnlySpan ipSpan, out long address) + private static unsafe bool TryParseIpv4(ReadOnlySpan ipSpan, out long address) { int end = ipSpan.Length; long tmpAddr; @@ -185,15 +59,13 @@ public static unsafe bool Ipv4StringToAddress(ReadOnlySpan ipSpan, out lon address = (uint)IPAddress.HostToNetworkOrder(unchecked((int)tmpAddr)); return true; } - else - { - // Failed to parse the address. - address = 0; - return false; - } + + // Failed to parse the address. + address = 0; + return false; } - public static unsafe bool Ipv6StringToAddress(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) + private static unsafe bool TryParseIPv6(ReadOnlySpan ipSpan, Span numbers, int numbersLength, out uint scope) { Debug.Assert(numbers != null); Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts); @@ -216,14 +88,17 @@ public static unsafe bool Ipv6StringToAddress(ReadOnlySpan ipSpan, Span 0) { scope = interfaceIndex; return true; // scopeId is a known interface name } + // scopeId is an unknown interface name } + // scopeId is not presented scope = 0; return true; @@ -233,68 +108,161 @@ public static unsafe bool Ipv6StringToAddress(ReadOnlySpan ipSpan, Span - /// Appends each of the numbers in address in indexed range [fromInclusive, toExclusive), - /// while also replacing the longest sequence of 0s found in that range with "::", as long - /// as the sequence is more than one 0. - /// - private static void AppendSections(ushort[] address, int fromInclusive, int toExclusive, StringBuilder buffer) + internal static int FormatIPv4Address(uint address, Span addressString) where TChar : unmanaged, IBinaryInteger { - // Find the longest sequence of zeros to be combined into a "::" - ReadOnlySpan addressSpan = new ReadOnlySpan(address, fromInclusive, toExclusive - fromInclusive); - (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(addressSpan); - bool needsColon = false; + address = (uint)IPAddress.NetworkToHostOrder(unchecked((int)address)); + + int pos = FormatByte(address >> 24, addressString); + addressString[pos++] = TChar.CreateTruncating('.'); + pos += FormatByte(address >> 16, addressString.Slice(pos)); + addressString[pos++] = TChar.CreateTruncating('.'); + pos += FormatByte(address >> 8, addressString.Slice(pos)); + addressString[pos++] = TChar.CreateTruncating('.'); + pos += FormatByte(address, addressString.Slice(pos)); + + return pos; - // Output all of the numbers before the zero sequence - for (int i = fromInclusive; i < zeroStart; i++) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int FormatByte(uint number, Span addressString) + { + number &= 0xFF; + + if (number >= 10) + { + uint hundreds, tens; + if (number >= 100) + { + (uint hundredsAndTens, number) = Math.DivRem(number, 10); + (hundreds, tens) = Math.DivRem(hundredsAndTens, 10); + + addressString[2] = TChar.CreateTruncating('0' + number); + addressString[1] = TChar.CreateTruncating('0' + tens); + addressString[0] = TChar.CreateTruncating('0' + hundreds); + return 3; + } + + (tens, number) = Math.DivRem(number, 10); + addressString[1] = TChar.CreateTruncating('0' + number); + addressString[0] = TChar.CreateTruncating('0' + tens); + return 2; + } + + addressString[0] = TChar.CreateTruncating('0' + number); + return 1; + } + } + + internal static int FormatIPv6Address(ushort[] address, uint scopeId, Span destination) where TChar : unmanaged, IBinaryInteger + { + int pos = 0; + + if (IPv6AddressHelper.ShouldHaveIpv4Embedded(address)) { - if (needsColon) + // We need to treat the last 2 ushorts as a 4-byte IPv4 address, + // so output the first 6 ushorts normally, followed by the IPv4 address. + AppendSections(address.AsSpan(0, 6), destination, ref pos); + if (destination[pos - 1] != TChar.CreateTruncating(':')) { - buffer.Append(':'); + destination[pos++] = TChar.CreateTruncating(':'); } - needsColon = true; - AppendHex(address[i], buffer); + + pos += FormatIPv4Address(ExtractIPv4Address(address), destination.Slice(pos)); + } + else + { + // No IPv4 address. Output all 8 sections as part of the IPv6 address + // with normal formatting rules. + AppendSections(address.AsSpan(0, 8), destination, ref pos); } - // Output the zero sequence if there is one - if (zeroStart >= 0) + // If there's a scope ID, append it. + if (scopeId != 0) { - buffer.Append("::"); - needsColon = false; - fromInclusive = zeroEnd; + destination[pos++] = TChar.CreateTruncating('%'); + + // TODO https://github.com/dotnet/runtime/issues/84527: Use UInt32 TryFormat for both char and byte once IUtf8SpanFormattable implementation exists + Span chars = stackalloc TChar[10]; + int bytesPos = 10; + do + { + (scopeId, uint digit) = Math.DivRem(scopeId, 10); + chars[--bytesPos] = TChar.CreateTruncating('0' + digit); + } + while (scopeId != 0); + Span used = chars.Slice(bytesPos); + used.CopyTo(destination.Slice(pos)); + pos += used.Length; } - // Output everything after the zero sequence - for (int i = fromInclusive; i < toExclusive; i++) + return pos; + + // Appends each of the numbers in address in indexed range [fromInclusive, toExclusive), + // while also replacing the longest sequence of 0s found in that range with "::", as long + // as the sequence is more than one 0. + static void AppendSections(ReadOnlySpan address, Span destination, ref int offset) { - if (needsColon) + // Find the longest sequence of zeros to be combined into a "::" + (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(address); + bool needsColon = false; + + // Handle a zero sequence if there is one + if (zeroStart >= 0) { - buffer.Append(':'); + // Output all of the numbers before the zero sequence + for (int i = 0; i < zeroStart; i++) + { + if (needsColon) + { + destination[offset++] = TChar.CreateTruncating(':'); + } + needsColon = true; + AppendHex(address[i], destination, ref offset); + } + + // Output the zero sequence if there is one + destination[offset++] = TChar.CreateTruncating(':'); + destination[offset++] = TChar.CreateTruncating(':'); + needsColon = false; + } + + // Output everything after the zero sequence + for (int i = zeroEnd; i < address.Length; i++) + { + if (needsColon) + { + destination[offset++] = TChar.CreateTruncating(':'); + } + needsColon = true; + AppendHex(address[i], destination, ref offset); } - needsColon = true; - AppendHex(address[i], buffer); } - } - /// Appends a number as hexadecimal (without the leading "0x") to the StringBuilder. - private static void AppendHex(ushort value, StringBuilder buffer) - { - if ((value & 0xF000) != 0) - buffer.Append(HexConverter.ToCharLower(value >> 12)); + // Appends a number as hexadecimal (without the leading "0x") + static void AppendHex(ushort value, Span destination, ref int offset) + { + if ((value & 0xFFF0) != 0) + { + if ((value & 0xFF00) != 0) + { + if ((value & 0xF000) != 0) + { + destination[offset++] = TChar.CreateTruncating(HexConverter.ToCharLower(value >> 12)); + } - if ((value & 0xFF00) != 0) - buffer.Append(HexConverter.ToCharLower(value >> 8)); + destination[offset++] = TChar.CreateTruncating(HexConverter.ToCharLower(value >> 8)); + } - if ((value & 0xFFF0) != 0) - buffer.Append(HexConverter.ToCharLower(value >> 4)); + destination[offset++] = TChar.CreateTruncating(HexConverter.ToCharLower(value >> 4)); + } - buffer.Append(HexConverter.ToCharLower(value)); + destination[offset++] = TChar.CreateTruncating(HexConverter.ToCharLower(value)); + } } /// Extracts the IPv4 address from the end of the IPv6 address byte array. private static uint ExtractIPv4Address(ushort[] address) { - uint ipv4address = (uint)address[6] << 16 | (uint)address[7]; + uint ipv4address = (uint)address[6] << 16 | address[7]; return (uint)IPAddress.HostToNetworkOrder(unchecked((int)ipv4address)); } } diff --git a/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs b/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs index 58f772cc632f6..a667a8a3de87f 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPNetwork.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Net.Sockets; using System.Runtime.InteropServices; +using System.Text.Unicode; #pragma warning disable SA1648 // TODO: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3595 @@ -20,7 +21,7 @@ namespace System.Net /// In other words, is always the first usable address of the network. /// The constructor and the parsing methods will throw in case there are non-zero bits after the prefix. /// - public readonly struct IPNetwork : IEquatable, ISpanFormattable, ISpanParsable + public readonly struct IPNetwork : IEquatable, ISpanFormattable, ISpanParsable, IUtf8SpanFormattable { private readonly IPAddress? _baseAddress; @@ -299,6 +300,10 @@ obj is IPNetwork other && bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => TryFormat(destination, out charsWritten); + /// + bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider) => + Utf8.TryWrite(utf8Destination, CultureInfo.InvariantCulture, $"{BaseAddress}/{(uint)PrefixLength}", out bytesWritten); + /// static IPNetwork IParsable.Parse([NotNull] string s, IFormatProvider? provider) => Parse(s); diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs index e007fd57d650d..92b1fd3df8ead 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Net.Sockets; +using System.Text; using Xunit; namespace System.Net.Primitives.Functional.Tests @@ -37,6 +38,7 @@ public class IPAddressParsingFormatting_Span : IPAddressParsingFormatting public override IPAddress Parse(string ipString) => IPAddress.Parse(ipString.AsSpan()); public override bool TryParse(string ipString, out IPAddress address) => IPAddress.TryParse(ipString.AsSpan(), out address); public virtual bool TryFormat(IPAddress address, Span destination, out int charsWritten) => address.TryFormat(destination, out charsWritten); + public virtual bool TryFormat(IPAddress address, Span utf8Destination, out int bytesWritten) => ((IUtf8SpanFormattable)address).TryFormat(utf8Destination, out bytesWritten, default, null); [Theory] [MemberData(nameof(ValidIpv4Addresses))] @@ -45,10 +47,49 @@ public void TryFormat_ProvidedBufferTooSmall_Failure(string addressString, strin { _ = expected; IPAddress address = Parse(addressString); - var result = new char[address.ToString().Length - 1]; - Assert.False(TryFormat(address, new Span(result), out int charsWritten)); - Assert.Equal(0, charsWritten); - Assert.Equal(new char[result.Length], result); + + // UTF16 + { + var result = new char[address.ToString().Length - 1]; + Assert.False(TryFormat(address, new Span(result), out int charsWritten)); + Assert.Equal(0, charsWritten); + } + + // UTF8 + { + var result = new byte[address.ToString().Length - 1]; + Assert.False(TryFormat(address, new Span(result), out int bytesWritten)); + Assert.Equal(0, bytesWritten); + } + } + + [Theory] + [MemberData(nameof(ValidIpv4Addresses))] + [MemberData(nameof(ValidIpv6Addresses))] + public void TryFormat_ProvidedBufferExactRightSize_Success(string addressString, string expected) + { + IPAddress address = Parse(addressString); + int requiredLength = address.ToString().Length; + + // UTF16 + { + var exactRequired = new char[requiredLength]; + Assert.True(TryFormat(address, new Span(exactRequired), out int charsWritten)); + Assert.Equal(expected.Length, charsWritten); + Assert.Equal( + address.AddressFamily == AddressFamily.InterNetworkV6 ? expected.ToLowerInvariant() : expected, + new string(exactRequired)); + } + + // UTF8 + { + var exactRequired = new byte[requiredLength]; + Assert.True(TryFormat(address, new Span(exactRequired), out int bytesWritten)); + Assert.Equal(expected.Length, bytesWritten); + Assert.Equal( + address.AddressFamily == AddressFamily.InterNetworkV6 ? expected.ToLowerInvariant() : expected, + Encoding.UTF8.GetString(exactRequired)); + } } [Theory] @@ -57,18 +98,27 @@ public void TryFormat_ProvidedBufferTooSmall_Failure(string addressString, strin public void TryFormat_ProvidedBufferLargerThanNeeded_Success(string addressString, string expected) { IPAddress address = Parse(addressString); + int requiredLength = address.ToString().Length; - const int IPv4MaxLength = 15; // TryFormat currently requires at least this amount of space for IPv4 addresses - int requiredLength = address.AddressFamily == AddressFamily.InterNetwork ? - IPv4MaxLength : - address.ToString().Length; - - var largerThanRequired = new char[requiredLength + 1]; - Assert.True(TryFormat(address, new Span(largerThanRequired), out int charsWritten)); - Assert.Equal(expected.Length, charsWritten); - Assert.Equal( - address.AddressFamily == AddressFamily.InterNetworkV6 ? expected.ToLowerInvariant() : expected, - new string(largerThanRequired, 0, charsWritten)); + // UTF16 + { + var largerThanRequired = new char[requiredLength + 1]; + Assert.True(TryFormat(address, new Span(largerThanRequired), out int charsWritten)); + Assert.Equal(expected.Length, charsWritten); + Assert.Equal( + address.AddressFamily == AddressFamily.InterNetworkV6 ? expected.ToLowerInvariant() : expected, + new string(largerThanRequired, 0, charsWritten)); + } + + // UTF8 + { + var largerThanRequired = new byte[requiredLength + 1]; + Assert.True(TryFormat(address, new Span(largerThanRequired), out int charsWritten)); + Assert.Equal(expected.Length, charsWritten); + Assert.Equal( + address.AddressFamily == AddressFamily.InterNetworkV6 ? expected.ToLowerInvariant() : expected, + Encoding.UTF8.GetString(largerThanRequired.AsSpan(0, charsWritten))); + } } } diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs index 56434733cef04..c4371cf0cf2f9 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs @@ -1,6 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; using Xunit; namespace System.Net.Primitives.Functional.Tests @@ -237,30 +241,60 @@ public void Equals_WhenNull_ReturnsFalse() Assert.False(network.Equals(null)); } - [Fact] - public void TryFormatSpan_EnoughLength_Succeeds() + public static IEnumerable CidrInputs() => + new[] + { + "127.0.0.0/24", + "172.16.0.0/12", + "10.0.0.0/16", + "192.168.2.0/24", + }.Select(s => new object[] { s }); + + [Theory] + [MemberData(nameof(CidrInputs))] + public void TryFormatSpan_NotEnoughLength_ReturnsFalse(string input) { - var input = "127.0.0.0/24"; - var network = IPNetwork.Parse(input); + IPNetwork network = IPNetwork.Parse(input); - Span span = stackalloc char[15]; // IPAddress.TryFormat requires a size of 15 + // UTF16 + { + Span span = stackalloc char[input.Length - 1]; + Assert.False(network.TryFormat(span, out int charsWritten)); + Assert.Equal(0, charsWritten); + } - Assert.True(network.TryFormat(span, out int charsWritten)); - Assert.Equal(input.Length, charsWritten); - Assert.Equal(input, span.Slice(0, charsWritten).ToString()); + // UTF8 + { + Span span = stackalloc byte[input.Length - 1]; + Assert.False(((IUtf8SpanFormattable)network).TryFormat(span, out int bytesWritten, default, null)); + Assert.Equal(0, bytesWritten); + } } [Theory] - [InlineData("127.127.127.127/32", 15)] - [InlineData("127.127.127.127/32", 0)] - [InlineData("127.127.127.127/32", 1)] - public void TryFormatSpan_NotEnoughLength_ReturnsFalse(string input, int spanLengthToTest) + [MemberData(nameof(CidrInputs))] + public void TryFormatSpan_EnoughLength_Succeeds(string input) { - var network = IPNetwork.Parse(input); + IPNetwork network = IPNetwork.Parse(input); - Span span = stackalloc char[spanLengthToTest]; - - Assert.False(network.TryFormat(span, out int charsWritten)); + for (int additionalLength = 0; additionalLength < 3; additionalLength++) + { + // UTF16 + { + Span span = stackalloc char[input.Length + additionalLength]; + Assert.True(network.TryFormat(span, out int charsWritten)); + Assert.Equal(input.Length, charsWritten); + Assert.Equal(input, span.Slice(0, charsWritten).ToString()); + } + + // UTF8 + { + Span span = stackalloc byte[input.Length + additionalLength]; + Assert.True(((IUtf8SpanFormattable)network).TryFormat(span, out int bytesWritten, default, null)); + Assert.Equal(input.Length, bytesWritten); + Assert.Equal(input, Encoding.UTF8.GetString(span.Slice(0, bytesWritten))); + } + } } [Fact]