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]