From d39dbacfc2d09f2aeb5c2b2548b973edd27b50cd Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 6 Mar 2023 16:39:52 -0500 Subject: [PATCH] Add IPAddress ISpanFormattable/Parsable implementations (#82913) * Add IPAddress ISpanFormattable/Parsable implementations * Address PR feedback --- .../ref/System.Net.Primitives.cs | 8 +- .../src/System/Net/IPAddress.cs | 34 ++++++++- .../tests/FunctionalTests/IPAddressParsing.cs | 74 ++++++++++++++++++- .../FunctionalTests/IPAddressParsingSpan.cs | 48 ------------ .../tests/FunctionalTests/IPAddressTest.cs | 4 +- .../FunctionalTests/IPEndPointParsing.cs | 14 ++-- ...tem.Net.Primitives.Functional.Tests.csproj | 1 - 7 files changed, 121 insertions(+), 62 deletions(-) delete mode 100644 src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsingSpan.cs 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 6f2dca85a1b6d..393c08923a6e3 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 + public partial class IPAddress : ISpanFormattable, ISpanParsable { public static readonly System.Net.IPAddress Any; public static readonly System.Net.IPAddress Broadcast; @@ -256,10 +256,16 @@ public IPAddress(System.ReadOnlySpan address, long scopeid) { } public static long NetworkToHostOrder(long network) { throw null; } public static System.Net.IPAddress Parse(System.ReadOnlySpan ipSpan) { throw null; } public static System.Net.IPAddress Parse(string ipString) { throw null; } + static IPAddress ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) { throw null; } + static IPAddress IParsable.Parse(string s, IFormatProvider? provider) { throw null; } public override string ToString() { throw null; } + 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; } 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; } + static bool IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out IPAddress? result) { throw null; } public bool TryWriteBytes(System.Span destination, out int bytesWritten) { throw null; } } public partial class IPEndPoint : System.Net.EndPoint 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 9c88e0563c421..39851508d6955 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/IPAddress.cs @@ -9,6 +9,8 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics; +#pragma warning disable SA1648 // TODO: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3595 + namespace System.Net { /// @@ -16,7 +18,7 @@ namespace System.Net /// Provides an Internet Protocol (IP) address. /// /// - public class IPAddress + public class IPAddress : ISpanFormattable, ISpanParsable { 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 }); @@ -236,6 +238,16 @@ public static bool TryParse(ReadOnlySpan ipSpan, [NotNullWhen(true)] out I return (address != null); } + /// + static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [NotNullWhen(true)] out IPAddress? result) => + // provider is explicitly ignored + TryParse(s, out result); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [NotNullWhen(true)] out IPAddress? result) => + // provider is explicitly ignored + TryParse(s, out result); + public static IPAddress Parse(string ipString) { ArgumentNullException.ThrowIfNull(ipString); @@ -248,6 +260,16 @@ public static IPAddress Parse(ReadOnlySpan ipSpan) return IPAddressParser.Parse(ipSpan, tryParse: false)!; } + /// + static IPAddress ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => + // provider is explicitly ignored + Parse(s); + + /// + static IPAddress IParsable.Parse(string s, IFormatProvider? provider) => + // provider is explicitly ignored + Parse(s); + public bool TryWriteBytes(Span destination, out int bytesWritten) { if (IsIPv6) @@ -386,6 +408,11 @@ public override string ToString() => IPAddressParser.IPv4AddressToString(PrivateAddress) : IPAddressParser.IPv6AddressToString(_numbers, PrivateScopeId); + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => + // format and provider are explicitly ignored + ToString(); + public bool TryFormat(Span destination, out int charsWritten) { return IsIPv4 ? @@ -393,6 +420,11 @@ public bool TryFormat(Span destination, out int charsWritten) IPAddressParser.IPv6AddressToString(_numbers, PrivateScopeId, 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); + public static long HostToNetworkOrder(long host) { return BitConverter.IsLittleEndian ? BinaryPrimitives.ReverseEndianness(host) : host; diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs index 77896869f102c..e007fd57d650d 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsing.cs @@ -7,10 +7,11 @@ namespace System.Net.Primitives.Functional.Tests { - public sealed class IPAddressParsing_String : IPAddressParsing + public class IPAddressParsingFormatting_String : IPAddressParsingFormatting { public override IPAddress Parse(string ipString) => IPAddress.Parse(ipString); public override bool TryParse(string ipString, out IPAddress address) => IPAddress.TryParse(ipString, out address); + public virtual string ToString(IPAddress address) => address.ToString(); [Fact] public void Parse_Null_Throws() @@ -20,9 +21,78 @@ public void Parse_Null_Throws() Assert.False(TryParse((string)null, out IPAddress ipAddress)); Assert.Null(ipAddress); } + + [Theory] + [MemberData(nameof(ValidIpv4Addresses))] + [MemberData(nameof(ValidIpv6Addresses))] + public void ToString_MatchesExpected(string addressString, string expected) + { + IPAddress address = Parse(addressString); + Assert.Equal(expected.ToLowerInvariant(), ToString(address)); + } + } + + 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); + + [Theory] + [MemberData(nameof(ValidIpv4Addresses))] + [MemberData(nameof(ValidIpv6Addresses))] + public void TryFormat_ProvidedBufferTooSmall_Failure(string addressString, string expected) + { + _ = 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); + } + + [Theory] + [MemberData(nameof(ValidIpv4Addresses))] + [MemberData(nameof(ValidIpv6Addresses))] + public void TryFormat_ProvidedBufferLargerThanNeeded_Success(string addressString, string expected) + { + IPAddress address = Parse(addressString); + + 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)); + } + } + + public sealed class IPAddressParsingFormatting_IParsable_IFormattable : IPAddressParsingFormatting_String + { + public override IPAddress Parse(string ipString) => Parse(ipString); + public override bool TryParse(string ipString, out IPAddress address) => TryParse(ipString, out address); + public override string ToString(IPAddress address) => ((IFormattable)address).ToString(null, null); + + private static T Parse(string s) where T : IParsable => T.Parse(s, null); + private static bool TryParse(string s, out T result) where T : IParsable => T.TryParse(s, null, out result); + } + + public sealed class IPAddressParsingFormatting_ISpanParsable_ISpanFormattable : IPAddressParsingFormatting_Span + { + public override IPAddress Parse(string ipString) => Parse(ipString); + public override bool TryParse(string ipString, out IPAddress address) => TryParse(ipString, out address); + public override bool TryFormat(IPAddress address, Span destination, out int charsWritten) => ((ISpanFormattable)address).TryFormat(destination, out charsWritten, default, null); + + private static T Parse(string s) where T : ISpanParsable => T.Parse(s.AsSpan(), null); + private static bool TryParse(string s, out T result) where T : ISpanParsable => T.TryParse(s.AsSpan(), null, out result); } - public abstract class IPAddressParsing + public abstract class IPAddressParsingFormatting { public abstract IPAddress Parse(string ipString); public abstract bool TryParse(string ipString, out IPAddress address); diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsingSpan.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsingSpan.cs deleted file mode 100644 index 5734b2276851e..0000000000000 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressParsingSpan.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Net.Sockets; -using Xunit; - -namespace System.Net.Primitives.Functional.Tests -{ - public sealed class IPAddressParsing_Span : IPAddressParsing - { - 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); - - - [Theory] - [MemberData(nameof(ValidIpv4Addresses))] - [MemberData(nameof(ValidIpv6Addresses))] - public void TryFormat_ProvidedBufferLargerThanNeeded_Success(string addressString, string expected) - { - IPAddress address = IPAddress.Parse(addressString); - - 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(address.TryFormat(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)); - } - - [Theory] - [MemberData(nameof(ValidIpv4Addresses))] - [MemberData(nameof(ValidIpv6Addresses))] - public void TryFormat_ProvidedBufferTooSmall_Failure(string addressString, string expected) - { - _ = expected; - IPAddress address = IPAddress.Parse(addressString); - var result = new char[address.ToString().Length - 1]; - Assert.False(address.TryFormat(new Span(result), out int charsWritten)); - Assert.Equal(0, charsWritten); - Assert.Equal(new char[result.Length], result); - } - } -} diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressTest.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressTest.cs index 584a219962138..8e232ced3a163 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressTest.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPAddressTest.cs @@ -304,8 +304,8 @@ public static void GetHashCode_ValidIPAddresses_Success(IPAddress ip) public static IEnumerable GetValidIPAddresses() { - return IPAddressParsing.ValidIpv4Addresses - .Concat(IPAddressParsing.ValidIpv6Addresses) + return IPAddressParsingFormatting.ValidIpv4Addresses + .Concat(IPAddressParsingFormatting.ValidIpv6Addresses) .Select(array => new object[] {IPAddress.Parse((string)array[0])}); } diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPEndPointParsing.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPEndPointParsing.cs index 5c602087376ec..78364545bca3d 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPEndPointParsing.cs +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPEndPointParsing.cs @@ -10,7 +10,7 @@ namespace System.Net.Primitives.Functional.Tests public class IPEndPointParsing { [Theory] - [MemberData(nameof(IPAddressParsing.ValidIpv4Addresses), MemberType = typeof(IPAddressParsing))] // Just borrow the list from IPAddressParsing + [MemberData(nameof(IPAddressParsingFormatting.ValidIpv4Addresses), MemberType = typeof(IPAddressParsingFormatting))] // Just borrow the list from IPAddressParsing public void Parse_ValidEndPoint_IPv4_Success(string address, string expectedAddress) { Parse_ValidEndPoint_Success(address, expectedAddress, true); @@ -62,16 +62,16 @@ private void Parse_ValidEndPoint_Success(string address, string expectedAddress, } [Theory] - [MemberData(nameof(IPAddressParsing.InvalidIpv4Addresses), MemberType = typeof(IPAddressParsing))] - [MemberData(nameof(IPAddressParsing.InvalidIpv4AddressesStandalone), MemberType = typeof(IPAddressParsing))] + [MemberData(nameof(IPAddressParsingFormatting.InvalidIpv4Addresses), MemberType = typeof(IPAddressParsingFormatting))] + [MemberData(nameof(IPAddressParsingFormatting.InvalidIpv4AddressesStandalone), MemberType = typeof(IPAddressParsingFormatting))] public void Parse_InvalidAddress_IPv4_Throws(string address) { Parse_InvalidAddress_Throws(address, true); } [Theory] - [MemberData(nameof(IPAddressParsing.InvalidIpv6Addresses), MemberType = typeof(IPAddressParsing))] - [MemberData(nameof(IPAddressParsing.InvalidIpv6AddressesNoInner), MemberType = typeof(IPAddressParsing))] + [MemberData(nameof(IPAddressParsingFormatting.InvalidIpv6Addresses), MemberType = typeof(IPAddressParsingFormatting))] + [MemberData(nameof(IPAddressParsingFormatting.InvalidIpv6AddressesNoInner), MemberType = typeof(IPAddressParsingFormatting))] public void Parse_InvalidAddress_IPv6_Throws(string address) { Parse_InvalidAddress_Throws(address, false); @@ -106,7 +106,7 @@ private void Parse_InvalidAddress_Throws(string address, bool isIPv4) } [Theory] - [MemberData(nameof(IPAddressParsing.ValidIpv4Addresses), MemberType = typeof(IPAddressParsing))] + [MemberData(nameof(IPAddressParsingFormatting.ValidIpv4Addresses), MemberType = typeof(IPAddressParsingFormatting))] public void Parse_InvalidPort_IPv4_Throws(string address, string expectedAddress) { _ = expectedAddress; @@ -114,7 +114,7 @@ public void Parse_InvalidPort_IPv4_Throws(string address, string expectedAddress } [Theory] - [MemberData(nameof(IPAddressParsing.ValidIpv6Addresses), MemberType = typeof(IPAddressParsing))] + [MemberData(nameof(IPAddressParsingFormatting.ValidIpv6Addresses), MemberType = typeof(IPAddressParsingFormatting))] public void Parse_InvalidPort_IPv6_Throws(string address, string expectedAddress) { _ = expectedAddress; diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/System.Net.Primitives.Functional.Tests.csproj b/src/libraries/System.Net.Primitives/tests/FunctionalTests/System.Net.Primitives.Functional.Tests.csproj index 05ec279eaf2c3..628da5bd9ca22 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/System.Net.Primitives.Functional.Tests.csproj +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/System.Net.Primitives.Functional.Tests.csproj @@ -15,7 +15,6 @@ -